Add display impl, rm outputManage, print and console.log default to browser console (#749)

* Add display impl, remove outputManage, print and console.log defaults to terminal

* Fixing tests

* Lint

* Erase unecessary code, add cuter CSS formating for errors, fix problems around REPL output

* Add fix to repl2 and lint

* lint

* Allow for list of display, fix elif to else

* Add better global option

* test work

* xfails

* (antocuni, mariana): let's try to start again with TDD methodology: write the minimum test and code for a simple display()

* (antocuni, mariana): this test works out of the box

* WIP: this test is broken, mariana is going to fix it

* add a failing test

* Add ability to deal with targets

* Add append arg and append tests

* Add multiple values to display

* Small adjustments to tests. I noticed I wasn;t running all at some point

* add display test

* Add console tests

* Add async tests

* Fix repl tests

* Fixing merging issues

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address antocuni's review

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixing more tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* linting

* Improve repl tests

* Change my test so codespell is hapy with it

* Test: change test_runtime_config to use json instead of toml to see if stops failing on CI

* kill this file: it is a merge artifact since it was renamed into test_py_config.py on the main branch

* Change test execution order and add async tests to async test file

Co-authored-by: Antonio Cuni <anto.cuni@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Mariana Meireles
2022-10-17 16:28:40 +02:00
committed by GitHub
parent beb3aa1574
commit 1587273868
18 changed files with 459 additions and 180 deletions

View File

@@ -111,45 +111,19 @@ export class BaseEvalElement extends HTMLElement {
this.preEvaluate();
let source: string;
let output: string;
try {
source = this.source ? await this.getSourceFromFile(this.source)
: this.getSourceFromElement();
this._register_esm(runtime);
<string>await runtime.run(
`output_manager.change(out="${this.outputElement.id}", err="${this.errorElement.id}", append=${this.appendOutput ? 'True' : 'False'})`,
);
output = <string>await runtime.run(source);
if (output !== undefined) {
if (Element === undefined) {
Element = <Element>runtime.globals.get('Element');
}
const out = Element(this.outputElement.id);
out.write.callKwargs(output, { append: this.appendOutput });
this.outputElement.hidden = false;
this.outputElement.style.display = 'block';
try {
<string>await runtime.run(`set_current_display_target(target_id="${this.id}")`);
<string>await runtime.run(source);
} finally {
<string>await runtime.run(`set_current_display_target(target_id=None)`);
}
await runtime.run(`output_manager.revert()`);
// check if this REPL contains errors, delete them and remove error classes
const errorElements = document.querySelectorAll(`div[id^='${this.errorElement.id}'][error]`);
if (errorElements.length > 0) {
errorElements.forEach( errorElement =>
{
errorElement.classList.add('hidden');
if (this.hasAttribute('std-err')) {
this.errorElement.hidden = true;
this.errorElement.style.removeProperty('display');
}
}
)
}
removeClasses(this.errorElement, ['bg-red-200', 'p-2']);
removeClasses(this.errorElement, ['py-error']);
this.postEvaluate();
} catch (err) {
logger.error(err);
@@ -159,7 +133,7 @@ export class BaseEvalElement extends HTMLElement {
}
const out = Element(this.errorElement.id);
addClasses(this.errorElement, ['bg-red-200', 'p-2']);
addClasses(this.errorElement, ['py-error']);
out.write.callKwargs(err.toString(), { append: this.appendOutput });
if (this.errorElement.children.length === 0){
this.errorElement.setAttribute('error', '');

View File

@@ -89,34 +89,6 @@ export function make_PyWidget(runtime: Runtime) {
createWidget(runtime, this.name, this.code, this.klass);
}
initOutErr(): void {
if (this.hasAttribute('output')) {
this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output'));
// in this case, the default output-mode is append, if hasn't been specified
if (!this.hasAttribute('output-mode')) {
this.setAttribute('output-mode', 'append');
}
} else {
if (this.hasAttribute('std-out')) {
this.outputElement = document.getElementById(this.getAttribute('std-out'));
} else {
// In this case neither output or std-out have been provided so we need
// to create a new output div to output to
this.outputElement = document.createElement('div');
this.outputElement.classList.add('output');
this.outputElement.hidden = true;
this.outputElement.id = this.id + '-' + this.getAttribute('exec-id');
}
if (this.hasAttribute('std-err')) {
this.errorElement = document.getElementById(this.getAttribute('std-err'));
} else {
this.errorElement = this.outputElement;
}
}
}
async getSourceFromFile(s: string): Promise<string> {
const response = await fetch(s);
return await response.text();

View File

@@ -1,7 +1,6 @@
import asyncio
import base64
import io
import sys
import time
from textwrap import dedent
@@ -124,6 +123,33 @@ class PyScript:
)
def set_current_display_target(target_id):
get_current_display_target._id = target_id
def get_current_display_target():
return get_current_display_target._id
get_current_display_target._id = None
def display(*values, target=None, append=True):
default_target = get_current_display_target()
if default_target is None and target is None:
raise Exception(
"Implicit target not allowed here. Please use display(..., target=...)"
)
if target is not None:
for v in values:
Element(target).write(v, append=append)
else:
for v in values:
Element(default_target).write(v, append=append)
class Element:
def __init__(self, element_id, element=None):
self._id = element_id
@@ -365,59 +391,4 @@ class PyListTemplate:
pass
class OutputCtxManager:
def __init__(self, out=None, output_to_console=True, append=True):
self._out = out
self._prev = out
self.output_to_console = output_to_console
self._append = append
def change(self, out=None, output_to_console=True, append=True):
self._prev = self._out
self._out = out
self.output_to_console = output_to_console
self._append = append
def revert(self):
self._out = self._prev
def write(self, value):
if self._out:
Element(self._out).write(value, self._append)
if self.output_to_console:
console.info(value)
class OutputManager:
def __init__(self, out=None, err=None, output_to_console=True, append=True):
sys.stdout = self._out_manager = OutputCtxManager(
out=out, output_to_console=output_to_console, append=append
)
sys.stderr = self._err_manager = OutputCtxManager(
out=err, output_to_console=output_to_console, append=append
)
self.output_to_console = output_to_console
self._append = append
def change(self, out=None, err=None, output_to_console=True, append=True):
self._out_manager.change(
out=out, output_to_console=output_to_console, append=append
)
sys.stdout = self._out_manager
self._err_manager.change(
out=err, output_to_console=output_to_console, append=append
)
sys.stderr = self._err_manager
self.output_to_console = output_to_console
self._append = append
def revert(self):
self._out_manager.revert()
self._err_manager.revert()
sys.stdout = self._out_manager
sys.stderr = self._err_manager
pyscript = PyScript()
output_manager = OutputManager()

View File

@@ -89,6 +89,7 @@ html {
opacity: 1;
}
color: #0f172a;
.py-pop-up {
text-align: center;
width: 600px;
@@ -107,7 +108,16 @@ html {
right: 5%;
}
.py-box {
/* Pop-up second layer end */
.py-error{
background-color: rgb(254 226 226);
border: solid;
border-color: #fca5a5;
color: #ff0000;
}
.py-box{
display: flex;
flex-direction: row;
justify-content: flex-start;

View File

@@ -8,7 +8,7 @@ class TestBasic(PyScriptTest):
self.pyscript_run(
"""
<py-script>
print('hello pyscript')
display('hello pyscript')
</py-script>
"""
)

View File

@@ -1,36 +0,0 @@
from .support import PyScriptTest
class TestAsync(PyScriptTest):
def test_multiple_async(self):
self.pyscript_run(
"""
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('A', i)
await asyncio.sleep(0.1)
</py-script>
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('B', i)
await asyncio.sleep(0.1)
js.console.log("async tadone")
</py-script>
"""
)
self.wait_for_console("async tadone")
assert self.console.log.lines == [
"Python initialization complete",
"A 0",
"B 0",
"A 1",
"B 1",
"A 2",
"B 2",
"async tadone",
]

View File

@@ -0,0 +1,247 @@
import re
from .support import PyScriptTest
class TestOutuput(PyScriptTest):
def test_simple_display(self):
self.pyscript_run(
"""
<py-script>
display('hello world')
</py-script>
"""
)
inner_html = self.page.content()
pattern = r'<div id="py-.*">hello world</div>'
assert re.search(pattern, inner_html)
def test_consecutive_display(self):
self.pyscript_run(
"""
<py-script>
display('hello 1')
</py-script>
<py-script>
display('hello 2')
</py-script>
"""
)
# need to improve this to get the first/second input
# instead of just searching for it in the page
inner_html = self.page.content()
first_pattern = r'<div id="py-.*?-2">hello 1</div>'
assert re.search(first_pattern, inner_html)
second_pattern = r'<div id="py-.*?-3">hello 2</div>'
assert re.search(second_pattern, inner_html)
assert first_pattern is not second_pattern
def test_multiple_display_calls_same_tag(self):
self.pyscript_run(
"""
<py-script>
display('hello')
display('world')
</py-script>
"""
)
inner_html = self.page.content()
pattern = r'<div id="py-.*?-2">hello</div>'
assert re.search(pattern, inner_html)
pattern = r'<div id="py-.*?-3">world</div>'
assert re.search(pattern, inner_html)
def test_no_implicit_target(self):
self.pyscript_run(
"""
<py-script>
def display_hello():
# this fails because we don't have any implicit target
# from event handlers
display('hello')
</py-script>
<button id="my-button" py-onClick="display_hello()">Click me</button>
"""
)
self.page.locator("text=Click me").click()
text = self.page.text_content("body")
assert "hello" not in text
# currently the test infrastructure doesn't allow to easily assert that
# js exceptions were raised this is a workaround but we need a better fix.
# Antonio promised to write it
assert len(self._page_errors) == 1
console_text = self._page_errors
assert (
"Implicit target not allowed here. Please use display(..., target=...)"
in console_text[0].message
)
self._page_errors = []
def test_explicit_target_pyscript_tag(self):
self.pyscript_run(
"""
<py-script>
def display_hello():
display('hello', target='second-pyscript-tag')
</py-script>
<py-script id="second-pyscript-tag">
display_hello()
</py-script>
"""
)
text = self.page.locator("id=second-pyscript-tag-2").inner_text()
assert "hello" in text
def test_explicit_target_on_button_tag(self):
self.pyscript_run(
"""
<py-script>
def display_hello():
display('hello', target='my-button')
</py-script>
<button id="my-button" py-onClick="display_hello()">Click me</button>
"""
)
self.page.locator("text=Click me").click()
text = self.page.locator("id=my-button").inner_text()
assert "hello" in text
def test_explicit_different_target_from_call(self):
self.pyscript_run(
"""
<py-script id="first-pyscript-tag">
def display_hello():
display('hello', target='second-pyscript-tag')
</py-script>
<py-script id="second-pyscript-tag">
print('nothing to see here')
</py-script>
<py-script>
display_hello()
</py-script>
"""
)
text = self.page.locator("id=second-pyscript-tag").all_inner_texts()
assert "hello" in text
def test_append_true(self):
self.pyscript_run(
"""
<py-script>
display('hello world', append=True)
</py-script>
"""
)
inner_html = self.page.content()
pattern = r'<div id="py-.*">hello world</div>'
assert re.search(pattern, inner_html)
def test_append_false(self):
self.pyscript_run(
"""
<py-script>
display('hello world', append=False)
</py-script>
"""
)
inner_html = self.page.content()
pattern = r'<py-script id="py-.*">hello world</py-script>'
assert re.search(pattern, inner_html)
def test_display_multiple_values(self):
self.pyscript_run(
"""
<py-script>
hello = 'hello'
world = 'world'
display(hello, world)
</py-script>
"""
)
inner_text = self.page.inner_text("html")
assert inner_text == "hello\nworld"
def test_display_list_dict_tuple(self):
self.pyscript_run(
"""
<py-script>
l = ['A', 1, '!']
d = {'B': 2, 'List': l}
t = ('C', 3, '!')
display(l, d, t)
</py-script>
"""
)
inner_text = self.page.inner_text("html")
print(inner_text)
assert (
inner_text
== "['A', 1, '!']\n{'B': 2, 'List': ['A', 1, '!']}\n('C', 3, '!')"
)
def test_image_display(self):
self.pyscript_run(
"""
<py-config> packages = [ "matplotlib"] </py-config>
<py-script>
import matplotlib.pyplot as plt
xpoints = [3, 6, 9]
ypoints = [1, 2, 3]
plt.plot(xpoints, ypoints)
plt.show()
</py-script>
"""
)
inner_html = self.page.content()
pattern = r'<style id="matplotlib-figure-styles">'
assert re.search(pattern, inner_html)
def test_empty_HTML_and_console_output(self):
self.pyscript_run(
"""
<py-script>
print('print from python')
console.log('print from js')
console.error('error from js');
</py-script>
"""
)
inner_html = self.page.content()
assert re.search("", inner_html)
console_text = self.console.all.lines
assert "print from python" in console_text
assert "print from js" in console_text
assert "error from js" in console_text
def test_text_HTML_and_console_output(self):
self.pyscript_run(
"""
<py-script>
display('0')
print('print from python')
console.log('print from js')
console.error('error from js');
</py-script>
"""
)
inner_text = self.page.inner_text("html")
assert "0" == inner_text
console_text = self.console.all.lines
assert "print from python" in console_text
assert "print from js" in console_text
assert "error from js" in console_text
def test_console_line_break(self):
self.pyscript_run(
"""
<py-script>
print('1print\\n2print')
print('1console\\n2console')
</py-script>
"""
)
console_text = self.console.all.lines
assert console_text.index("1print") == (console_text.index("2print") - 1)
assert console_text.index("1console") == (console_text.index("2console") - 1)

View File

@@ -0,0 +1,75 @@
import re
from .support import PyScriptTest
class TestAsync(PyScriptTest):
def test_multiple_async(self):
self.pyscript_run(
"""
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('A', i)
await asyncio.sleep(0.1)
</py-script>
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('B', i)
await asyncio.sleep(0.1)
js.console.log("async tadone")
</py-script>
"""
)
self.wait_for_console("async tadone")
assert self.console.log.lines == [
"Python initialization complete",
"A 0",
"B 0",
"A 1",
"B 1",
"A 2",
"B 2",
"async tadone",
]
def test_multiple_async_display(self):
self.pyscript_run(
"""
<py-script id="py1">
def say_hello():
display('hello')
</py-script>
<py-script id="py2">
say_hello()
</py-script>
"""
)
inner_html = self.page.content()
pattern = r'<div id="py2-2">hello</div>'
assert re.search(pattern, inner_html)
def test_multiple_async_multiple_display(self):
self.pyscript_run(
"""
<py-script id='pyA'>
import asyncio
for i in range(2):
display('A')
await asyncio.sleep(0)
</py-script>
<py-script id='pyB'>
import asyncio
for i in range(2):
display('B')
await asyncio.sleep(0)
</py-script>
"""
)
inner_text = self.page.inner_text("html")
assert "A\nB\nA\nB" in inner_text

View File

@@ -39,8 +39,8 @@ class TestStyle(PyScriptTest):
<py-config>
name = "foo"
</py-config>
<py-script>print("hello")</py-script>
<py-repl>print("hello")</py-repl>
<py-script>display("hello")</py-script>
<py-repl>display("hello")</py-repl>
<py-title>hello</py-title>
<py-inputbox label="my input">
import js

View File

@@ -95,7 +95,7 @@ class TestConfig(PyScriptTest):
import sys, js
pyodide_version = sys.modules["pyodide"].__version__
js.console.log("version", pyodide_version)
pyodide_version
display(pyodide_version)
</py-script>
""",
)

View File

@@ -1,3 +1,6 @@
import pytest
from playwright.sync_api import expect
from .support import PyScriptTest
@@ -20,15 +23,15 @@ class TestPyRepl(PyScriptTest):
"""
)
self.page.locator("py-repl").type("print(2+2)")
self.page.locator("py-repl").type('display("hello")')
# We only have one button in the page
self.page.locator("button").click()
# The result gets the id of the repl + n
repl_result = self.page.wait_for_selector("#my-repl-1", state="attached")
repl_result = self.page.wait_for_selector("#my-repl-2", state="attached")
assert repl_result.inner_text() == "4"
assert repl_result.inner_text() == "hello"
def test_repl_runs_with_shift_enter(self):
self.pyscript_run(
@@ -36,10 +39,77 @@ class TestPyRepl(PyScriptTest):
<py-repl id="my-repl" auto-generate="true"> </py-repl>
"""
)
self.page.locator("py-repl").type("2+2")
self.page.locator("py-repl").type('display("hello")')
# Confirm that we get a result by using the keys shortcut
self.page.keyboard.press("Shift+Enter")
py_repl = self.page.query_selector("#my-repl-2")
assert "hello" in py_repl.inner_text()
def test_repl_console_ouput(self):
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
"""
)
self.page.locator("py-repl").type("print('apple')")
self.page.keyboard.press("Enter")
self.page.locator("py-repl").type("console.log('banana')")
self.page.locator("button").click()
# The result gets the id of the repl + n
repl_result = self.page.wait_for_selector("#my-repl-1", state="attached")
assert repl_result.text_content() == "4"
assert repl_result.inner_text() == ""
def test_repl_error_ouput(self):
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
"""
)
self.page.locator("py-repl").type("this is an error")
self.page.locator("button").click()
expect(self.page.locator(".py-error")).to_be_visible()
# console errors are observable on the headed instance
# but is just not possible to access them using the self object
@pytest.mark.xfail(reason="Cannot access console errors")
def test_repl_error_ouput_console(self):
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
"""
)
self.page.locator("py-repl").type("this is an error")
self.page.locator("button").click()
def test_repl_error_and_fail_moving_forward_ouput(self):
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
"""
)
self.page.locator("py-repl").type("this is an error")
self.page.locator("button").click()
expect(self.page.locator(".py-error")).to_be_visible()
self.page.keyboard.press("Shift+Enter")
expect(self.page.locator(".py-error")).to_be_visible()
# this tests the fact that a new error div should be created once there's
# an error but also that it should disappear automatically once the error
# is fixed
def test_repl_show_error_fix_error_check_for_ouput(self):
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
"""
)
self.page.locator("py-repl").type("d")
self.page.keyboard.press("Shift+Enter")
expect(self.page.locator(".py-error")).to_be_visible()
self.page.keyboard.press("Backspace")
self.page.locator("py-repl").type("display('ok')")
self.page.keyboard.press("Shift+Enter")
repl_result = self.page.wait_for_selector("#my-repl-2", state="attached")
assert repl_result.inner_text() == "ok"

View File

@@ -258,20 +258,18 @@ class TestExamples(PyScriptTest):
assert self.page.title() == "REPL"
wait_for_render(self.page, "*", "<py-repl.*?>")
self.page.locator("py-repl").type("print('Hello, World!')")
self.page.locator("py-repl").type("display('Hello, World!')")
self.page.locator("button").click()
assert self.page.locator("#my-repl-1").text_content() == "Hello, World!"
assert self.page.locator("#my-repl-2").text_content() == "Hello, World!"
# Confirm that using the second repl still works properly
self.page.locator("#my-repl-2").type("2*2")
self.page.locator("#my-repl-2").type("display(2*2)")
self.page.keyboard.press("Shift+Enter")
# Make sure that the child of the second repl is attached properly
# before looking into the text_content
second_repl_result = self.page.wait_for_selector(
"#my-repl-2-2", state="attached"
)
assert second_repl_result.text_content() == "4"
assert self.page.wait_for_selector("#my-repl-2-1", state="attached")
assert self.page.locator("#my-repl-2-1").text_content() == "4"
def test_repl2(self):
self.goto("examples/repl2.html")
@@ -279,7 +277,7 @@ class TestExamples(PyScriptTest):
assert self.page.title() == "Custom REPL Example"
wait_for_render(self.page, "*", "<py-repl.*?>")
# confirm we can import utils and run one command
self.page.locator("py-repl").type("import utils\nutils.now()")
self.page.locator("py-repl").type("import utils\ndisplay(utils.now())")
self.page.locator("button").click()
# Make sure the output is in the page
self.page.wait_for_selector("#output")