Support top level await in console.html (#1459)

This commit is contained in:
Hood Chatham 2021-04-14 16:09:50 -04:00 committed by GitHub
parent d27232fe5b
commit b7b71837c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 98 additions and 93 deletions

View File

@ -183,6 +183,8 @@ substitutions:
some completion issues (see
[#821](https://github.com/pyodide/pyodide/issues/821) and
[#1160](https://github.com/pyodide/pyodide/issues/1160)
- {{ Enhancement }} Support top-level await in the console
[#1459](https://github.com/pyodide/pyodide/issues/1459)
### Packages

View File

@ -4,42 +4,36 @@ import io
import sys
import platform
from contextlib import contextmanager
import builtins
import rlcompleter
import asyncio
from pyodide import eval_code_async
import ast
# this import can fail when we are outside a browser (e.g. for tests)
try:
import js
import pyodide_js
_dummy_promise = js.Promise.resolve()
_load_packages_from_imports = pyodide_js.loadPackagesFromImports
from pyodide_js import loadPackagesFromImports as _load_packages_from_imports
from asyncio import ensure_future
except ImportError:
from asyncio import Future
class _FakePromise:
"""A promise that mimic the JS promises.
def ensure_future(co): # type: ignore
fut = Future()
try:
co.send(None)
except StopIteration as v:
result = v.args[0] if v.args else None
fut.set_result(result)
except BaseException as e:
fut.set_exception(e)
else:
raise Exception("coroutine didn't finish in one pass")
return fut
Only `then is supported` and there is no asynchronicity.
execution occurs when then is call.
This is mainly to fake `load_packages_from_imports`
and `InteractiveConsole.run_complete` in contexts
where JS promises are not available (tests)."""
def __init__(self, args=None):
self.args = (args,) if args is not None else ()
def then(self, func, *args):
return _FakePromise(func(*self.args))
_dummy_promise = _FakePromise()
def _load_packages_from_imports(*args):
return _dummy_promise
async def _load_packages_from_imports(*args):
pass
__all__ = ["InteractiveConsole", "repr_shorten", "displayhook"]
__all__ = ["InteractiveConsole", "repr_shorten"]
class _StdStream(io.TextIOWrapper):
@ -97,8 +91,7 @@ class InteractiveConsole(code.InteractiveConsole):
Base implementation for an interactive console that manages
stdout/stderr redirection. Since packages are loaded before running
code, `runcode` returns a JS promise. Override `sys.displayhook` to
catch the result of an execution.
code, `runcode` returns a JS promise.
`self.stdout_callback` and `self.stderr_callback` can be overloaded.
@ -111,7 +104,7 @@ class InteractiveConsole(code.InteractiveConsole):
stderr_callback
Function to call at each `sys.stderr` flush.
persistent_stream_redirection
Wether or not the std redirection should be kept between calls to
Whether or not the std redirection should be kept between calls to
`runcode`.
"""
@ -130,13 +123,16 @@ class InteractiveConsole(code.InteractiveConsole):
self._streams_redirected = False
if persistent_stream_redirection:
self.redirect_stdstreams()
self.run_complete = _dummy_promise
self.run_complete: asyncio.Future = asyncio.Future()
self.run_complete.set_result(None)
self._completer = rlcompleter.Completer(self.locals) # type: ignore
# all nonalphanums except '.'
# see https://github.com/python/cpython/blob/a4258e8cd776ba655cc54ba54eaeffeddb0a267c/Modules/readline.c#L1211
self.completer_word_break_characters = (
""" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?"""
)
self.output_truncated_text = "\\n[[;orange;]<long output truncated>]\\n"
self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT # type: ignore
def redirect_stdstreams(self):
""" Toggle stdout/stderr redirections. """
@ -220,22 +216,34 @@ class InteractiveConsole(code.InteractiveConsole):
function sets the promise `self.run_complete`.
If you need to wait for the end of the computation,
you should await for it."""
parent_runcode = super().runcode
source = "\n".join(self.buffer)
self.run_complete = ensure_future(
self.load_packages_and_run(self.run_complete, source)
)
def load_packages_and_run(*args):
def run(*args):
with self.stdstreams_redirections():
parent_runcode(code)
# in CPython's REPL, flush is performed
# by input(prompt) at each new prompt ;
# since we are not using input, we force
# flushing here
self.flush_all()
async def load_packages_and_run(self, run_complete, source):
try:
await run_complete
except BaseException:
# Throw away old error
pass
with self.stdstreams_redirections():
await _load_packages_from_imports(source)
try:
result = await eval_code_async(source, self.locals)
except BaseException as e:
from traceback import print_exception
return _load_packages_from_imports(source).then(run)
self.run_complete = self.run_complete.then(load_packages_and_run)
print_exception(type(e), e, e.__traceback__)
raise e
else:
self.display(result)
# in CPython's REPL, flush is performed
# by input(prompt) at each new prompt ;
# since we are not using input, we force
# flushing here
self.flush_all()
return result
def __del__(self):
self.restore_stdstreams()
@ -282,6 +290,11 @@ class InteractiveConsole(code.InteractiveConsole):
completions = self._completer.global_matches(source) # type: ignore
return completions, start
def display(self, value):
if value is None:
return
print(repr_shorten(value, separator=self.output_truncated_text))
def repr_shorten(
value: Any, limit: int = 1000, split: Optional[int] = None, separator: str = "..."
@ -299,33 +312,3 @@ def repr_shorten(
if len(text) > limit:
text = f"{text[:split]}{separator}{text[-split:]}"
return text
def displayhook(value, repr: Callable[[Any], str]):
"""A displayhook with custom `repr` function.
It is intendend to overload `sys.displayhook`. Note that monkeypatch
`builtins.repr` does not work in `sys.displayhook`. The pointer to
`repr` seems hardcoded in default `sys.displayhook` version
(which is written in C)."""
# from https://docs.python.org/3/library/sys.html#sys.displayhook
# If value is not None, this function prints repr(value) to
# sys.stdout, and saves value in builtins._. If repr(value) is not
# encodable to sys.stdout.encoding with sys.stdout.errors error
# handler (which is probably 'strict'), encode it to
# sys.stdout.encoding with 'backslashreplace' error handler.
if value is None:
return
builtins._ = None # type: ignore
text = repr(value)
try:
sys.stdout.write(text)
except UnicodeEncodeError:
bytes = text.encode(sys.stdout.encoding, "backslashreplace")
if hasattr(sys.stdout, "buffer"):
sys.stdout.buffer.write(bytes)
else:
text = bytes.decode(sys.stdout.encoding, "strict")
sys.stdout.write(text)
sys.stdout.write("\n")
builtins._ = value # type: ignore

View File

@ -22,15 +22,6 @@
from pyodide import console
import __main__
def displayhook(value):
separator = "\\n[[;orange;]<long output truncated>]\\n"
_repr = lambda v: console.repr_shorten(v, separator=separator)
return console.displayhook(value, _repr)
sys.displayhook = displayhook
class PyConsole(console.InteractiveConsole):
def __init__(self):
super().__init__(
@ -54,7 +45,12 @@
for( const c of command.split('\n') ) {
const prompt = pyconsole.push(c) ? ps2 : ps1;
term.set_prompt(prompt);
await pyconsole.run_complete;
let run_complete = pyconsole.run_complete;
try {
let r = await run_complete;
r.destroy();
} catch(_){ }
run_complete.destroy();
}
term.resume();
}

View File

@ -1,7 +1,6 @@
import pytest
from pathlib import Path
import sys
import io
sys.path.append(str(Path(__file__).parents[2] / "src" / "pyodide-py"))
@ -72,6 +71,16 @@ def test_interactive_console_streams(safe_sys_redirections):
shell.push("1+1")
assert my_stdout == "foo\nfoobar\nfoobar\n2\n"
assert shell.run_complete.result() == 2
my_stderr = ""
shell.push("raise Exception('hi')")
assert my_stderr.endswith("Exception: hi\n")
assert shell.run_complete.exception() is not None
my_stderr = ""
shell.push("1+1")
assert my_stderr == ""
assert shell.run_complete.result() == 2
shell.restore_stdstreams()
@ -121,12 +130,6 @@ def test_repr(safe_sys_redirections):
console.repr_shorten(string, limit=limit, separator=sep)
) == 2 * (limit // 2) + len(sep)
sys.stdout = io.StringIO()
console.displayhook(
[0] * 100, lambda v: console.repr_shorten(v, 100, separator=sep)
)
assert len(sys.stdout.getvalue()) == 100 + len(sep) + 1 # for \n
@pytest.fixture
def safe_selenium_sys_redirections(selenium):
@ -153,12 +156,12 @@ def test_interactive_console(selenium, safe_selenium_sys_redirections):
result = None
def displayhook(value):
def display(value):
global result
result = value
shell = InteractiveConsole()
sys.displayhook = displayhook
shell.display = display
"""
)
@ -238,3 +241,24 @@ def test_completion(selenium, safe_selenium_sys_redirections):
],
8,
]
def test_interactive_console_top_level_await(selenium, safe_selenium_sys_redirections):
selenium.run(
"""
import sys
from pyodide.console import InteractiveConsole
result = None
def display(value):
global result
result = value
shell = InteractiveConsole()
shell.display = display
"""
)
selenium.run("shell.push('from js import fetch')")
selenium.run("shell.push('await (await fetch(`packages.json`)).json()')")
assert selenium.run("result") == None