diff --git a/docs/conf.py b/docs/conf.py index 34f5243dc..bbb5420c7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -151,3 +151,4 @@ def delete_attrs(cls): delete_attrs(pyodide.webloop.WebLoop) delete_attrs(pyodide.webloop.WebLoopPolicy) +delete_attrs(pyodide.console.PyodideConsole) diff --git a/docs/project/changelog.md b/docs/project/changelog.md index e761c3f85..a50e313ef 100644 --- a/docs/project/changelog.md +++ b/docs/project/changelog.md @@ -13,7 +13,7 @@ substitutions: ## [Unreleased] -- {{ API }} {any}`loadPyodide` no longer automatically stores the API into a +- {{ API }} {any}`loadPyodide ` no longer automatically stores the API into a global variable called `pyodide`. To get old behavior, say `globalThis.pyodide = await loadPyodide({...})`. {pr}`1597` @@ -38,6 +38,13 @@ substitutions: type. This is particularly important if the error is a `KeyboardInterrupt`. {pr}`1447` +- {{ Enhancement }} Added {any}`Console` class closely based on the Python standard + library `code.InteractiveConsole` but with support for top level await and + stream redirection. Also added the subclass {any}`PyodideConsole` which + automatically uses {any}`pyodide.loadPackagesFromImports` on the code before running + it. + {pr}`1125`, {pr}`1155`, {pr}`1635` + - {{ Update }} Pyodide now runs Python 3.9.5. {pr}`1637` @@ -59,7 +66,7 @@ substitutions: - The following standard library modules are now available as standalone packages - distlib - They are loaded by default in {any}`globalThis.loadPyodide`, however this behavior + They are loaded by default in {any}`loadPyodide `, however this behavior can be disabled with the `fullStdLib` parameter set to `false`. All optional stdlib modules can then be loaded as needed with {any}`pyodide.loadPackage`. {pr}`1543` diff --git a/src/py/_pyodide/console.py b/src/py/_pyodide/console.py new file mode 100644 index 000000000..f8ff55fc9 --- /dev/null +++ b/src/py/_pyodide/console.py @@ -0,0 +1,479 @@ +import ast +import asyncio +from asyncio import ensure_future, Future +from codeop import Compile, CommandCompiler, _features # type: ignore +from contextlib import ( + contextmanager, + redirect_stdout, + redirect_stderr, + ExitStack, +) +from contextlib import _RedirectStream # type: ignore +import rlcompleter +import platform +import sys +import traceback +from typing import Literal +from typing import ( + Optional, + Callable, + Any, + List, + Tuple, + Union, + Tuple, +) + +from _pyodide._base import should_quiet, CodeRunner + +__all__ = ["repr_shorten", "BANNER", "Console", "PyodideConsole"] + + +def _banner(): + """A banner similar to the one printed by the real Python interpreter.""" + # copied from https://github.com/python/cpython/blob/799f8489d418b7f9207d333eac38214931bd7dcc/Lib/code.py#L214 + cprt = 'Type "help", "copyright", "credits" or "license" for more information.' + version = platform.python_version() + build = f"({', '.join(platform.python_build())})" + return f"Python {version} {build} on WebAssembly VM\n{cprt}" + + +BANNER = _banner() +del _banner + + +class redirect_stdin(_RedirectStream): + _stream = "stdin" + + +class _WriteStream: + """A utility class so we can specify our own handlers for writes to sdout, stderr""" + + def __init__(self, write_handler, name=None): + self.write_handler = write_handler + self.name = name + + def write(self, text): + self.write_handler(text) + + def flush(self): + pass + + +class _ReadStream: + """A utility class so we can specify our own handler for reading from stdin""" + + def __init__(self, read_handler, name=None): + self.read_handler = read_handler + self.name = name + + def readline(self, n=-1): + return self.read_handler(n) + + def flush(self): + pass + + +class _Compile(Compile): + """Compile code with CodeRunner, and remember future imports + + Instances of this class behave much like the built-in compile function, + but if one is used to compile text containing a future statement, it + "remembers" and compiles all subsequent program texts with the statement in + force. It uses CodeRunner instead of the built in compile. + """ + + def __init__( + self, + *, + return_mode="last_expr", + quiet_trailing_semicolon=True, + flags=0x0, + ): + super().__init__() + self.flags |= flags + self.return_mode = return_mode + self.quiet_trailing_semicolon = quiet_trailing_semicolon + + def __call__(self, source, filename, symbol) -> CodeRunner: # type: ignore + return_mode = self.return_mode + if self.quiet_trailing_semicolon and should_quiet(source): + return_mode = None + code_runner = CodeRunner( + source, + mode=symbol, + filename=filename, + return_mode=return_mode, + flags=self.flags, + ).compile() + for feature in _features: + if code_runner.code.co_flags & feature.compiler_flag: + self.flags |= feature.compiler_flag + return code_runner + + +class _CommandCompiler(CommandCompiler): + """Compile code with CodeRunner, and remember future imports, return None if + code is incomplete. + + Instances of this class have __call__ methods identical in signature to + compile; the difference is that if the instance compiles program text + containing a __future__ statement, the instance 'remembers' and compiles all + subsequent program texts with the statement in force. + + If the source is determined to be incomplete, will suppress the SyntaxError + and return ``None``. + """ + + def __init__( + self, + *, + return_mode="last_expr", + quiet_trailing_semicolon=True, + flags=0x0, + ): + self.compiler = _Compile( + return_mode=return_mode, + quiet_trailing_semicolon=quiet_trailing_semicolon, + flags=flags, + ) + + def __call__(self, source, filename="", symbol="single") -> Optional[CodeRunner]: # type: ignore + return super().__call__(source, filename, symbol) # type: ignore + + +INCOMPLETE: Literal["incomplete"] = "incomplete" +SYNTAX_ERROR: Literal["syntax-error"] = "syntax-error" +COMPLETE: Literal["complete"] = "complete" + + +class ConsoleFuture(Future): + """A future with extra fields used as the return value for :any:`Console` apis. + + Attributes + ---------- + syntax_check : str + One of ``"incomplete"``, ``"syntax-error"``, or ``"complete"`. If the value is + ``"incomplete"`` then the future has already been resolved with result equal to + ``None``. If the value is ``"syntax-error"``, the ``Future`` has already been + rejected with a ``SyntaxError``. If the value is ``"complete"``, then the input + complete and syntactically correct. + + formatted_error : str + If the ``Future`` is rejected, this will be filled with a formatted version of + the code. This is a convenience that simplifies code and helps to avoid large + memory leaks when using from Javascript. + + """ + + def __init__( + self, + syntax: Union[ + Literal["incomplete"], Literal["syntax-error"], Literal["complete"] + ], + ): + super().__init__() + self.syntax_check: Union[ + Literal["incomplete"], Literal["syntax-error"], Literal["complete"] + ] = syntax + self.formatted_error: Optional[str] = None + + +class Console: + """Interactive Pyodide console + + An interactive console based on the Python standard library + `code.InteractiveConsole` that manages stream redirections and asynchronous + execution of the code. + + The stream callbacks can be modified directly as long as + `persistent_stream_redirection` isn't in effect. + + Parameters + ---------- + globals : ``dict`` + The global namespace in which to evaluate the code. Defaults to a new empty dictionary. + + stdout_callback : ``Callable[[str], None]`` + Function to call at each write to ``sys.stdout``. Defaults to ``None``. + + stderr_callback : ``Callable[[str], None]`` + Function to call at each write to ``sys.stderr``. Defaults to ``None``. + + stdin_callback : ``Callable[[str], None]`` + Function to call at each read from ``sys.stdin``. Defaults to ``None``. + + persistent_stream_redirection : ``bool`` + Should redirection of standard streams be kept between calls to :any:`runcode `? + Defaults to ``False``. + + filename : ``str`` + The file name to report in error messages. Defaults to ````. + + Attributes + ---------- + globals : ``Dict[str, Any]`` + The namespace used as the global + + stdout_callback : ``Callback[[str], None]`` + Function to call at each write to ``sys.stdout``. + + stderr_callback : ``Callback[[str], None]`` + Function to call at each write to ``sys.stderr``. + + stdin_callback : ``Callback[[str], None]`` + Function to call at each read from ``sys.stdin``. + + buffer : ``List[str]`` + The list of strings that have been :any:`pushed ` to the console. + + completer_word_break_characters : ``str`` + The set of characters considered by :any:`complete ` to be word breaks. + """ + + def __init__( + self, + globals: Optional[dict] = None, + *, + stdout_callback: Optional[Callable[[str], None]] = None, + stderr_callback: Optional[Callable[[str], None]] = None, + stdin_callback: Optional[Callable[[str], None]] = None, + persistent_stream_redirection: bool = False, + filename: str = "", + ): + if globals is None: + globals = {"__name__": "__console__", "__doc__": None} + self.globals = globals + self._stdout = None + self._stderr = None + self.stdout_callback = stdout_callback + self.stderr_callback = stderr_callback + self.stdin_callback = stdin_callback + self.filename = filename + self.buffer: List[str] = [] + self._lock = asyncio.Lock() + self._streams_redirected = False + self._stream_generator = None # track persistent stream redirection + if persistent_stream_redirection: + self.persistent_redirect_streams() + self._completer = rlcompleter.Completer(self.globals) # 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._compile = _CommandCompiler(flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT) # type: ignore + + def persistent_redirect_streams(self): + """Redirect stdin/stdout/stderr persistently""" + if self._stream_generator: + return + self._stream_generator = self._stdstreams_redirections_inner() + next(self._stream_generator) # trigger stream redirection + # streams will be reverted to normal when self._stream_generator is destroyed. + + def persistent_restore_streams(self): + """Restore stdin/stdout/stderr if they have been persistently redirected""" + # allowing _stream_generator to be garbage collected restores the streams + self._stream_generator = None + + @contextmanager + def redirect_streams(self): + """A context manager to redirect standard streams. + + This supports nesting.""" + yield from self._stdstreams_redirections_inner() + + def _stdstreams_redirections_inner(self): + """This is the generator which implements redirect_streams and the stdstreams_redirections""" + # already redirected? + if self._streams_redirected: + yield + return + redirects = [] + if self.stdout_callback: + redirects.append( + redirect_stdout( + _WriteStream(self.stdout_callback, name=sys.stdout.name) + ) + ) + if self.stderr_callback: + redirects.append( + redirect_stderr( + _WriteStream(self.stderr_callback, name=sys.stderr.name) + ) + ) + if self.stdin_callback: + redirects.append( + redirect_stdin(_ReadStream(self.stdin_callback, name=sys.stdin.name)) + ) + try: + self._streams_redirected = True + with ExitStack() as stack: + for redirect in redirects: + stack.enter_context(redirect) + yield + finally: + self._streams_redirected = False + + def runsource(self, source: str, filename: str = "") -> ConsoleFuture: + """Compile and run source code in the interpreter. + + Returns + ------- + :any:`ConsoleFuture` + + """ + try: + code = self._compile(source, filename, "single") + except (OverflowError, SyntaxError, ValueError) as e: + # Case 1 + res = ConsoleFuture(SYNTAX_ERROR) + res.set_exception(e) + res.formatted_error = self.formatsyntaxerror(e) + return res + + if code is None: + res = ConsoleFuture(INCOMPLETE) + res.set_result(None) + return res + + res = ConsoleFuture(COMPLETE) + + def done_cb(fut): + exc = fut.exception() + if exc: + res.formatted_error = self.formattraceback(exc) + res.set_exception(exc) + exc = None + else: + res.set_result(fut.result()) + + ensure_future(self.runcode(source, code)).add_done_callback(done_cb) + return res + + async def runcode(self, source: str, code: CodeRunner) -> Any: + """Execute a code object and return the result.""" + async with self._lock: + with self.redirect_streams(): + try: + return await code.run_async(self.globals) + finally: + sys.stdout.flush() + sys.stderr.flush() + + def formatsyntaxerror(self, e: Exception) -> str: + """Format the syntax error that just occurred. + + This doesn't include a stack trace because there isn't one. The actual + error object is stored into `sys.last_value`. + """ + sys.last_type = type(e) + sys.last_value = e + sys.last_traceback = None + try: + return "".join(traceback.format_exception_only(type(e), e)) + finally: + e = None # type: ignore + + def num_frames_to_keep(self, tb): + keep_frames = False + kept_frames = 0 + # Try to trim out stack frames inside our code + for (frame, _) in traceback.walk_tb(tb): + keep_frames = keep_frames or frame.f_code.co_filename == "" + keep_frames = keep_frames or frame.f_code.co_filename == "" + if keep_frames: + kept_frames += 1 + return kept_frames + + def formattraceback(self, e: Exception) -> str: + """Format the exception that just occurred. + + The actual error object is stored into `sys.last_value`. + """ + print("!!!!!!!!!!!") + print("formattraceback!") + try: + sys.last_type = type(e) + sys.last_value = e + sys.last_traceback = e.__traceback__ + nframes = self.num_frames_to_keep(e.__traceback__) + return "".join( + traceback.format_exception(type(e), e, e.__traceback__, -nframes) + ) + finally: + e = None # type: ignore + + def push(self, line: str) -> ConsoleFuture: + """Push a line to the interpreter. + + The line should not have a trailing newline; it may have internal + newlines. The line is appended to a buffer and the interpreter's + runsource() method is called with the concatenated contents of the + buffer as source. If this indicates that the command was executed or + invalid, the buffer is reset; otherwise, the command is incomplete, and + the buffer is left as it was after the line was appended. + + The return value is the result of calling :any:`Console.runsource` on the current buffer + contents. + """ + self.buffer.append(line) + source = "\n".join(self.buffer) + result = self.runsource(source, self.filename) + if result.syntax_check != INCOMPLETE: + self.buffer = [] + return result + + def complete(self, source: str) -> Tuple[List[str], int]: + """Use Python's rlcompleter to complete the source string using the :any:`globals ` namespace. + + Finds last "word" in the source string and completes it with rlcompleter. Word + breaks are determined by the set of characters in + :any:`completer_word_break_characters `. + + Parameters + ---------- + source : str + The source string to complete at the end. + + Returns + ------- + completions : List[str] + A list of completion strings. + start : int + The index where completion starts. + + Examples + -------- + >>> shell = Console() + >>> shell.complete("str.isa") + (['str.isalnum(', 'str.isalpha(', 'str.isascii('], 0) + >>> shell.complete("a = 5 ; str.isa") + (['str.isalnum(', 'str.isalpha(', 'str.isascii('], 8) + """ + start = max(map(source.rfind, self.completer_word_break_characters)) + 1 + source = source[start:] + if "." in source: + completions = self._completer.attr_matches(source) # type: ignore + else: + completions = self._completer.global_matches(source) # type: ignore + return completions, start + + +def repr_shorten( + value: Any, limit: int = 1000, split: Optional[int] = None, separator: str = "..." +) -> str: + """Compute the string representation of ``value`` and shorten it + if necessary. + + If it is longer than ``limit`` then return the firsts ``split`` + characters and the last ``split`` characters seperated by '...'. + Default value for ``split`` is `limit // 2`. + """ + if split is None: + split = limit // 2 + text = repr(value) + if len(text) > limit: + text = f"{text[:split]}{separator}{text[-split:]}" + return text diff --git a/src/py/pyodide/__init__.py b/src/py/pyodide/__init__.py index 7535c698e..4824913c1 100644 --- a/src/py/pyodide/__init__.py +++ b/src/py/pyodide/__init__.py @@ -49,6 +49,7 @@ __all__ = [ "unregister_js_module", "create_once_callable", "create_proxy", + "console", "should_quiet", "ConversionError", "destroy_proxies", diff --git a/src/py/pyodide/console.py b/src/py/pyodide/console.py index 702449d4b..c7a2f84f6 100644 --- a/src/py/pyodide/console.py +++ b/src/py/pyodide/console.py @@ -1,388 +1,39 @@ -import ast -import asyncio -import code -from codeop import Compile, CommandCompiler, _features # type: ignore -from contextlib import ( - contextmanager, - redirect_stdout, - redirect_stderr, - ExitStack, -) -from contextlib import _RedirectStream # type: ignore -import rlcompleter -import platform -import sys -import traceback -from typing import Optional, Callable, Any, List, Tuple +from _pyodide.console import Console, repr_shorten, ConsoleFuture +import _pyodide.console + +BANNER = _pyodide.console.BANNER # type: ignore +from _pyodide._base import CodeRunner -from _pyodide._base import eval_code_async, should_quiet, CodeRunner from ._core import IN_BROWSER # this import can fail when we are outside a browser (e.g. for tests) if IN_BROWSER: from pyodide_js import loadPackagesFromImports as _load_packages_from_imports - from asyncio import ensure_future else: - from asyncio import Future - - 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 async def _load_packages_from_imports(*args): pass -__all__ = ["repr_shorten", "BANNER"] +__all__ = ["Console", "PyodideConsole", "Banner", "repr_shorten"] -def _banner(): - """A banner similar to the one printed by the real Python interpreter.""" - # copied from https://github.com/python/cpython/blob/799f8489d418b7f9207d333eac38214931bd7dcc/Lib/code.py#L214 - cprt = 'Type "help", "copyright", "credits" or "license" for more information.' - version = platform.python_version() - build = f"({', '.join(platform.python_build())})" - return f"Python {version} {build} on WebAssembly VM\n{cprt}" +class PyodideConsole(Console): + """A subclass of :any:`Console` that uses :any:`pyodide.loadPackagesFromImports` before running the code.""" + async def runcode(self, source: str, code: CodeRunner) -> ConsoleFuture: + """Execute a code object. -BANNER = _banner() -del _banner - - -class redirect_stdin(_RedirectStream): - _stream = "stdin" - - -class _WriteStream: - """A utility class so we can specify our own handlers for writes to sdout, stderr""" - - def __init__(self, write_handler, name=None): - self.write_handler = write_handler - self.name = name - - def write(self, text): - self.write_handler(text) - - def flush(self): - pass - - -class _ReadStream: - """A utility class so we can specify our own handler for reading from stdin""" - - def __init__(self, read_handler, name=None): - self.read_handler = read_handler - self.name = name - - def readline(self, n=-1): - return self.read_handler(n) - - def flush(self): - pass - - -class _CodeRunnerCompile(Compile): - """Compile code with CodeRunner, and remember future imports - - Instances of this class behave much like the built-in compile function, - but if one is used to compile text containing a future statement, it - "remembers" and compiles all subsequent program texts with the statement in - force. It uses CodeRunner instead of the built in compile. - """ - - def __init__( - self, - *, - return_mode="last_expr", - quiet_trailing_semicolon=True, - flags=0x0, - ): - super().__init__() - self.flags |= flags - self.return_mode = return_mode - self.quiet_trailing_semicolon = quiet_trailing_semicolon - - def __call__(self, source, filename, symbol) -> CodeRunner: # type: ignore - return_mode = self.return_mode - if self.quiet_trailing_semicolon and should_quiet(source): - return_mode = None - code_runner = CodeRunner( - source, - mode=symbol, - filename=filename, - return_mode=return_mode, - flags=self.flags, - ).compile() - for feature in _features: - if code_runner.code.co_flags & feature.compiler_flag: - self.flags |= feature.compiler_flag - return code_runner - - -class _CodeRunnerCommandCompiler(CommandCompiler): - """Compile code with CodeRunner, and remember future imports, return None if - code is incomplete. - - Instances of this class have __call__ methods identical in signature to - compile; the difference is that if the instance compiles program text - containing a __future__ statement, the instance 'remembers' and compiles all - subsequent program texts with the statement in force. - - If the source is determined to be incomplete, will suppress the SyntaxError - and return ``None``. - """ - - def __init__( - self, - *, - return_mode="last_expr", - quiet_trailing_semicolon=True, - flags=0x0, - ): - self.compiler = _CodeRunnerCompile( - return_mode=return_mode, - quiet_trailing_semicolon=quiet_trailing_semicolon, - flags=flags, - ) - - def __call__(self, source, filename="", symbol="single") -> CodeRunner: # type: ignore - return super().__call__(source, filename, symbol) # type: ignore - - -class _InteractiveConsole(code.InteractiveConsole): - """Interactive Pyodide console - - Base implementation for an interactive console that manages - stdout/stderr redirection. Since packages are loaded before running - code, :any:`_InteractiveConsole.runcode` returns a JS promise. - - ``self.stdout_callback`` and ``self.stderr_callback`` can be overloaded. - - Parameters - ---------- - locals - Namespace to evaluate code. - stdout_callback - Function to call at each ``sys.stdout`` flush. - stderr_callback - Function to call at each ``sys.stderr`` flush. - persistent_stream_redirection - Whether or not the std redirection should be kept between calls to - ``runcode``. - """ - - def __init__( - self, - locals: Optional[dict] = None, - *, - stdout_callback: Optional[Callable[[str], None]] = None, - stderr_callback: Optional[Callable[[str], None]] = None, - stdin_callback: Optional[Callable[[str], None]] = None, - persistent_stream_redirection: bool = False, - ): - super().__init__(locals) - self._stdout = None - self._stderr = None - self.stdout_callback = stdout_callback - self.stderr_callback = stderr_callback - self.stdin_callback = stdin_callback - self._streams_redirected = False - self._stream_generator = None # track persistent stream redirection - if persistent_stream_redirection: - self.persistent_redirect_streams() - 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;]]\\n" - self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT # type: ignore - - def persistent_redirect_streams(self): - """Redirect stdin/stdout/stderr persistently""" - if self._stream_generator: - return - self._stream_generator = self._stdstreams_redirections_inner() - next(self._stream_generator) # trigger stream redirection - # streams will be reverted to normal when self._stream_generator is destroyed. - - def persistent_restore_streams(self): - """Restore stdin/stdout/stderr if they have been persistently redirected""" - # allowing _stream_generator to be garbage collected restores the streams - self._stream_generator = None - - @contextmanager - def redirect_streams(self): - """A context manager to redirect standard streams. - - This supports nesting.""" - yield from self._stdstreams_redirections_inner() - - def _stdstreams_redirections_inner(self): - """This is the generator which implements redirect_streams and the stdstreams_redirections""" - # already redirected? - if self._streams_redirected: - yield - return - redirects = [] - if self.stdout_callback: - redirects.append( - redirect_stdout( - _WriteStream(self.stdout_callback, name=sys.stdout.name) - ) - ) - if self.stderr_callback: - redirects.append( - redirect_stderr( - _WriteStream(self.stderr_callback, name=sys.stderr.name) - ) - ) - if self.stdin_callback: - redirects.append( - redirect_stdin(_ReadStream(self.stdin_callback, name=sys.stdin.name)) - ) - try: - self._streams_redirected = True - with ExitStack() as stack: - for redirect in redirects: - stack.enter_context(redirect) - yield - finally: - self._streams_redirected = False - - def flush_all(self): - """Force stdout/stderr flush.""" - with self.redirect_streams(): - sys.stdout.flush() - sys.stderr.flush() - - def runsource(self, *args, **kwargs): - """Force streams redirection. - - Syntax errors are not thrown by :any:`_InteractiveConsole.runcode` but - here in :any:`_InteractiveConsole.runsource`. This is why we force - redirection here since doing twice is not an issue. - """ - - with self.redirect_streams(): - return super().runsource(*args, **kwargs) - - def runcode(self, code): - """Load imported packages then run code, async. - - To achieve nice result representation, the interactive console is fully - implemented in Python. The interactive console api is synchronous, but - we want to implement asynchronous package loading and top level await. - Thus, instead of blocking like it normally would, this this function - sets the future ``self.run_complete``. If you need the result of the - computation, you should await for it. - """ - source = "\n".join(self.buffer) - self.run_complete = ensure_future( - self.load_packages_and_run(self.run_complete, source) - ) - - def num_frames_to_keep(self, tb): - keep_frames = False - kept_frames = 0 - # Try to trim out stack frames inside our code - for (frame, _) in traceback.walk_tb(tb): - keep_frames = keep_frames or frame.f_code.co_filename == "" - keep_frames = keep_frames or frame.f_code.co_filename == "" - if keep_frames: - kept_frames += 1 - return kept_frames - - async def load_packages_and_run(self, run_complete, source): - try: - await run_complete - except BaseException: - # Throw away old error - pass - with self.redirect_streams(): - await _load_packages_from_imports(source) - try: - result = await eval_code_async( - source, self.locals, filename="" - ) - except BaseException as e: - nframes = self.num_frames_to_keep(e.__traceback__) - traceback.print_exception(type(e), e, e.__traceback__, -nframes) - 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 complete(self, source: str) -> Tuple[List[str], int]: - """Use CPython's rlcompleter to complete a source from local namespace. - - You can use ``completer_word_break_characters`` to get/set the - way ``source`` is splitted to find the last part to be completed. - - Parameters - ---------- - source - The source string to complete at the end. + All exceptions are caught except SystemExit, which is reraised. Returns ------- - completions - A list of completion strings. - start - The index where completion starts. - - Examples - -------- - >>> shell = _InteractiveConsole() - >>> shell.complete("str.isa") - (['str.isalnum(', 'str.isalpha(', 'str.isascii('], 0) - >>> shell.complete("a = 5 ; str.isa") - (['str.isalnum(', 'str.isalpha(', 'str.isascii('], 8) + The return value is a dependent sum type with the following possibilities: + * `("success", result : Any)` -- the code executed successfully + * `("exception", message : str)` -- An exception occurred. `message` is the + result of calling :any:`Console.formattraceback`. """ - start = max(map(source.rfind, self.completer_word_break_characters)) + 1 - source = source[start:] - if "." in source: - completions = self._completer.attr_matches(source) # type: ignore - else: - completions = self._completer.global_matches(source) # type: ignore - return completions, start + from pyodide_js import loadPackagesFromImports - 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 = "..." -): - """Compute the string representation of ``value`` and shorten it - if necessary. - - If it is longer than ``limit`` then return the firsts ``split`` - characters and the last ``split`` characters seperated by '...'. - Default value for ``split`` is `limit // 2`. - """ - if split is None: - split = limit // 2 - text = repr(value) - if len(text) > limit: - text = f"{text[:split]}{separator}{text[-split:]}" - return text + await loadPackagesFromImports(source) + return await super().runcode(source, code) diff --git a/src/py/pyodide/webloop.py b/src/py/pyodide/webloop.py index 1e3397ac3..3b0ebdaff 100644 --- a/src/py/pyodide/webloop.py +++ b/src/py/pyodide/webloop.py @@ -390,3 +390,6 @@ class WebLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore def set_event_loop(self, loop: asyncio.AbstractEventLoop): """Set the current event loop""" self._default_loop = loop + + +__all__ = ["WebLoop", "WebLoopPolicy"] diff --git a/src/templates/console.html b/src/templates/console.html index 4885295f5..ec2978353 100644 --- a/src/templates/console.html +++ b/src/templates/console.html @@ -17,9 +17,11 @@