mirror of
https://github.com/pyscript/pyscript.git
synced 2026-03-18 07:00:35 -04:00
wrap runPython in async (#1212)
This commit is contained in:
@@ -150,7 +150,7 @@ export function make_PyRepl(interpreter: Interpreter) {
|
||||
/** Execute the python code written in the editor, and automatically
|
||||
* display() the last evaluated expression
|
||||
*/
|
||||
execute(): void {
|
||||
async execute(): Promise<void> {
|
||||
const pySrc = this.getPySrc();
|
||||
|
||||
// determine the output element
|
||||
@@ -166,7 +166,7 @@ export function make_PyRepl(interpreter: Interpreter) {
|
||||
outEl.innerHTML = '';
|
||||
|
||||
// execute the python code
|
||||
const pyResult = pyExec(interpreter, pySrc, outEl);
|
||||
const pyResult = (await pyExec(interpreter, pySrc, outEl)).result;
|
||||
|
||||
// display the value of the last evaluated expression (REPL-style)
|
||||
if (pyResult !== undefined) {
|
||||
|
||||
@@ -16,17 +16,30 @@ export function make_PyScript(interpreter: Interpreter, app: PyScriptApp) {
|
||||
stderr_manager: Stdio | null;
|
||||
|
||||
async connectedCallback() {
|
||||
ensureUniqueId(this);
|
||||
// Save innerHTML information in srcCode so we can access it later
|
||||
// once we clean innerHTML (which is required since we don't want
|
||||
// source code to be rendered on the screen)
|
||||
this.srcCode = this.innerHTML;
|
||||
const pySrc = await this.getPySrc();
|
||||
this.innerHTML = '';
|
||||
/**
|
||||
* Since connectedCallback is async, multiple py-script tags can be executed in
|
||||
* an order which is not particularly sequential. The locking mechanism here ensures
|
||||
* a sequential execution of multiple py-script tags present in one page.
|
||||
*
|
||||
* Concurrent access to the multiple py-script tags is thus avoided.
|
||||
*/
|
||||
let releaseLock: any;
|
||||
try {
|
||||
releaseLock = await app.tagExecutionLock();
|
||||
ensureUniqueId(this);
|
||||
// Save innerHTML information in srcCode so we can access it later
|
||||
// once we clean innerHTML (which is required since we don't want
|
||||
// source code to be rendered on the screen)
|
||||
this.srcCode = this.innerHTML;
|
||||
const pySrc = await this.getPySrc();
|
||||
this.innerHTML = '';
|
||||
|
||||
app.plugins.beforePyScriptExec({interpreter: interpreter, src: pySrc, pyScriptTag: this});
|
||||
const result = pyExec(interpreter, pySrc, this);
|
||||
app.plugins.afterPyScriptExec({interpreter: interpreter, src: pySrc, pyScriptTag: this, result: result});
|
||||
app.plugins.beforePyScriptExec({interpreter: interpreter, src: pySrc, pyScriptTag: this});
|
||||
const result = (await pyExec(interpreter, pySrc, this)).result;
|
||||
app.plugins.afterPyScriptExec({interpreter: interpreter, src: pySrc, pyScriptTag: this, result: result});
|
||||
} finally {
|
||||
releaseLock()
|
||||
}
|
||||
}
|
||||
|
||||
async getPySrc(): Promise<string> {
|
||||
@@ -183,12 +196,14 @@ function createElementsWithEventListeners(interpreter: Interpreter, pyAttribute:
|
||||
}
|
||||
} else {
|
||||
el.addEventListener(event, () => {
|
||||
try {
|
||||
interpreter.run(handlerCode)
|
||||
}
|
||||
catch (err) {
|
||||
displayPyException(err, el.parentElement);
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
await interpreter.run(handlerCode);
|
||||
}
|
||||
catch (err) {
|
||||
displayPyException(err, el.parentElement);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
// TODO: Should we actually map handlers in JS instead of Python?
|
||||
@@ -208,7 +223,7 @@ function createElementsWithEventListeners(interpreter: Interpreter, pyAttribute:
|
||||
}
|
||||
|
||||
/** Mount all elements with attribute py-mount into the Python namespace */
|
||||
export function mountElements(interpreter: Interpreter) {
|
||||
export async function mountElements(interpreter: Interpreter) {
|
||||
const matches: NodeListOf<HTMLElement> = document.querySelectorAll('[py-mount]');
|
||||
logger.info(`py-mount: found ${matches.length} elements`);
|
||||
|
||||
@@ -217,5 +232,5 @@ export function mountElements(interpreter: Interpreter) {
|
||||
const mountName = el.getAttribute('py-mount') || el.id.split('-').join('_');
|
||||
source += `\n${mountName} = Element("${el.id}")`;
|
||||
}
|
||||
interpreter.run(source);
|
||||
await interpreter.run(source);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ function createWidget(interpreter: Interpreter, name: string, code: string, klas
|
||||
this.shadow.appendChild(this.wrapper);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
interpreter.runButDontRaise(this.code);
|
||||
async connectedCallback() {
|
||||
await interpreter.runButDontRaise(this.code);
|
||||
this.proxyClass = interpreter.globals.get(this.klass);
|
||||
this.proxy = this.proxyClass(this);
|
||||
this.proxy.connect();
|
||||
|
||||
@@ -51,7 +51,7 @@ export abstract class Interpreter extends Object {
|
||||
* (asynchronously) which can call its own API behind the scenes.
|
||||
* Python exceptions are turned into JS exceptions.
|
||||
* */
|
||||
abstract run(code: string): unknown;
|
||||
abstract run(code: string): Promise<{result: any}>;
|
||||
|
||||
/**
|
||||
* Same as run, but Python exceptions are not propagated: instead, they
|
||||
@@ -60,10 +60,10 @@ export abstract class Interpreter extends Object {
|
||||
* This is a bad API and should be killed/refactored/changed eventually,
|
||||
* but for now we have code which relies on it.
|
||||
* */
|
||||
runButDontRaise(code: string): unknown {
|
||||
async runButDontRaise(code: string): Promise<unknown> {
|
||||
let result: unknown;
|
||||
try {
|
||||
result = this.run(code);
|
||||
result = (await this.run(code)).result;
|
||||
} catch (error: unknown) {
|
||||
logger.error('Error:', error);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PluginManager, define_custom_element } from './plugin';
|
||||
import { make_PyScript, initHandlers, mountElements } from './components/pyscript';
|
||||
import { PyodideInterpreter } from './pyodide';
|
||||
import { getLogger } from './logger';
|
||||
import { showWarning, globalExport } from './utils';
|
||||
import { showWarning, globalExport, createLock } from './utils';
|
||||
import { calculatePaths } from './plugins/fetch';
|
||||
import { createCustomElements } from './components/elements';
|
||||
import { UserError, ErrorCode, _createAlertBanner } from './exceptions';
|
||||
@@ -21,7 +21,6 @@ import { StdioDirector as StdioDirector } from './plugins/stdiodirector';
|
||||
// @ts-ignore
|
||||
import pyscript from './python/pyscript.py';
|
||||
import { robustFetch } from './fetch';
|
||||
import type { Plugin } from './plugin';
|
||||
|
||||
const logger = getLogger('pyscript/main');
|
||||
|
||||
@@ -65,6 +64,7 @@ export class PyScriptApp {
|
||||
PyScript: ReturnType<typeof make_PyScript>;
|
||||
plugins: PluginManager;
|
||||
_stdioMultiplexer: StdioMultiplexer;
|
||||
tagExecutionLock: any; // this is used to ensure that py-script tags are executed sequentially
|
||||
|
||||
constructor() {
|
||||
// initialize the builtin plugins
|
||||
@@ -75,6 +75,7 @@ export class PyScriptApp {
|
||||
this._stdioMultiplexer.addListener(DEFAULT_STDIO);
|
||||
|
||||
this.plugins.add(new StdioDirector(this._stdioMultiplexer));
|
||||
this.tagExecutionLock = createLock();
|
||||
}
|
||||
|
||||
// Error handling logic: if during the execution we encounter an error
|
||||
@@ -180,7 +181,7 @@ export class PyScriptApp {
|
||||
|
||||
this.logStatus('Setting up virtual environment...');
|
||||
await this.setupVirtualEnv(interpreter);
|
||||
mountElements(interpreter);
|
||||
await mountElements(interpreter);
|
||||
|
||||
// lifecycle (6.5)
|
||||
this.plugins.afterSetup(interpreter);
|
||||
|
||||
@@ -5,10 +5,9 @@ import type { Interpreter } from './interpreter';
|
||||
|
||||
const logger = getLogger('pyexec');
|
||||
|
||||
export function pyExec(interpreter: Interpreter, pysrc: string, outElem: HTMLElement) {
|
||||
export async function pyExec(interpreter: Interpreter, pysrc: string, outElem: HTMLElement) {
|
||||
//This is pyscript.py
|
||||
const pyscript_py = interpreter.interface.pyimport('pyscript');
|
||||
|
||||
ensureUniqueId(outElem);
|
||||
pyscript_py.set_current_display_target(outElem.id);
|
||||
try {
|
||||
@@ -23,13 +22,14 @@ export function pyExec(interpreter: Interpreter, pysrc: string, outElem: HTMLEle
|
||||
'\nSee https://docs.pyscript.net/latest/guides/asyncio.html for more information.',
|
||||
);
|
||||
}
|
||||
return interpreter.run(pysrc);
|
||||
return (await interpreter.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);
|
||||
return {result: undefined};
|
||||
}
|
||||
} finally {
|
||||
pyscript_py.set_current_display_target(undefined);
|
||||
|
||||
@@ -78,12 +78,32 @@ export class PyodideInterpreter extends Interpreter {
|
||||
await this.loadPackage('micropip');
|
||||
}
|
||||
logger.info('pyodide loaded and initialized');
|
||||
this.run('print("Python initialization complete")')
|
||||
await this.run('print("Python initialization complete")')
|
||||
}
|
||||
|
||||
run(code: string): unknown {
|
||||
return this.interface.runPython(code);
|
||||
/* eslint-disable */
|
||||
async run(code: string): Promise<{result: any}> {
|
||||
/**
|
||||
* eslint wants `await` keyword to be used i.e.
|
||||
* { result: await this.interface.runPython(code) }
|
||||
* However, `await` is not a no-op (no-operation) i.e.
|
||||
* `await 42` is NOT the same as `42` i.e. if the awaited
|
||||
* thing is not a promise, it is wrapped inside a promise and
|
||||
* that promise is awaited. Thus, it changes the execution order.
|
||||
* See https://stackoverflow.com/questions/55262996/does-awaiting-a-non-promise-have-any-detectable-effect
|
||||
* Thus, `eslint` is disabled for this block / snippet.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The output of `runPython` is wrapped inside an object
|
||||
* since an object is not thennable and avoids return of
|
||||
* a coroutine directly. This is so we do not `await` the results
|
||||
* of the underlying python execution, even if it's an
|
||||
* awaitable object (Future, Task, etc.)
|
||||
*/
|
||||
return { result: this.interface.runPython(code) };
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
registerJsModule(name: string, module: object): void {
|
||||
this.interface.registerJsModule(name, module);
|
||||
@@ -96,7 +116,8 @@ export class PyodideInterpreter extends Interpreter {
|
||||
// but one of our tests tries to use a locally downloaded older version of pyodide
|
||||
// for which the signature of `loadPackage` accepts the above params as args i.e.
|
||||
// the call uses `logger.info.bind(logger), logger.info.bind(logger)`.
|
||||
if (this.run("import sys; sys.modules['pyodide'].__version__").toString().startsWith("0.22")) {
|
||||
const pyodide_version = (await this.run("import sys; sys.modules['pyodide'].__version__")).result.toString();
|
||||
if (pyodide_version.startsWith("0.22")) {
|
||||
await this.interface.loadPackage(names, { messageCallback: logger.info.bind(logger), errorCallback: logger.info.bind(logger) });
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -203,12 +203,10 @@ 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)
|
||||
|
||||
@@ -112,3 +112,26 @@ export function createSingularWarning(msg: string, sentinelText: string | null =
|
||||
_createAlertBanner(msg, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A new asynchronous lock
|
||||
* @private
|
||||
*/
|
||||
export function createLock() {
|
||||
// This is a promise that is resolved when the lock is open, not resolved when lock is held.
|
||||
let _lock = Promise.resolve();
|
||||
|
||||
/**
|
||||
* Acquire the async lock
|
||||
* @returns A zero argument function that releases the lock.
|
||||
* @private
|
||||
*/
|
||||
async function acquireLock() {
|
||||
const old_lock = _lock;
|
||||
let releaseLock: () => void;
|
||||
_lock = new Promise((resolve) => (releaseLock = resolve));
|
||||
await old_lock;
|
||||
return releaseLock;
|
||||
}
|
||||
return acquireLock;
|
||||
}
|
||||
|
||||
@@ -311,6 +311,7 @@ class TestBasic(PyScriptTest):
|
||||
== 'print("hello world!")\n'
|
||||
)
|
||||
|
||||
@pytest.mark.skip(reason="pys-onClick is broken, we should kill it, see #1213")
|
||||
def test_pys_onClick_shows_deprecation_warning(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
|
||||
@@ -149,3 +149,58 @@ class TestAsync(PyScriptTest):
|
||||
self.console.error.lines[-1]
|
||||
== "Implicit target not allowed here. Please use display(..., target=...)"
|
||||
)
|
||||
|
||||
def test_sync_and_async_order(self):
|
||||
"""
|
||||
The order of execution is defined as follows:
|
||||
1. first, we execute all the py-script tag in order
|
||||
2. then, we start all the tasks which were scheduled with create_task
|
||||
|
||||
Note that tasks are started *AFTER* all py-script tags have been
|
||||
executed. That's why the console.log() inside mytask1 and mytask2 are
|
||||
executed after e.g. js.console.log("6").
|
||||
"""
|
||||
src = """
|
||||
<py-script>
|
||||
import js
|
||||
js.console.log("1")
|
||||
</py-script>
|
||||
|
||||
<py-script>
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def mytask1():
|
||||
js.console.log("7")
|
||||
await asyncio.sleep(0)
|
||||
js.console.log("9")
|
||||
|
||||
js.console.log("2")
|
||||
asyncio.create_task(mytask1())
|
||||
js.console.log("3")
|
||||
</py-script>
|
||||
|
||||
<py-script>
|
||||
import js
|
||||
js.console.log("4")
|
||||
</py-script>
|
||||
|
||||
<py-script>
|
||||
import asyncio
|
||||
import js
|
||||
|
||||
async def mytask2():
|
||||
js.console.log("8")
|
||||
await asyncio.sleep(0)
|
||||
js.console.log("10")
|
||||
js.console.log("DONE")
|
||||
|
||||
js.console.log("5")
|
||||
asyncio.create_task(mytask2())
|
||||
js.console.log("6")
|
||||
</py-script>
|
||||
"""
|
||||
self.pyscript_run(src, wait_for_pyscript=False)
|
||||
self.wait_for_console("DONE")
|
||||
lines = self.console.log.lines[-11:]
|
||||
assert lines == ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "DONE"]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from .support import PyScriptTest
|
||||
@@ -79,6 +80,7 @@ class TestSplashscreen(PyScriptTest):
|
||||
assert self.console.log.lines[0] == self.PY_COMPLETE
|
||||
assert "hello pyscript" in self.console.log.lines
|
||||
|
||||
@pytest.mark.skip(reason="pys-onClick is broken, we should kill it, see #1213")
|
||||
def test_splashscreen_closes_on_error_with_pys_onClick(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,7 @@ from .support import PyScriptTest
|
||||
class TestOutputHandling(PyScriptTest):
|
||||
# Source of a script to test the TargetedStdio functionality
|
||||
|
||||
def test_targeted_stdio(self):
|
||||
def test_targeted_stdio_solo(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
|
||||
@@ -51,19 +51,19 @@ describe('PyodideInterpreter', () => {
|
||||
});
|
||||
|
||||
it('should check if interpreter can run python code asynchronously', async () => {
|
||||
expect(interpreter.run('2+3')).toBe(5);
|
||||
expect((await interpreter.run('2+3')).result).toBe(5);
|
||||
});
|
||||
|
||||
it('should capture stdout', async () => {
|
||||
stdio.reset();
|
||||
interpreter.run("print('hello')");
|
||||
await interpreter.run("print('hello')");
|
||||
expect(stdio.captured_stdout).toBe('hello\n');
|
||||
});
|
||||
|
||||
it('should check if interpreter is able to load a package', async () => {
|
||||
await interpreter.loadPackage('numpy');
|
||||
interpreter.run('import numpy as np');
|
||||
interpreter.run('x = np.ones((10,))');
|
||||
await interpreter.run('import numpy as np');
|
||||
await interpreter.run('x = np.ones((10,))');
|
||||
expect(interpreter.globals.get('x').toJs()).toBeInstanceOf(Float64Array);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user