Remove 'Implicit Async', Don't Await runtime.run() (#928)

* Revert to runPython instead of await runPythonAsync

* "Implicit Coroutines" are no longer permitted in py-script tags

* Tests added for the above

* xfail test_importmap (See #938)
This commit is contained in:
Jeff Glass
2022-11-16 13:11:40 -06:00
committed by GitHub
parent 41ebaaf366
commit 0b23310a06
17 changed files with 318 additions and 74 deletions

View File

@@ -150,7 +150,7 @@ export function make_PyRepl(runtime: Runtime) {
/** Execute the python code written in the editor, and automatically
* display() the last evaluated expression
*/
async execute(): Promise<void> {
execute(): void {
const pySrc = this.getPySrc();
// determine the output element
@@ -166,7 +166,7 @@ export function make_PyRepl(runtime: Runtime) {
outEl.innerHTML = '';
// execute the python code
const pyResult = await pyExec(runtime, pySrc, outEl);
const pyResult = pyExec(runtime, pySrc, outEl);
// display the value of the last evaluated expression (REPL-style)
if (pyResult !== undefined) {

View File

@@ -12,7 +12,7 @@ export function make_PyScript(runtime: Runtime) {
ensureUniqueId(this);
const pySrc = await this.getPySrc();
this.innerHTML = '';
await pyExec(runtime, pySrc, this);
pyExec(runtime, pySrc, this);
}
async getPySrc(): Promise<string> {

View File

@@ -1,18 +1,30 @@
import { getLogger } from './logger';
import { ensureUniqueId } from './utils';
import { ensureUniqueId, ltrim } from './utils';
import { UserError } from './exceptions';
import type { Runtime } from './runtime';
const logger = getLogger('pyexec');
export async function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
export 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);
//This is the python function defined in pyscript.py
const usesTopLevelAwait = runtime.globals.get('uses_top_level_await')
try {
try {
return await runtime.run(pysrc);
} catch (err) {
if (usesTopLevelAwait(pysrc)){
throw new UserError(
'The use of top-level "await", "async for", and ' +
'"async with" is deprecated.' +
'\nPlease write a coroutine containing ' +
'your code and schedule it using asyncio.ensure_future() or similar.' +
'\nSee https://docs.pyscript.net/latest/guides/asyncio.html for more information.'
)
}
return 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

View File

@@ -75,8 +75,8 @@ export class PyodideRuntime extends Runtime {
logger.info('pyodide loaded and initialized');
}
async run(code: string): Promise<any> {
return await this.interpreter.runPythonAsync(code);
run(code: string) {
return this.interpreter.runPython(code);
}
registerJsModule(name: string, module: object): void {

View File

@@ -1,3 +1,4 @@
import ast
import asyncio
import base64
import html
@@ -404,4 +405,28 @@ class PyListTemplate:
pass
class TopLevelAsyncFinder(ast.NodeVisitor):
def is_source_top_level_await(self, source):
self.async_found = False
node = ast.parse(source)
self.generic_visit(node)
return self.async_found
def visit_Await(self, node):
self.async_found = True
def visit_AsyncFor(self, node):
self.async_found = True
def visit_AsyncWith(self, node):
self.async_found = True
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
pass # Do not visit children of async function defs
def uses_top_level_await(source: str) -> bool:
return TopLevelAsyncFinder().is_source_top_level_await(source)
pyscript = PyScript()

View File

@@ -55,7 +55,7 @@ export abstract class Runtime extends Object {
* (asynchronously) which can call its own API behind the scenes.
* Python exceptions are turned into JS exceptions.
* */
abstract run(code: string): Promise<unknown>;
abstract run(code: string);
/**
* Same as run, but Python exceptions are not propagated: instead, they
@@ -64,11 +64,16 @@ export abstract class Runtime extends Object {
* This is a bad API and should be killed/refactored/changed eventually,
* but for now we have code which relies on it.
* */
async runButDontRaise(code: string): Promise<unknown> {
return this.run(code).catch(err => {
const error = err as Error;
logger.error('Error:', error);
});
runButDontRaise(code: string) {
let result
try{
result = this.run(code)
}
catch (err){
const error = err as Error
logger.error('Error:', error)
}
return result
}
/**

View File

@@ -2,56 +2,146 @@ from .support import PyScriptTest
class TestAsync(PyScriptTest):
# ensure_future() and create_task() should behave similarly;
# we'll use the same source code to test both
coroutine_script = """
<py-script>
import js
import asyncio
js.console.log("first")
async def main():
await asyncio.sleep(1)
js.console.log("third")
asyncio.{func}(main())
js.console.log("second")
</py-script>
"""
def test_asyncio_ensure_future(self):
self.pyscript_run(self.coroutine_script.format(func="ensure_future"))
self.wait_for_console("third")
assert self.console.log.lines == [self.PY_COMPLETE, "first", "second", "third"]
def test_asyncio_create_task(self):
self.pyscript_run(self.coroutine_script.format(func="create_task"))
self.wait_for_console("third")
assert self.console.log.lines == [self.PY_COMPLETE, "first", "second", "third"]
def test_asyncio_gather(self):
self.pyscript_run(
"""
<py-script id="pys">
import asyncio
import js
from pyodide.ffi import to_js
async def coro(delay):
await asyncio.sleep(delay)
return(delay)
async def get_results():
results = await asyncio.gather(*[coro(d) for d in range(3,0,-1)])
js.console.log(to_js(results))
js.console.log("DONE")
asyncio.ensure_future(get_results())
</py-script>
"""
)
self.wait_for_console("DONE")
assert self.console.log.lines[-2:] == ["[3,2,1]", "DONE"]
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)
async def a_func():
for i in range(3):
js.console.log('A', i)
await asyncio.sleep(0.1)
asyncio.ensure_future(a_func())
</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")
async def b_func():
for i in range(3):
js.console.log('B', i)
await asyncio.sleep(0.1)
js.console.log('b func done')
asyncio.ensure_future(b_func())
</py-script>
"""
)
self.wait_for_console("async tadone")
self.wait_for_console("b func done")
assert self.console.log.lines == [
"Python initialization complete",
self.PY_COMPLETE,
"A 0",
"B 0",
"A 1",
"B 1",
"A 2",
"B 2",
"async tadone",
"b func done",
]
def test_multiple_async_multiple_display(self):
def test_multiple_async_multiple_display_targetted(self):
self.pyscript_run(
"""
<py-script id='pyA'>
import js
import asyncio
async def a_func():
for i in range(2):
display(f'A{i}', target='pyA')
await asyncio.sleep(0.1)
asyncio.ensure_future(a_func())
</py-script>
<py-script id='pyB'>
import js
import asyncio
async def a_func():
for i in range(2):
display(f'B{i}', target='pyB')
await asyncio.sleep(0.1)
js.console.log("B DONE")
asyncio.ensure_future(a_func())
</py-script>
"""
)
self.wait_for_console("B DONE")
inner_text = self.page.inner_text("html")
assert "A0\nA1\nB0\nB1" in inner_text
def test_async_display_untargetted(self):
self.pyscript_run(
"""
<py-script id='pyA'>
import asyncio
for i in range(2):
display('A')
await asyncio.sleep(0)
</py-script>
import js
<py-script id='pyB'>
import asyncio
for i in range(2):
display('B')
await asyncio.sleep(0)
async def a_func():
try:
display('A')
await asyncio.sleep(0.1)
except Exception as err:
js.console.error(str(err))
await asyncio.sleep(1)
js.console.log("DONE")
asyncio.ensure_future(a_func())
</py-script>
"""
)
inner_text = self.page.inner_text("html")
assert "A\nB\nA\nB" in inner_text
self.wait_for_console("DONE")
assert (
self.console.error.lines[-1]
== "Implicit target not allowed here. Please use display(..., target=...)"
)

View File

@@ -1,6 +1,9 @@
import pytest
from .support import PyScriptTest
@pytest.mark.xfail(reason="See PR #938")
class TestImportmap(PyScriptTest):
def test_importmap(self):
src = """

View File

@@ -100,7 +100,7 @@ class TestConfig(PyScriptTest):
""",
)
assert self.console.log.lines == [self.PY_COMPLETE, "version 0.20.0"]
assert self.console.log.lines[-1] == "version 0.20.0"
version = self.page.locator("py-script").inner_text()
assert version == "0.20.0"

View File

@@ -63,13 +63,9 @@ class TestPyRepl(PyScriptTest):
</py-repl>
"""
)
self.page.wait_for_selector("#runButton")
self.page.keyboard.press("Shift+Enter")
# when we use locator('button').click() the message appears
# immediately, with keyboard.press we need to wait for it. I don't
# really know why it has a different behavior, I didn't investigate
# further.
self.wait_for_console("hello world")
assert self.console.log.lines == [self.PY_COMPLETE, "hello world"]
def test_display(self):
self.pyscript_run(

View File

@@ -1,4 +1,5 @@
import sys
import textwrap
from unittest.mock import Mock
import pyscript
@@ -48,3 +49,71 @@ def test_format_mime_HTML():
out, mime = pyscript.format_mime(obj)
assert out == "<p>hello</p>"
assert mime == "text/html"
def test_uses_top_level_await():
# Basic Case
src = "x = 1"
assert pyscript.uses_top_level_await(src) is False
# Comments are not top-level await
src = textwrap.dedent(
"""
#await async for async with asyncio
"""
)
assert pyscript.uses_top_level_await(src) is False
# Top-level-await cases
src = textwrap.dedent(
"""
async def foo():
pass
await foo
"""
)
assert pyscript.uses_top_level_await(src) is True
src = textwrap.dedent(
"""
async with object():
pass
"""
)
assert pyscript.uses_top_level_await(src) is True
src = textwrap.dedent(
"""
async for _ in range(10):
pass
"""
)
assert pyscript.uses_top_level_await(src) is True
# Acceptable await/async for/async with cases
src = textwrap.dedent(
"""
async def foo():
await foo()
"""
)
assert pyscript.uses_top_level_await(src) is False
src = textwrap.dedent(
"""
async def foo():
async with object():
pass
"""
)
assert pyscript.uses_top_level_await(src) is False
src = textwrap.dedent(
"""
async def foo():
async for _ in range(10):
pass
"""
)
assert pyscript.uses_top_level_await(src) is False