From cbedd43833bf0739feedc406edb6fe5bef5a7ddd Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 21 Oct 2022 15:36:27 +0200 Subject: [PATCH 01/32] move the code for registering ES modules into main.ts, and add a big comment which explains some of the problems of the current approach. But I don't want to tackled it in this PR --- pyscriptjs/src/components/base.ts | 34 --------------------- pyscriptjs/src/components/pyscript.ts | 30 ------------------ pyscriptjs/src/main.ts | 44 +++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 64 deletions(-) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index 2cc47962205..a8df25a7d50 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -74,39 +74,6 @@ export class BaseEvalElement extends HTMLElement { return this.code; } - protected async _register_esm(runtime: Runtime): Promise { - const imports: { [key: string]: unknown } = {}; - const nodes = document.querySelectorAll("script[type='importmap']"); - const importmaps: Array = []; - nodes.forEach( node => - { - let importmap; - try { - importmap = JSON.parse(node.textContent); - if (importmap?.imports == null) return; - importmaps.push(importmap); - } catch { - return; - } - } - ) - for (const importmap of importmaps){ - for (const [name, url] of Object.entries(importmap.imports)) { - if (typeof name != 'string' || typeof url != 'string') continue; - - try { - // XXX: pyodide doesn't like Module(), failing with - // "can't read 'name' of undefined" at import time - imports[name] = { ...(await import(url)) }; - } catch { - logger.error(`failed to fetch '${url}' for '${name}'`); - } - } - } - - runtime.registerJsModule('esm', imports); - } - async evaluate(runtime: Runtime): Promise { this.preEvaluate(); @@ -114,7 +81,6 @@ export class BaseEvalElement extends HTMLElement { try { source = this.source ? await this.getSourceFromFile(this.source) : this.getSourceFromElement(); - this._register_esm(runtime); try { await runtime.run(`set_current_display_target(target_id="${this.id}")`); diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 4233ba7834a..bc9cc86a8b9 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -64,36 +64,6 @@ export class PyScript extends BaseEvalElement { } } - protected async _register_esm(runtime: Runtime): Promise { - for (const node of document.querySelectorAll("script[type='importmap']")) { - const importmap = (() => { - try { - return JSON.parse(node.textContent); - } catch { - return null; - } - })(); - - if (importmap?.imports == null) continue; - - for (const [name, url] of Object.entries(importmap.imports)) { - if (typeof name != 'string' || typeof url != 'string') continue; - - let exports: object; - try { - // XXX: pyodide doesn't like Module(), failing with - // "can't read 'name' of undefined" at import time - exports = { ...(await import(url)) }; - } catch { - logger.warn(`failed to fetch '${url}' for '${name}'`); - continue; - } - - runtime.registerJsModule(name, exports); - } - } - } - getSourceFromElement(): string { return htmlDecode(this.code); } diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index ded7a4f83d5..5c3b36bfe25 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -194,11 +194,55 @@ class PyScriptApp { // lifecycle (7) executeScripts(runtime: Runtime) { + this.register_importmap(runtime); for (const script of scriptsQueue_) { void script.evaluate(runtime); } scriptsQueue.set([]); } + + async register_importmap(runtime: Runtime) { + // make importmap ES modules available from python using 'import'. + // + // XXX: this code can probably be improved as it hides too many + // errors. Moreover at the time of writing we don't really have a test + // for it and this functionality is used only by the d3 example. We + // might want to rethink the whole approach at some point. E.g., maybe + // we should move it to py-config? + // + // Moreover, it's also wrong because it's async and currently we don't + // await the module to be fully registered before executing the code + // inside py-script. It's also unclear whether we want to wait or not + // (or maybe only wait only if we do an actual 'import'?) + for (const node of document.querySelectorAll("script[type='importmap']")) { + const importmap = (() => { + try { + return JSON.parse(node.textContent); + } catch { + return null; + } + })(); + + if (importmap?.imports == null) continue; + + for (const [name, url] of Object.entries(importmap.imports)) { + if (typeof name != 'string' || typeof url != 'string') continue; + + let exports: object; + try { + // XXX: pyodide doesn't like Module(), failing with + // "can't read 'name' of undefined" at import time + exports = { ...(await import(url)) }; + } catch { + logger.warn(`failed to fetch '${url}' for '${name}'`); + continue; + } + + runtime.registerJsModule(name, exports); + } + } + } + } function pyscript_get_config() { From 667ed3e2ca8e2b87203a9e74ed0ca24ab18daa62 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 21 Oct 2022 15:50:59 +0200 Subject: [PATCH 02/32] start to move the logic for executing python code into pyExec --- pyscriptjs/src/components/base.ts | 8 ++------ pyscriptjs/src/pyexec.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 pyscriptjs/src/pyexec.ts diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index a8df25a7d50..112e926d4c9 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -1,6 +1,7 @@ import { guidGenerator, addClasses, removeClasses } from '../utils'; import type { Runtime } from '../runtime'; import { getLogger } from '../logger'; +import { pyExec } from '../pyexec'; const logger = getLogger('pyscript/base'); @@ -82,12 +83,7 @@ export class BaseEvalElement extends HTMLElement { source = this.source ? await this.getSourceFromFile(this.source) : this.getSourceFromElement(); - try { - await runtime.run(`set_current_display_target(target_id="${this.id}")`); - await runtime.run(source); - } finally { - await runtime.run(`set_current_display_target(target_id=None)`); - } + await pyExec(runtime, source, this.id); removeClasses(this.errorElement, ['py-error']); this.postEvaluate(); diff --git a/pyscriptjs/src/pyexec.ts b/pyscriptjs/src/pyexec.ts new file mode 100644 index 00000000000..7c0f9bc734a --- /dev/null +++ b/pyscriptjs/src/pyexec.ts @@ -0,0 +1,14 @@ +import type { Runtime } from './runtime'; + +type OutputMode = "append" | "replace"; + +export async function pyExec(runtime: Runtime, pysrc: string, targetID: string) +{ + try { + await runtime.run(`set_current_display_target(target_id="${targetID}")`); + await runtime.run(pysrc); + } + finally { + await runtime.run(`set_current_display_target(target_id=None)`); + } +} From f31274acc29efc76af192e1247cdffbcb15e3ed8 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 21 Oct 2022 15:54:25 +0200 Subject: [PATCH 03/32] kill unused code --- pyscriptjs/src/components/base.ts | 5 ----- pyscriptjs/src/components/pyrepl.ts | 5 ----- 2 files changed, 10 deletions(-) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index 112e926d4c9..12c09a153d9 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -29,11 +29,6 @@ export class BaseEvalElement extends HTMLElement { this.setOutputMode("append"); } - addToOutput(s: string) { - this.outputElement.innerHTML += '
' + s + '
'; - this.outputElement.hidden = false; - } - setOutputMode(defaultMode = "append") { const mode = this.hasAttribute('output-mode') ? this.getAttribute('output-mode') : defaultMode; diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index a6b8231f03b..a3a9b0ac05d 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -147,11 +147,6 @@ export function make_PyRepl(runtime: Runtime) { logger.debug(`element ${this.id} successfully connected`); } - addToOutput(s: string): void { - this.outputElement.innerHTML += '
' + s + '
'; - this.outputElement.hidden = false; - } - preEvaluate(): void { this.setOutputMode("replace"); if(!this.appendOutput) { From 41e6debc4d061f304b0a46192e99a1866527281e Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 21 Oct 2022 17:09:32 +0200 Subject: [PATCH 04/32] Introduce pytest --no-fake-server. When using this option, you use a real http server instead of using playwright's internal routing. There are cases in which it's useful, in particular: 1) if you want to test the page on different browser 2) if you want to use the chrome dev tools, because apparently when using the fake server chromium is unable to locate the sourcemaps This commit reintroduces some of the code which was killed by PR #829. --- pyscriptjs/tests/integration/conftest.py | 50 +++++++++++++++++++ pyscriptjs/tests/integration/support.py | 24 ++++++--- .../tests/integration/test_00_support.py | 2 +- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/pyscriptjs/tests/integration/conftest.py b/pyscriptjs/tests/integration/conftest.py index 92b34c19304..18c2c946a9c 100644 --- a/pyscriptjs/tests/integration/conftest.py +++ b/pyscriptjs/tests/integration/conftest.py @@ -1,4 +1,8 @@ """All data required for testing examples""" +import threading +from http.server import HTTPServer as SuperHTTPServer +from http.server import SimpleHTTPRequestHandler + import pytest from .support import Logger @@ -7,3 +11,49 @@ @pytest.fixture(scope="session") def logger(): return Logger() + + +def pytest_addoption(parser): + parser.addoption( + "--no-fake-server", + action="store_true", + help="Use a real HTTP server instead of http://fakeserver", + ) + + +class HTTPServer(SuperHTTPServer): + """ + Class for wrapper to run SimpleHTTPServer on Thread. + Ctrl +Only Thread remains dead when terminated with C. + Keyboard Interrupt passes. + """ + + def run(self): + try: + self.serve_forever() + except KeyboardInterrupt: + pass + finally: + self.server_close() + + +@pytest.fixture(scope="session") +def http_server(logger): + class MyHTTPRequestHandler(SimpleHTTPRequestHandler): + def log_message(self, fmt, *args): + logger.log("http_server", fmt % args, color="blue") + + host, port = "127.0.0.1", 8080 + base_url = f"http://{host}:{port}" + + # serve_Run forever under thread + server = HTTPServer((host, port), MyHTTPRequestHandler) + + thread = threading.Thread(None, server.run) + thread.start() + + yield base_url # Transition to test here + + # End thread + server.shutdown() + thread.join() diff --git a/pyscriptjs/tests/integration/support.py b/pyscriptjs/tests/integration/support.py index 90b51abf43d..6c54462ca0f 100644 --- a/pyscriptjs/tests/integration/support.py +++ b/pyscriptjs/tests/integration/support.py @@ -70,11 +70,19 @@ def init(self, request, tmpdir, logger, page): tmpdir.join("build").mksymlinkto(BUILD) self.tmpdir.chdir() self.logger = logger - self.fake_server = "http://fake_server" - self.router = SmartRouter( - "fake_server", logger=logger, usepdb=request.config.option.usepdb - ) - self.router.install(page) + + if request.config.option.no_fake_server: + # use a real HTTP server. Note that as soon as we request the + # fixture, the server automatically starts in its own thread. + self.http_server = request.getfixturevalue("http_server") + else: + # use the internal playwright routing + self.http_server = "http://fake_server" + self.router = SmartRouter( + "fake_server", logger=logger, usepdb=request.config.option.usepdb + ) + self.router.install(page) + # self.init_page(page) # # this extra print is useful when using pytest -s, else we start printing @@ -157,7 +165,7 @@ def writefile(self, filename, content): def goto(self, path): self.logger.reset() self.logger.log("page.goto", path, color="yellow") - url = f"{self.fake_server}/{path}" + url = f"{self.http_server}/{path}" self.page.goto(url, timeout=0) def wait_for_console(self, text, *, timeout=None, check_errors=True): @@ -224,8 +232,8 @@ def pyscript_run(self, snippet, *, extra_head="", wait_for_pyscript=True): doc = f""" - - + + {extra_head} diff --git a/pyscriptjs/tests/integration/test_00_support.py b/pyscriptjs/tests/integration/test_00_support.py index 6f83004257e..902fb124a64 100644 --- a/pyscriptjs/tests/integration/test_00_support.py +++ b/pyscriptjs/tests/integration/test_00_support.py @@ -91,7 +91,7 @@ def test_check_errors(self): # stack trace msg = str(exc.value) assert "Error: this is an error" in msg - assert f"at {self.fake_server}/mytest.html" in msg + assert f"at {self.http_server}/mytest.html" in msg # # after a call to check_errors, the errors are cleared self.check_errors() From 507466a704fccebffda73ef2cf1f3d04ab63ac1f Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Fri, 21 Oct 2022 17:29:10 +0200 Subject: [PATCH 05/32] remove the cast, it's not necessary --- pyscriptjs/src/pyexec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyscriptjs/src/pyexec.ts b/pyscriptjs/src/pyexec.ts index 7c0f9bc734a..d22ea6e3686 100644 --- a/pyscriptjs/src/pyexec.ts +++ b/pyscriptjs/src/pyexec.ts @@ -5,10 +5,10 @@ type OutputMode = "append" | "replace"; export async function pyExec(runtime: Runtime, pysrc: string, targetID: string) { try { - await runtime.run(`set_current_display_target(target_id="${targetID}")`); - await runtime.run(pysrc); + await runtime.run(`set_current_display_target(target_id="${targetID}")`); + await runtime.run(pysrc); } finally { - await runtime.run(`set_current_display_target(target_id=None)`); + await runtime.run(`set_current_display_target(target_id=None)`); } } From f8888d32de1672c05c1d10d0a3039588ee47692b Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 00:17:50 +0200 Subject: [PATCH 06/32] kill the support for std-err: it was unused and it will be refactored --- pyscriptjs/src/components/pyrepl.ts | 5 +---- pyscriptjs/src/components/pyscript.ts | 6 +----- pyscriptjs/tests/unit/pyscript.test.ts | 10 ---------- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index a3a9b0ac05d..c6fb6070989 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -137,9 +137,7 @@ export function make_PyRepl(runtime: Runtime) { mainDiv.appendChild(this.outputElement); } - this.errorElement = this.hasAttribute('std-err') - ? document.getElementById(this.getAttribute('std-err')) - : this.outputElement; + this.errorElement = this.outputElement; } this.appendChild(mainDiv); @@ -185,7 +183,6 @@ export function make_PyRepl(runtime: Runtime) { addReplAttribute('output'); addReplAttribute('std-out'); - addReplAttribute('std-err'); newPyRepl.setAttribute('exec-id', nextExecId.toString()); this.parentElement.appendChild(newPyRepl); diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index bc9cc86a8b9..1a32afac639 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -49,11 +49,7 @@ export class PyScript extends BaseEvalElement { mainDiv.appendChild(this.outputElement); } - if (this.hasAttribute('std-err')) { - this.errorElement = document.getElementById(this.getAttribute('std-err')); - } else { - this.errorElement = this.outputElement; - } + this.errorElement = this.outputElement; } this.appendChild(mainDiv); diff --git a/pyscriptjs/tests/unit/pyscript.test.ts b/pyscriptjs/tests/unit/pyscript.test.ts index 9b8d5d95b4a..e5cebb0a192 100644 --- a/pyscriptjs/tests/unit/pyscript.test.ts +++ b/pyscriptjs/tests/unit/pyscript.test.ts @@ -43,16 +43,6 @@ describe('PyScript', () => { expect(instance.outputElement.getAttribute('id')).toBe("std-out") }) - it('confirm that std-err id element sets errorElement', async () => { - expect(instance.outputElement).toBe(undefined); - - instance.setAttribute('id', 'std-err') - instance.connectedCallback(); - - // We should have an errorElement - expect(instance.errorElement.getAttribute('id')).toBe("std-err") - }) - it('test output attribute path', async () => { expect(instance.outputElement).toBe(undefined); expect(instance.errorElement).toBe(undefined) From b1cd5a2de045a059b82913ee7284858d04068189 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 00:28:09 +0200 Subject: [PATCH 07/32] kill support for std-out as well --- pyscriptjs/src/components/pyrepl.ts | 22 ++++++++-------------- pyscriptjs/src/components/pyscript.ts | 21 +++++++-------------- 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index c6fb6070989..669c479a7fa 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -123,19 +123,14 @@ export function make_PyRepl(runtime: Runtime) { if (this.hasAttribute('output')) { this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output')); } 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'); - - // add the output div id if there's not output pre-defined - mainDiv.appendChild(this.outputElement); - } + // 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'); + + // add the output div id if there's not output pre-defined + mainDiv.appendChild(this.outputElement); this.errorElement = this.outputElement; } @@ -182,7 +177,6 @@ export function make_PyRepl(runtime: Runtime) { }; addReplAttribute('output'); - addReplAttribute('std-out'); newPyRepl.setAttribute('exec-id', nextExecId.toString()); this.parentElement.appendChild(newPyRepl); diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 1a32afac639..f8c427d2448 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -34,21 +34,14 @@ export class PyScript extends BaseEvalElement { 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 - - // Let's check if we have an id first and create one if not - this.outputElement = document.createElement('div'); - const exec_id = this.getAttribute('exec-id'); - this.outputElement.id = this.id + (exec_id ? '-' + exec_id : ''); - - // add the output div id if there's not output pre-defined - mainDiv.appendChild(this.outputElement); - } + // Let's check if we have an id first and create one if not + this.outputElement = document.createElement('div'); + const exec_id = this.getAttribute('exec-id'); + this.outputElement.id = this.id + (exec_id ? '-' + exec_id : ''); + + // add the output div id if there's not output pre-defined + mainDiv.appendChild(this.outputElement); this.errorElement = this.outputElement; } From 1134c84f0a3e9d0693002e500ae01a44196c66d0 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 00:48:02 +0200 Subject: [PATCH 08/32] kill kill kill --- pyscriptjs/src/components/pyscript.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index f8c427d2448..3685ca58966 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -33,16 +33,6 @@ export class PyScript extends BaseEvalElement { if (!this.hasAttribute('output-mode')) { this.setAttribute('output-mode', 'append'); } - } else { - - // Let's check if we have an id first and create one if not - this.outputElement = document.createElement('div'); - const exec_id = this.getAttribute('exec-id'); - this.outputElement.id = this.id + (exec_id ? '-' + exec_id : ''); - - // add the output div id if there's not output pre-defined - mainDiv.appendChild(this.outputElement); - this.errorElement = this.outputElement; } this.appendChild(mainDiv); From b3fc5bce9b020a9fe853e6dc277c60204da89a50 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 10:20:17 +0200 Subject: [PATCH 09/32] add a pytest option --dev which is like --headed but also automatically open the devtools panel --- pyscriptjs/tests/integration/conftest.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pyscriptjs/tests/integration/conftest.py b/pyscriptjs/tests/integration/conftest.py index 18c2c946a9c..fe8a7453e6a 100644 --- a/pyscriptjs/tests/integration/conftest.py +++ b/pyscriptjs/tests/integration/conftest.py @@ -19,6 +19,29 @@ def pytest_addoption(parser): action="store_true", help="Use a real HTTP server instead of http://fakeserver", ) + parser.addoption( + "--dev", + action="store_true", + help="Automatically open a devtools panel. Implies --headed", + ) + + +@pytest.fixture(scope="session") +def browser_type_launch_args(request): + """ + Override the browser_type_launch_args defined by pytest-playwright to + support --devtools. + + NOTE: this has been tested with pytest-playwright==0.3.0. It might break + with newer versions of it. + """ + if request.config.option.dev: + request.config.option.headed = True + # this calls the "original" fixture defined by pytest_playwright.py + launch_options = request.getfixturevalue("browser_type_launch_args") + if request.config.option.dev: + launch_options["devtools"] = True + return launch_options class HTTPServer(SuperHTTPServer): From 179befd026f88d77f261dcac642a9f3d8880b889 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 10:36:56 +0200 Subject: [PATCH 10/32] typo --- pyscriptjs/tests/integration/test_02_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscriptjs/tests/integration/test_02_output.py b/pyscriptjs/tests/integration/test_02_output.py index 9e769eec6fb..17e2b1c0a28 100644 --- a/pyscriptjs/tests/integration/test_02_output.py +++ b/pyscriptjs/tests/integration/test_02_output.py @@ -3,7 +3,7 @@ from .support import PyScriptTest -class TestOutuput(PyScriptTest): +class TestOutput(PyScriptTest): def test_simple_display(self): self.pyscript_run( """ From 14c6537acc541f3a3ac2e61ee6dd2249095e85cb Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 11:54:33 +0200 Subject: [PATCH 11/32] improve this test and mark it as xfail, see issue #878 --- .../tests/integration/test_02_output.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pyscriptjs/tests/integration/test_02_output.py b/pyscriptjs/tests/integration/test_02_output.py index 17e2b1c0a28..4c201b31a68 100644 --- a/pyscriptjs/tests/integration/test_02_output.py +++ b/pyscriptjs/tests/integration/test_02_output.py @@ -1,5 +1,7 @@ import re +import pytest + from .support import PyScriptTest @@ -16,26 +18,23 @@ def test_simple_display(self): pattern = r'
hello world
' assert re.search(pattern, inner_html) + @pytest.mark.xfail(reason="issue #878") def test_consecutive_display(self): self.pyscript_run( """ display('hello 1') +

hello 2

- display('hello 2') + display('hello 3') - """ + """ ) - # 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'
hello 1
' - assert re.search(first_pattern, inner_html) - second_pattern = r'
hello 2
' - assert re.search(second_pattern, inner_html) - - assert first_pattern is not second_pattern + inner_text = self.page.inner_text("body") + lines = inner_text.splitlines() + lines = [line for line in lines if line != ""] # remove empty lines + assert lines == ["hello 1", "hello 2", "hello 3"] def test_multiple_display_calls_same_tag(self): self.pyscript_run( From 33b03a4cb36e5ff376adbe1ab2355c52e975080c Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 12:00:02 +0200 Subject: [PATCH 12/32] add a failing test --- pyscriptjs/tests/integration/test_02_output.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pyscriptjs/tests/integration/test_02_output.py b/pyscriptjs/tests/integration/test_02_output.py index 4c201b31a68..f977e3ce11f 100644 --- a/pyscriptjs/tests/integration/test_02_output.py +++ b/pyscriptjs/tests/integration/test_02_output.py @@ -36,6 +36,19 @@ def test_consecutive_display(self): lines = [line for line in lines if line != ""] # remove empty lines assert lines == ["hello 1", "hello 2", "hello 3"] + @pytest.mark.xfail(reason="fix me") + def test_output_attribute(self): + self.pyscript_run( + """ + + display('hello world') + +
+ """ + ) + mydiv = self.page.locator("#mydiv") + assert mydiv.inner_text() == "hello world" + def test_multiple_display_calls_same_tag(self): self.pyscript_run( """ From 9f75fc856f12615c5995898f6cb14e867d0022eb Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 12:29:15 +0200 Subject: [PATCH 13/32] kill more unneeded code and adapt the tests accordingly --- pyscriptjs/src/components/pyscript.ts | 16 ---------------- pyscriptjs/tests/integration/test_02_output.py | 14 ++++++-------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 3685ca58966..123302eb30e 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -12,8 +12,6 @@ const logger = getLogger('py-script'); export class PyScript extends BaseEvalElement { constructor() { super(); - - // add an extra div where we can attach the codemirror editor this.shadow.appendChild(this.wrapper); } @@ -22,20 +20,6 @@ export class PyScript extends BaseEvalElement { this.code = htmlDecode(this.innerHTML); this.innerHTML = ''; - const mainDiv = document.createElement('div'); - addClasses(mainDiv, ['output']); - // add Editor to main PyScript div - - 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'); - } - } - - this.appendChild(mainDiv); addToScriptsQueue(this); if (this.hasAttribute('src')) { diff --git a/pyscriptjs/tests/integration/test_02_output.py b/pyscriptjs/tests/integration/test_02_output.py index f977e3ce11f..57f6d49903d 100644 --- a/pyscriptjs/tests/integration/test_02_output.py +++ b/pyscriptjs/tests/integration/test_02_output.py @@ -58,11 +58,9 @@ def test_multiple_display_calls_same_tag(self): """ ) - inner_html = self.page.content() - pattern = r'
hello
' - assert re.search(pattern, inner_html) - pattern = r'
world
' - assert re.search(pattern, inner_html) + tag = self.page.locator("py-script") + lines = tag.inner_text().splitlines() + assert lines == ["hello", "world"] def test_no_implicit_target(self): self.pyscript_run( @@ -101,10 +99,10 @@ def display_hello(): display_hello() - """ + """ ) - text = self.page.locator("id=second-pyscript-tag-2").inner_text() - assert "hello" in text + text = self.page.locator("id=second-pyscript-tag").inner_text() + assert text == "hello" def test_explicit_target_on_button_tag(self): self.pyscript_run( From 591534cc198b9a336314b4cf295c94050df77dfc Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 12:55:52 +0200 Subject: [PATCH 14/32] kill guidGenerator and use sequential numbers instead, they are much easier to test and to use in the debugger for humans --- pyscriptjs/src/components/base.ts | 4 ++-- pyscriptjs/src/utils.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index 12c09a153d9..1d67552c391 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -1,4 +1,4 @@ -import { guidGenerator, addClasses, removeClasses } from '../utils'; +import { ensureUniqueId, addClasses, removeClasses } from '../utils'; import type { Runtime } from '../runtime'; import { getLogger } from '../logger'; import { pyExec } from '../pyexec'; @@ -57,7 +57,7 @@ export class BaseEvalElement extends HTMLElement { } checkId() { - if (!this.id) this.id = 'py-' + guidGenerator(); + ensureUniqueId(this); } getSourceFromElement(): string { diff --git a/pyscriptjs/src/utils.ts b/pyscriptjs/src/utils.ts index 62c882cbaab..d3f243d2af7 100644 --- a/pyscriptjs/src/utils.ts +++ b/pyscriptjs/src/utils.ts @@ -40,11 +40,10 @@ function ltrim(code: string): string { : code; } -function guidGenerator(): string { - const S4 = function (): string { - return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); - }; - return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4(); +let _uniqueIdCounter = 0; +function ensureUniqueId(el: HTMLElement) { + if (el.id === "") + el.id = "py-internal-" + _uniqueIdCounter++; } /* @@ -110,4 +109,5 @@ function globalExport(name: string, obj: any) { -export { addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError, readTextFromPath, inJest, globalExport, }; +export { addClasses, removeClasses, getLastPath, ltrim, htmlDecode, ensureUniqueId, + showError, handleFetchError, readTextFromPath, inJest, globalExport, }; From e2f4b8f834cad8ac61cc999eb88c64095bd500b5 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 13:05:00 +0200 Subject: [PATCH 15/32] refactor pyExec to take an HTMLElement instead of an id --- pyscriptjs/src/components/base.ts | 2 +- pyscriptjs/src/pyexec.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index 1d67552c391..fecdf196130 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -78,7 +78,7 @@ export class BaseEvalElement extends HTMLElement { source = this.source ? await this.getSourceFromFile(this.source) : this.getSourceFromElement(); - await pyExec(runtime, source, this.id); + await pyExec(runtime, source, this); removeClasses(this.errorElement, ['py-error']); this.postEvaluate(); diff --git a/pyscriptjs/src/pyexec.ts b/pyscriptjs/src/pyexec.ts index d22ea6e3686..2686f898383 100644 --- a/pyscriptjs/src/pyexec.ts +++ b/pyscriptjs/src/pyexec.ts @@ -1,14 +1,18 @@ +import { ensureUniqueId } from './utils'; import type { Runtime } from './runtime'; type OutputMode = "append" | "replace"; -export async function pyExec(runtime: Runtime, pysrc: string, targetID: string) +export async function pyExec(runtime: Runtime, pysrc: string, out: HTMLElement) { + // this is the python function defined in pyscript.py + const set_current_display_target = runtime.globals.get('set_current_display_target'); + ensureUniqueId(out); + set_current_display_target(out.id); try { - await runtime.run(`set_current_display_target(target_id="${targetID}")`); await runtime.run(pysrc); } finally { - await runtime.run(`set_current_display_target(target_id=None)`); + set_current_display_target(undefined); } } From c8ba605ad011d9a3c7fcfd316a6660f9049db2d2 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 19:02:35 +0200 Subject: [PATCH 16/32] make PyScript a standalone class and remove the intheritance from BaseEvalElement. This contains a bit of mechnical copy&paste but I will simplify the code in the next commits --- pyscriptjs/src/components/pyscript.ts | 29 +++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 123302eb30e..2f5932f1476 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -2,21 +2,28 @@ import { addToScriptsQueue, } from '../stores'; -import { addClasses, htmlDecode } from '../utils'; -import { BaseEvalElement } from './base'; +import { htmlDecode, ensureUniqueId } from '../utils'; import type { Runtime } from '../runtime'; import { getLogger } from '../logger'; +import { pyExec } from '../pyexec'; const logger = getLogger('py-script'); -export class PyScript extends BaseEvalElement { +export class PyScript extends HTMLElement { + shadow: ShadowRoot; + wrapper: HTMLElement; + code: string; + source: string; + constructor() { super(); + this.shadow = this.attachShadow({ mode: 'open' }); + this.wrapper = document.createElement('slot'); this.shadow.appendChild(this.wrapper); } connectedCallback() { - this.checkId(); + ensureUniqueId(this); this.code = htmlDecode(this.innerHTML); this.innerHTML = ''; @@ -30,6 +37,20 @@ export class PyScript extends BaseEvalElement { getSourceFromElement(): string { return htmlDecode(this.code); } + + async getSourceFromFile(s: string): Promise { + const response = await fetch(s); + this.code = await response.text(); + return this.code; + } + + async evaluate(runtime: Runtime): Promise { + const pySourceCode = this.source ? await this.getSourceFromFile(this.source) + : this.getSourceFromElement(); + + await pyExec(runtime, pySourceCode, this); + } + } /** Defines all possible py-on* and their corresponding event types */ From 76280c4f1cc2479076fc3f98ec9e937e3702c4be Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 19:04:50 +0200 Subject: [PATCH 17/32] simplify this test. With test_pyscript_hello we just want to check that we can execute a very simple python code, no need to check the behavior of display() --- pyscriptjs/tests/integration/test_01_basic.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pyscriptjs/tests/integration/test_01_basic.py b/pyscriptjs/tests/integration/test_01_basic.py index f11e53d0935..e039c3dc63e 100644 --- a/pyscriptjs/tests/integration/test_01_basic.py +++ b/pyscriptjs/tests/integration/test_01_basic.py @@ -1,5 +1,3 @@ -import re - from .support import PyScriptTest @@ -8,16 +6,15 @@ def test_pyscript_hello(self): self.pyscript_run( """ - display('hello pyscript') + import js + js.console.log('hello pyscript') - """ + """ ) - # this is a very ugly way of checking the content of the DOM. If we - # find ourselves to write a lot of code in this style, we will - # probably want to write a nicer API for it. - inner_html = self.page.locator("py-script").inner_html() - pattern = r'
hello pyscript
' - assert re.search(pattern, inner_html) + assert self.console.log.lines == [ + self.PY_COMPLETE, + "hello pyscript", + ] def test_execution_in_order(self): """ From dcdb020dd848d972634a4ce2e3fb8fe29a7dcde4 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 20:18:26 +0200 Subject: [PATCH 18/32] woooo, finally kill the last remaining store, scriptQueue. Instead of using a queue of scripts, we immediately run the python code inside connectedCallback. This has also the advantage that if we dynamically add a tag to the DOM, the python code is automatically executed, as the new test demonstrates (the test would have been failed before this commit) --- pyscriptjs/src/components/pyscript.ts | 70 ++++++++----------- pyscriptjs/src/main.ts | 25 +++---- pyscriptjs/src/stores.ts | 8 --- pyscriptjs/tests/integration/test_01_basic.py | 20 ++++++ 4 files changed, 59 insertions(+), 64 deletions(-) delete mode 100644 pyscriptjs/src/stores.ts diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 2f5932f1476..4ea2c5b1ca4 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -1,7 +1,3 @@ -import { - addToScriptsQueue, -} from '../stores'; - import { htmlDecode, ensureUniqueId } from '../utils'; import type { Runtime } from '../runtime'; import { getLogger } from '../logger'; @@ -9,48 +5,44 @@ import { pyExec } from '../pyexec'; const logger = getLogger('py-script'); -export class PyScript extends HTMLElement { - shadow: ShadowRoot; - wrapper: HTMLElement; - code: string; - source: string; - - constructor() { - super(); - this.shadow = this.attachShadow({ mode: 'open' }); - this.wrapper = document.createElement('slot'); - this.shadow.appendChild(this.wrapper); - } - - connectedCallback() { - ensureUniqueId(this); - this.code = htmlDecode(this.innerHTML); - this.innerHTML = ''; +export function make_PyScript(runtime: Runtime) { - addToScriptsQueue(this); + class PyScript extends HTMLElement { + shadow: ShadowRoot; + wrapper: HTMLElement; - if (this.hasAttribute('src')) { - this.source = this.getAttribute('src'); + constructor() { + super(); + this.shadow = this.attachShadow({ mode: 'open' }); + this.wrapper = document.createElement('slot'); + this.shadow.appendChild(this.wrapper); } - } - - getSourceFromElement(): string { - return htmlDecode(this.code); - } - async getSourceFromFile(s: string): Promise { - const response = await fetch(s); - this.code = await response.text(); - return this.code; - } - - async evaluate(runtime: Runtime): Promise { - const pySourceCode = this.source ? await this.getSourceFromFile(this.source) - : this.getSourceFromElement(); + async connectedCallback() { + ensureUniqueId(this); + const pySrc = this.getPySrc(); + this.innerHTML = ''; + await pyExec(runtime, pySrc, this); + } - await pyExec(runtime, pySourceCode, this); + getPySrc(): string { + if (this.hasAttribute('src')) { + throw new Error('implement me'); + } + else { + return htmlDecode(this.innerHTML); + } + } + /* + async getSourceFromFile(s: string): Promise { + const response = await fetch(s); + this.code = await response.text(); + return this.code; + } + */ } + return PyScript; } /** Defines all possible py-on* and their corresponding event types */ diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index 5c3b36bfe25..bc9a215e7b1 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -3,23 +3,16 @@ import './styles/pyscript_base.css'; import { loadConfigFromElement } from './pyconfig'; import type { AppConfig } from './pyconfig'; import type { Runtime } from './runtime'; -import { PyScript, initHandlers, mountElements } from './components/pyscript'; +import { make_PyScript, initHandlers, mountElements } from './components/pyscript'; import { PyLoader } from './components/pyloader'; import { PyodideRuntime } from './pyodide'; import { getLogger } from './logger'; -import { scriptsQueue } from './stores'; import { handleFetchError, showError, globalExport } from './utils' import { createCustomElements } from './components/elements'; const logger = getLogger('pyscript/main'); -let scriptsQueue_: PyScript[]; -scriptsQueue.subscribe((value: PyScript[]) => { - scriptsQueue_ = value; -}); - - /* High-level overview of the lifecycle of a PyScript App: @@ -37,7 +30,8 @@ scriptsQueue.subscribe((value: PyScript[]) => { 6. setup the environment, install packages - 7. run user scripts + 7. connect the py-script web component. This causes the execution of all the + user scripts 8. initialize the rest of web components such as py-button, py-repl, etc. @@ -57,11 +51,11 @@ class PyScriptApp { config: AppConfig; loader: PyLoader; + PyScript: any; // XXX would be nice to have a more precise type for the class itself // lifecycle (1) main() { this.loadConfig(); - customElements.define('py-script', PyScript); this.showLoader(); this.loadRuntime(); } @@ -128,7 +122,6 @@ class PyScriptApp { // // Invariant: this.config and this.loader are set and available. async afterRuntimeLoad(runtime: Runtime): Promise { - // XXX what is the JS/TS standard way of doing asserts? console.assert(this.config !== undefined); console.assert(this.loader !== undefined); @@ -195,17 +188,15 @@ class PyScriptApp { // lifecycle (7) executeScripts(runtime: Runtime) { this.register_importmap(runtime); - for (const script of scriptsQueue_) { - void script.evaluate(runtime); - } - scriptsQueue.set([]); + this.PyScript = make_PyScript(runtime); + customElements.define('py-script', this.PyScript); } async register_importmap(runtime: Runtime) { // make importmap ES modules available from python using 'import'. // - // XXX: this code can probably be improved as it hides too many - // errors. Moreover at the time of writing we don't really have a test + // XXX: this code can probably be improved because errors are silently + // ignored. Moreover at the time of writing we don't really have a test // for it and this functionality is used only by the d3 example. We // might want to rethink the whole approach at some point. E.g., maybe // we should move it to py-config? diff --git a/pyscriptjs/src/stores.ts b/pyscriptjs/src/stores.ts deleted file mode 100644 index b97cec82bf4..00000000000 --- a/pyscriptjs/src/stores.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { writable } from 'svelte/store'; -import type { PyScript } from './components/pyscript'; - -export const scriptsQueue = writable([]); - -export const addToScriptsQueue = (script: PyScript) => { - scriptsQueue.update(scriptsQueue => [...scriptsQueue, script]); -}; diff --git a/pyscriptjs/tests/integration/test_01_basic.py b/pyscriptjs/tests/integration/test_01_basic.py index e039c3dc63e..0c4fdc76532 100644 --- a/pyscriptjs/tests/integration/test_01_basic.py +++ b/pyscriptjs/tests/integration/test_01_basic.py @@ -94,3 +94,23 @@ def test_packages(self): "Loaded asciitree", # printed by pyodide "hello asciitree", # printed by us ] + + def test_dynamically_add_py_script_tag(self): + self.pyscript_run( + """ + + + """ + ) + self.page.locator("button").click() + self.page.locator("py-script") # wait until appears + assert self.console.log.lines == [ + self.PY_COMPLETE, + "hello world", + ] From dca2ffbb8bd8b9767a99dcf160c05c5905cb1064 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 20:25:19 +0200 Subject: [PATCH 19/32] add a test for and implement the functionality --- pyscriptjs/src/components/pyscript.ts | 18 ++++++++---------- pyscriptjs/tests/integration/test_01_basic.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 4ea2c5b1ca4..aaaa2c7fd63 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -20,26 +20,24 @@ export function make_PyScript(runtime: Runtime) { async connectedCallback() { ensureUniqueId(this); - const pySrc = this.getPySrc(); + const pySrc = await this.getPySrc(); this.innerHTML = ''; await pyExec(runtime, pySrc, this); } - getPySrc(): string { + async getPySrc(): Promise { if (this.hasAttribute('src')) { - throw new Error('implement me'); + // XXX: what happens if the fetch() fails? + // We should handle the case correctly, but in my defense + // this case was broken also before the refactoring. FIXME! + const url = this.getAttribute('src'); + const response = await fetch(url); + return await response.text(); } else { return htmlDecode(this.innerHTML); } } - /* - async getSourceFromFile(s: string): Promise { - const response = await fetch(s); - this.code = await response.text(); - return this.code; - } - */ } return PyScript; diff --git a/pyscriptjs/tests/integration/test_01_basic.py b/pyscriptjs/tests/integration/test_01_basic.py index 0c4fdc76532..3b9218055be 100644 --- a/pyscriptjs/tests/integration/test_01_basic.py +++ b/pyscriptjs/tests/integration/test_01_basic.py @@ -114,3 +114,15 @@ def test_dynamically_add_py_script_tag(self): self.PY_COMPLETE, "hello world", ] + + def test_py_script_src_attribute(self): + self.writefile("foo.py", "print('hello from foo')") + self.pyscript_run( + """ + + """ + ) + assert self.console.log.lines == [ + self.PY_COMPLETE, + "hello from foo", + ] From 69d03aa4c3b7cb631b5abf6bfdfbe50a21dd1a8f Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 20:30:28 +0200 Subject: [PATCH 20/32] we no longer need the shadow root for --- pyscriptjs/src/components/pyscript.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index aaaa2c7fd63..a069db21a89 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -8,15 +8,6 @@ const logger = getLogger('py-script'); export function make_PyScript(runtime: Runtime) { class PyScript extends HTMLElement { - shadow: ShadowRoot; - wrapper: HTMLElement; - - constructor() { - super(); - this.shadow = this.attachShadow({ mode: 'open' }); - this.wrapper = document.createElement('slot'); - this.shadow.appendChild(this.wrapper); - } async connectedCallback() { ensureUniqueId(this); From 499956607a66e874ce64bd6086739da9e88eb01a Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 20:59:42 +0200 Subject: [PATCH 21/32] add the logic to display exceptions which are raised inside , and the relevant test --- pyscriptjs/src/components/base.ts | 5 +- pyscriptjs/src/pyexec.ts | 53 +++++++++++++++++-- pyscriptjs/tests/integration/test_01_basic.py | 28 +++++++++- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index fecdf196130..1f03fd77993 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -1,7 +1,7 @@ import { ensureUniqueId, addClasses, removeClasses } from '../utils'; import type { Runtime } from '../runtime'; import { getLogger } from '../logger'; -import { pyExec } from '../pyexec'; +import { pyExecDontHandleErrors } from '../pyexec'; const logger = getLogger('pyscript/base'); @@ -78,7 +78,8 @@ export class BaseEvalElement extends HTMLElement { source = this.source ? await this.getSourceFromFile(this.source) : this.getSourceFromElement(); - await pyExec(runtime, source, this); + // XXX we should use pyExec and let it display the errors + await pyExecDontHandleErrors(runtime, source, this); removeClasses(this.errorElement, ['py-error']); this.postEvaluate(); diff --git a/pyscriptjs/src/pyexec.ts b/pyscriptjs/src/pyexec.ts index 2686f898383..f59094f515e 100644 --- a/pyscriptjs/src/pyexec.ts +++ b/pyscriptjs/src/pyexec.ts @@ -1,9 +1,56 @@ -import { ensureUniqueId } from './utils'; +import { getLogger } from './logger'; +import { ensureUniqueId, addClasses } from './utils'; import type { Runtime } from './runtime'; -type OutputMode = "append" | "replace"; +const logger = getLogger('pyexec'); -export async function pyExec(runtime: Runtime, pysrc: string, out: HTMLElement) +export async function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) +{ + // this is the python function defined in pyscript.py + const set_current_display_target = runtime.globals.get('set_current_display_target'); + ensureUniqueId(outElem); + set_current_display_target(outElem.id); + try { + try { + await runtime.run(pysrc); + } + catch (err) { + // XXX: currently we display exceptions in the same position as + // the output. But we probably need a better way to do that, + // e.g. allowing plugins to intercept exceptions and display them + // in a configurable way. + displayPyException(err, outElem); + } + } + finally { + set_current_display_target(undefined); + } +} + + +function displayPyException(err: any, errElem: HTMLElement) { + //addClasses(errElem, ['py-error']) + const pre = document.createElement('pre'); + pre.className = "py-error"; + + if (err.name === "PythonError") { + // err.message contains the python-level traceback (i.e. a string + // starting with: "Traceback (most recent call last) ..." + logger.error("Python exception:\n" + err.message); + pre.innerText = err.message; + } + else { + // this is very likely a normal JS exception. The best we can do is to + // display it as is. + logger.error("Non-python exception:\n" + err); + pre.innerText = err; + } + errElem.appendChild(pre); +} + + +// XXX this is used by base.ts but should be removed once we complete the refactoring +export async function pyExecDontHandleErrors(runtime: Runtime, pysrc: string, out: HTMLElement) { // this is the python function defined in pyscript.py const set_current_display_target = runtime.globals.get('set_current_display_target'); diff --git a/pyscriptjs/tests/integration/test_01_basic.py b/pyscriptjs/tests/integration/test_01_basic.py index 3b9218055be..533a40c25ff 100644 --- a/pyscriptjs/tests/integration/test_01_basic.py +++ b/pyscriptjs/tests/integration/test_01_basic.py @@ -6,8 +6,7 @@ def test_pyscript_hello(self): self.pyscript_run( """ - import js - js.console.log('hello pyscript') + print('hello pyscript') """ ) @@ -16,6 +15,31 @@ def test_pyscript_hello(self): "hello pyscript", ] + def test_python_exception(self): + self.pyscript_run( + """ + + print('hello pyscript') + raise Exception('this is an error') + + """ + ) + assert self.console.log.lines == [self.PY_COMPLETE, "hello pyscript"] + # check that we sent the traceback to the console + tb_lines = self.console.error.lines[-1].splitlines() + assert tb_lines[0] == "[pyexec] Python exception:" + assert tb_lines[1] == "Traceback (most recent call last):" + assert tb_lines[-1] == "Exception: this is an error" + # + # check that we show the traceback in the page. Note that here we + # display the "raw" python traceback, without the "[pyexec] Python + # exception:" line (which is useful in the console, but not for the + # user) + pre = self.page.locator("py-script > pre") + tb_lines = pre.inner_text().splitlines() + assert tb_lines[0] == "Traceback (most recent call last):" + assert tb_lines[-1] == "Exception: this is an error" + def test_execution_in_order(self): """ Check that they py-script tags are executed in the same order they are From 50c872ff84a1d68160c58e5d33954f4bf0421ed2 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sat, 22 Oct 2022 23:57:12 +0200 Subject: [PATCH 22/32] kill this test, addToOutput was killed --- pyscriptjs/tests/unit/base.test.ts | 11 ----------- pyscriptjs/tests/unit/pyrepl.test.ts | 13 ------------- 2 files changed, 24 deletions(-) diff --git a/pyscriptjs/tests/unit/base.test.ts b/pyscriptjs/tests/unit/base.test.ts index 595e76a1ca9..47266f3a4f3 100644 --- a/pyscriptjs/tests/unit/base.test.ts +++ b/pyscriptjs/tests/unit/base.test.ts @@ -15,17 +15,6 @@ describe('BaseEvalElement', () => { expect(instance).toBeInstanceOf(BaseEvalElement); }); - it('addToOutput sets outputElements property correctly', async () => { - instance.outputElement = document.createElement('body'); - instance.addToOutput('Hello, world!'); - - expect(instance.outputElement.innerHTML).toBe('
Hello, world!
'); - expect(instance.outputElement.hidden).toBe(false); - - instance.addToOutput('Have a good day!'); - expect(instance.outputElement.innerHTML).toBe('
Hello, world!
Have a good day!
'); - }); - it('setOutputMode updates appendOutput property correctly', async () => { // Confirm that the default mode is 'append' expect(instance.appendOutput).toBe(true); diff --git a/pyscriptjs/tests/unit/pyrepl.test.ts b/pyscriptjs/tests/unit/pyrepl.test.ts index fc4f0b06de2..b72e91cebe0 100644 --- a/pyscriptjs/tests/unit/pyrepl.test.ts +++ b/pyscriptjs/tests/unit/pyrepl.test.ts @@ -52,17 +52,4 @@ describe('PyRepl', () => { // Confirm that our innerHTML is set as well expect(editorNode).toContain("Hello") }) - - it("confirm that addToOutput updates output element", async () => { - expect(instance.outputElement).toBe(undefined) - - // This is just to avoid throwing the test since outputElement is undefined - instance.outputElement = document.createElement("div") - - instance.addToOutput("Hello, World!") - - expect(instance.outputElement.innerHTML).toBe("
Hello, World!
") - expect(instance.outputElement.hidden).toBe(false) - }) - }); From eaacc2189c545dcb563c1dd72f645b90641496b9 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 23 Oct 2022 00:02:58 +0200 Subject: [PATCH 23/32] kill pyscript.test.ts: it doesn't make sense to unit-test it at this level because you would need to mock the whole Runtime, and you would end up testing the mock instead of something actually useful. PyScript is battle-tested a lot by integration tests --- pyscriptjs/tests/unit/pyscript.test.ts | 66 -------------------------- 1 file changed, 66 deletions(-) delete mode 100644 pyscriptjs/tests/unit/pyscript.test.ts diff --git a/pyscriptjs/tests/unit/pyscript.test.ts b/pyscriptjs/tests/unit/pyscript.test.ts deleted file mode 100644 index e5cebb0a192..00000000000 --- a/pyscriptjs/tests/unit/pyscript.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { jest } from "@jest/globals" - -import { PyScript } from "../../src/components/pyscript" - -customElements.define('py-script', PyScript) - -describe('PyScript', () => { - let instance: PyScript; - - beforeEach(() => { - instance = new PyScript(); - }) - - it('PyScript instantiates correctly', async () => { - expect(instance).toBeInstanceOf(PyScript) - }) - - it('connectedCallback gets or sets a new id', async () => { - expect(instance.id).toBe(''); - - instance.connectedCallback(); - const instanceId = instance.id; - // id should be similar to py-4850c8c3-d70d-d9e0-03c1-3cfeb0bcec0d-container - expect(instanceId).toMatch(/py-(\w+-){1,5}/); - - // calling checkId directly should return the same id - instance.checkId(); - expect(instance.id).toEqual(instanceId); - }); - - it('connectedCallback creates output div', async () => { - instance.connectedCallback(); - - expect(instance.innerHTML).toContain('
') - }) - - it('confirm that outputElement has std-out id element', async () => { - expect(instance.outputElement).toBe(undefined); - - instance.setAttribute('id', 'std-out') - instance.connectedCallback(); - - expect(instance.outputElement.getAttribute('id')).toBe("std-out") - }) - - it('test output attribute path', async () => { - expect(instance.outputElement).toBe(undefined); - expect(instance.errorElement).toBe(undefined) - - const createdOutput = document.createElement("output") - - instance.setAttribute('output', 'output') - instance.connectedCallback(); - - expect(instance.innerHTML).toBe('
') - }) - - it('getSourceFromElement returns decoded html', async () => { - instance.innerHTML = "

Hello

" - - instance.connectedCallback(); - const source = instance.getSourceFromElement(); - - expect(source).toBe("

Hello

") - }) -}) From d3c68ea69f5ae6f46f1df31fb2058a754a5e9cfb Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 23 Oct 2022 00:25:51 +0200 Subject: [PATCH 24/32] test_multiple_async_display is not really about async and it's not about multiple display. It is about implicit display() target, so move it it the correct class and improve the test to be less dependent on the exact
id --- pyscriptjs/tests/integration/test_02_output.py | 18 ++++++++++++++++++ pyscriptjs/tests/integration/test_03_async.py | 18 ------------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pyscriptjs/tests/integration/test_02_output.py b/pyscriptjs/tests/integration/test_02_output.py index 57f6d49903d..0b004ce0a95 100644 --- a/pyscriptjs/tests/integration/test_02_output.py +++ b/pyscriptjs/tests/integration/test_02_output.py @@ -62,6 +62,24 @@ def test_multiple_display_calls_same_tag(self): lines = tag.inner_text().splitlines() assert lines == ["hello", "world"] + def test_implicit_target_from_a_different_tag(self): + self.pyscript_run( + """ + + def say_hello(): + display('hello') + + + + say_hello() + + """ + ) + py1 = self.page.locator("#py1") + py2 = self.page.locator("#py2") + assert py1.inner_text() == "" + assert py2.inner_text() == "hello" + def test_no_implicit_target(self): self.pyscript_run( """ diff --git a/pyscriptjs/tests/integration/test_03_async.py b/pyscriptjs/tests/integration/test_03_async.py index 9a32bbb57bb..84fc91ee6ef 100644 --- a/pyscriptjs/tests/integration/test_03_async.py +++ b/pyscriptjs/tests/integration/test_03_async.py @@ -1,5 +1,3 @@ -import re - from .support import PyScriptTest @@ -37,22 +35,6 @@ def test_multiple_async(self): "async tadone", ] - def test_multiple_async_display(self): - self.pyscript_run( - """ - - def say_hello(): - display('hello') - - - say_hello() - - """ - ) - inner_html = self.page.content() - pattern = r'
hello
' - assert re.search(pattern, inner_html) - def test_multiple_async_multiple_display(self): self.pyscript_run( """ From afdf35518605a6ef0a1058777f36983e38ab8eaa Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 23 Oct 2022 00:35:58 +0200 Subject: [PATCH 25/32] PyLoader doesn't have to inherit from BaseEvalElement --- pyscriptjs/src/components/pyloader.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyscriptjs/src/components/pyloader.ts b/pyscriptjs/src/components/pyloader.ts index cd38454fc28..b4708ff4df3 100644 --- a/pyscriptjs/src/components/pyloader.ts +++ b/pyscriptjs/src/components/pyloader.ts @@ -1,9 +1,8 @@ -import { BaseEvalElement } from './base'; import { getLogger } from '../logger'; const logger = getLogger('py-loader'); -export class PyLoader extends BaseEvalElement { +export class PyLoader extends HTMLElement { widths: string[]; label: string; mount_name: string; From b3d43d6fb9d957e58a7d5343b847b92d56767487 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 23 Oct 2022 00:37:34 +0200 Subject: [PATCH 26/32] py-title doesn't have to inherit from BaseEvalElement --- pyscriptjs/src/components/pytitle.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyscriptjs/src/components/pytitle.ts b/pyscriptjs/src/components/pytitle.ts index dcdb036233e..5cd21fe77da 100644 --- a/pyscriptjs/src/components/pytitle.ts +++ b/pyscriptjs/src/components/pytitle.ts @@ -1,7 +1,6 @@ -import { BaseEvalElement } from './base'; import { addClasses, htmlDecode } from '../utils'; -export class PyTitle extends BaseEvalElement { +export class PyTitle extends HTMLElement { widths: string[]; label: string; mount_name: string; From ce1104bfcbb348463d7c9fb1265682fb4f9775ca Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 23 Oct 2022 00:41:15 +0200 Subject: [PATCH 27/32] PyButton doesn't have to inherit from BaseEvalElement --- pyscriptjs/src/components/pybutton.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyscriptjs/src/components/pybutton.ts b/pyscriptjs/src/components/pybutton.ts index 88865e77ad3..79716312e98 100644 --- a/pyscriptjs/src/components/pybutton.ts +++ b/pyscriptjs/src/components/pybutton.ts @@ -1,17 +1,18 @@ -import { BaseEvalElement } from './base'; -import { getAttribute, addClasses, htmlDecode } from '../utils'; +import { getAttribute, addClasses, htmlDecode, ensureUniqueId } from '../utils'; import { getLogger } from '../logger' import type { Runtime } from '../runtime'; const logger = getLogger('py-button'); export function make_PyButton(runtime: Runtime) { - class PyButton extends BaseEvalElement { + class PyButton extends HTMLElement { widths: string[] = []; label: string | undefined = undefined; class: string[]; defaultClass: string[]; mount_name: string | undefined = undefined; + code: string; + constructor() { super(); @@ -41,7 +42,7 @@ export function make_PyButton(runtime: Runtime) { } async connectedCallback() { - this.checkId(); + ensureUniqueId(this); this.code = htmlDecode(this.innerHTML) || ""; this.mount_name = this.id.split('-').join('_'); this.innerHTML = ''; From e345ed5d43dfabde87f07a137092b33993936723 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 23 Oct 2022 00:43:06 +0200 Subject: [PATCH 28/32] PyInputBox doesn't have to inherit from BaseEvalElement --- pyscriptjs/src/components/pyinputbox.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyscriptjs/src/components/pyinputbox.ts b/pyscriptjs/src/components/pyinputbox.ts index 7e76edc94c4..64ce1b216a6 100644 --- a/pyscriptjs/src/components/pyinputbox.ts +++ b/pyscriptjs/src/components/pyinputbox.ts @@ -1,15 +1,16 @@ -import { BaseEvalElement } from './base'; -import { getAttribute, addClasses, htmlDecode } from '../utils'; +import { getAttribute, addClasses, htmlDecode, ensureUniqueId } from '../utils'; import { getLogger } from '../logger' import type { Runtime } from '../runtime'; const logger = getLogger('py-inputbox'); export function make_PyInputBox(runtime: Runtime) { - class PyInputBox extends BaseEvalElement { + class PyInputBox extends HTMLElement { widths: string[] = []; label: string | undefined = undefined; mount_name: string | undefined = undefined; + code: string; + constructor() { super(); @@ -20,7 +21,7 @@ export function make_PyInputBox(runtime: Runtime) { } async connectedCallback() { - this.checkId(); + ensureUniqueId(this); this.code = htmlDecode(this.innerHTML); this.mount_name = this.id.split('-').join('_'); this.innerHTML = ''; From 5d460424234e9d8199a7d878e26a25aa4788379a Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 23 Oct 2022 00:46:52 +0200 Subject: [PATCH 29/32] fix the PyTitle tests --- pyscriptjs/src/components/pytitle.ts | 2 +- pyscriptjs/tests/unit/pytitle.test.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyscriptjs/src/components/pytitle.ts b/pyscriptjs/src/components/pytitle.ts index 5cd21fe77da..c4b63723220 100644 --- a/pyscriptjs/src/components/pytitle.ts +++ b/pyscriptjs/src/components/pytitle.ts @@ -1,4 +1,4 @@ -import { addClasses, htmlDecode } from '../utils'; +import { addClasses, htmlDecode, ensureUniqueId } from '../utils'; export class PyTitle extends HTMLElement { widths: string[]; diff --git a/pyscriptjs/tests/unit/pytitle.test.ts b/pyscriptjs/tests/unit/pytitle.test.ts index dc1a7bd9316..922da432ce7 100644 --- a/pyscriptjs/tests/unit/pytitle.test.ts +++ b/pyscriptjs/tests/unit/pytitle.test.ts @@ -25,16 +25,14 @@ describe("PyTitle", () => { }) it("label renders correctly on the page and updates id", async () => { - instance.innerHTML = "Hello, world!" - // We need this to test mount_name works properly since connectedCallback - // doesn't automatically call checkId (should it?) - instance.checkId(); + instance.innerHTML = "Hello, world!"; + instance.id = "my-fancy-title"; instance.connectedCallback(); expect(instance.label).toBe("Hello, world!") // mount_name should be similar to: py_be025f4c_2150_7f2a_1a85_af92970c2a0e - expect(instance.mount_name).toMatch(/py_(\w+_){1,5}/); + expect(instance.mount_name).toMatch("my_fancy_title"); expect(instance.innerHTML).toContain("

Hello, world!

") }) }) From f999e6249ae90bf1376ca92dc0e20454dd8e96bc Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 23 Oct 2022 00:49:26 +0200 Subject: [PATCH 30/32] fix the pyinputbox and pybutton unit tests --- pyscriptjs/tests/unit/pybutton.test.ts | 5 +++-- pyscriptjs/tests/unit/pyinputbox.test.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyscriptjs/tests/unit/pybutton.test.ts b/pyscriptjs/tests/unit/pybutton.test.ts index cc1658c78bc..8955e246d2e 100644 --- a/pyscriptjs/tests/unit/pybutton.test.ts +++ b/pyscriptjs/tests/unit/pybutton.test.ts @@ -2,6 +2,7 @@ import { jest } from '@jest/globals'; import type { Runtime } from "../../src/runtime" import { FakeRuntime } from "./fakeruntime" import { make_PyButton } from '../../src/components/pybutton'; +import { ensureUniqueId } from '../../src/utils'; const runtime: Runtime = new FakeRuntime(); const PyButton = make_PyButton(runtime); @@ -25,8 +26,8 @@ describe('PyButton', () => { // id should be similar to py-4850c8c3-d70d-d9e0-03c1-3cfeb0bcec0d-container expect(instanceId).toMatch(/py-(\w+-){1,5}container/); - // calling checkId directly should return the same id - instance.checkId(); + // ensureUniqueId doesn't change the ID + ensureUniqueId(instance); expect(instance.id).toEqual(instanceId); }); diff --git a/pyscriptjs/tests/unit/pyinputbox.test.ts b/pyscriptjs/tests/unit/pyinputbox.test.ts index c8d0aeab6e6..991e7cb75b9 100644 --- a/pyscriptjs/tests/unit/pyinputbox.test.ts +++ b/pyscriptjs/tests/unit/pyinputbox.test.ts @@ -2,6 +2,7 @@ import { jest } from "@jest/globals" import type { Runtime } from "../../src/runtime" import { FakeRuntime } from "./fakeruntime" import { make_PyInputBox } from "../../src/components/pyinputbox" +import { ensureUniqueId } from '../../src/utils'; const runtime: Runtime = new FakeRuntime(); const PyInputBox = make_PyInputBox(runtime); @@ -29,8 +30,8 @@ describe("PyInputBox", () => { // id should be similar to py-4850c8c3-d70d-d9e0-03c1-3cfeb0bcec0d-container expect(instanceId).toMatch(/py-(\w+-){1,5}container/); - // calling checkId directly should return the same id - instance.checkId(); + // ensureUniqueId doesn't change the ID + ensureUniqueId(instance); expect(instance.id).toEqual(instanceId); }); From ecac29ad0298c67aad7eaf7a89551894f3a51819 Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 23 Oct 2022 00:51:55 +0200 Subject: [PATCH 31/32] add a comment --- pyscriptjs/src/components/base.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index 2f831004492..2d48596be16 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -1,3 +1,8 @@ +// XXX this should be eventually killed. +// The only remaining class which inherit from BaseEvalElement is PyRepl: we +// should merge the two classes together, do a refactoing of how PyRepl to use +// the new pyExec and in general clean up the unnecessary code. + import { ensureUniqueId, addClasses, removeClasses, getAttribute } from '../utils'; import type { Runtime } from '../runtime'; import { getLogger } from '../logger'; From 3eb07da9f1f946cb66d5fe6fab07040624573bdb Mon Sep 17 00:00:00 2001 From: Antonio Cuni Date: Sun, 23 Oct 2022 22:51:09 +0200 Subject: [PATCH 32/32] remove outdate comment --- pyscriptjs/tests/unit/pytitle.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/pyscriptjs/tests/unit/pytitle.test.ts b/pyscriptjs/tests/unit/pytitle.test.ts index 922da432ce7..a4b91e5f4d4 100644 --- a/pyscriptjs/tests/unit/pytitle.test.ts +++ b/pyscriptjs/tests/unit/pytitle.test.ts @@ -31,7 +31,6 @@ describe("PyTitle", () => { instance.connectedCallback(); expect(instance.label).toBe("Hello, world!") - // mount_name should be similar to: py_be025f4c_2150_7f2a_1a85_af92970c2a0e expect(instance.mount_name).toMatch("my_fancy_title"); expect(instance.innerHTML).toContain("

Hello, world!

") }) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy