mirror of https://github.com/pyodide/pyodide.git
Support top level await in console.html (#1459)
This commit is contained in:
parent
d27232fe5b
commit
b7b71837c6
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue