diff --git a/IPython/core/async_helpers.py b/IPython/core/async_helpers.py index e8096e0..a6ff860 100644 --- a/IPython/core/async_helpers.py +++ b/IPython/core/async_helpers.py @@ -1,28 +1,35 @@ """ Async helper function that are invalid syntax on Python 3.5 and below. -Known limitation and possible improvement. +This code is best effort, and may have edge cases not behaving as expected. In +particular it contain a number of heuristics to detect whether code is +effectively async and need to run in an event loop or not. -Top level code that contain a return statement (instead of, or in addition to -await) will be detected as requiring being wrapped in async calls. This should -be prevented as early return will not work. +Some constructs (like top-level `return`, or `yield`) are taken care of +explicitly to actually raise a SyntaxError and stay as close as possible to +Python semantics. """ import ast import sys -import inspect from textwrap import dedent, indent -from types import CodeType -def _asyncio_runner(coro): - """ - Handler for asyncio autoawait - """ - import asyncio +class _AsyncIORunner: + + def __call__(self, coro): + """ + Handler for asyncio autoawait + """ + import asyncio + + return asyncio.get_event_loop().run_until_complete(coro) - return asyncio.get_event_loop().run_until_complete(coro) + def __str__(self): + return 'asyncio' + +_asyncio_runner = _AsyncIORunner() def _curio_runner(coroutine): @@ -36,12 +43,14 @@ def _curio_runner(coroutine): def _trio_runner(async_fn): import trio + async def loc(coro): """ We need the dummy no-op async def to protect from trio's internal. See https://github.com/python-trio/trio/issues/89 """ return await coro + return trio.run(loc, async_fn) @@ -60,7 +69,9 @@ def _pseudo_sync_runner(coro): return exc.value else: # TODO: do not raise but return an execution result with the right info. - raise RuntimeError("{coro_name!r} needs a real async loop".format(coro_name=coro.__name__)) + raise RuntimeError( + "{coro_name!r} needs a real async loop".format(coro_name=coro.__name__) + ) def _asyncify(code: str) -> str: @@ -69,7 +80,7 @@ def _asyncify(code: str) -> str: And setup a bit of context to run it later. """ res = dedent( - """ + """ async def __wrapper__(): try: {usercode} @@ -86,12 +97,13 @@ class _AsyncSyntaxErrorVisitor(ast.NodeVisitor): the implementation involves wrapping the repl in an async function, it is erroneously allowed (e.g. yield or return at the top level) """ + def generic_visit(self, node): func_types = (ast.FunctionDef, ast.AsyncFunctionDef) invalid_types = (ast.Return, ast.Yield, ast.YieldFrom) if isinstance(node, func_types): - return # Don't recurse into functions + return # Don't recurse into functions elif isinstance(node, invalid_types): raise SyntaxError() else: diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 3f4950d..8230a01 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2806,7 +2806,7 @@ class InteractiveShell(SingletonConfigurable): self.events.trigger('post_run_cell', result) return result - def _run_cell(self, raw_cell, store_history, silent, shell_futures): + def _run_cell(self, raw_cell:str, store_history:bool, silent:bool, shell_futures:bool): """Internal method to run a complete IPython cell.""" coro = self.run_cell_async( raw_cell, @@ -2815,11 +2815,16 @@ class InteractiveShell(SingletonConfigurable): shell_futures=shell_futures, ) + # run_cell_async is async, but may not actually need and eventloop. + # when this is the case, we want to run it using the pseudo_sync_runner + # so that code can invoke eventloops (for example via the %run , and + # `%paste` magic. try: interactivity = coro.send(None) except StopIteration as exc: return exc.value + # if code was not async, sending `None` was actually executing the code. if isinstance(interactivity, ExecutionResult): return interactivity @@ -3159,7 +3164,6 @@ class InteractiveShell(SingletonConfigurable): return eval(code_obj, user_ns) - @asyncio.coroutine def run_code(self, code_obj, result=None, *, async_=False): """Execute a code object. diff --git a/IPython/core/magics/basic.py b/IPython/core/magics/basic.py index af0ab4d..6a557fc 100644 --- a/IPython/core/magics/basic.py +++ b/IPython/core/magics/basic.py @@ -632,7 +632,12 @@ class AsyncMagics(BasicMagics): runner, and activate autoawait. If the object is a fully qualified object name, attempt to import it and - set it as the runner, and activate autoawait.""" + set it as the runner, and activate autoawait. + + + The exact behavior of autoawait is experimental and subject to change + across version of IPython and Python. + """ param = parameter_s.strip() d = {True: "on", False: "off"} diff --git a/IPython/terminal/embed.py b/IPython/terminal/embed.py index ef519ea..188844f 100644 --- a/IPython/terminal/embed.py +++ b/IPython/terminal/embed.py @@ -19,23 +19,6 @@ from IPython.terminal.ipapp import load_default_config from traitlets import Bool, CBool, Unicode from IPython.utils.io import ask_yes_no -from contextlib import contextmanager - -_sentinel = object() -@contextmanager -def new_context(): - import trio._core._run as tcr - old_runner = getattr(tcr.GLOBAL_RUN_CONTEXT, 'runner', _sentinel) - old_task = getattr(tcr.GLOBAL_RUN_CONTEXT, 'task', None) - if old_runner is not _sentinel: - del tcr.GLOBAL_RUN_CONTEXT.runner - tcr.GLOBAL_RUN_CONTEXT.task = None - yield - if old_runner is not _sentinel: - tcr.GLOBAL_RUN_CONTEXT.runner = old_runner - tcr.GLOBAL_RUN_CONTEXT.task = old_task - - class KillEmbedded(Exception):pass # kept for backward compatibility as IPython 6 was released with @@ -400,12 +383,11 @@ def embed(**kwargs): cls = type(saved_shell_instance) cls.clear_instance() frame = sys._getframe(1) - with new_context(): - shell = InteractiveShellEmbed.instance(_init_location_id='%s:%s' % ( - frame.f_code.co_filename, frame.f_lineno), **kwargs) - shell(header=header, stack_depth=2, compile_flags=compile_flags, - _call_location_id='%s:%s' % (frame.f_code.co_filename, frame.f_lineno)) - InteractiveShellEmbed.clear_instance() + shell = InteractiveShellEmbed.instance(_init_location_id='%s:%s' % ( + frame.f_code.co_filename, frame.f_lineno), **kwargs) + shell(header=header, stack_depth=2, compile_flags=compile_flags, + _call_location_id='%s:%s' % (frame.f_code.co_filename, frame.f_lineno)) + InteractiveShellEmbed.clear_instance() #restore previous instance if saved_shell_instance is not None: cls = type(saved_shell_instance) diff --git a/docs/source/interactive/autoawait.rst b/docs/source/interactive/autoawait.rst index 4a60e4e..90931b1 100644 --- a/docs/source/interactive/autoawait.rst +++ b/docs/source/interactive/autoawait.rst @@ -3,6 +3,11 @@ Asynchronous in REPL: Autoawait =============================== +.. note:: + + This feature is experimental and behavior can change betwen python and + IPython version without prior deprecation. + Starting with IPython 7.0, and when user Python 3.6 and above, IPython offer the ability to run asynchronous code from the REPL. Constructs which are :exc:`SyntaxError` s in the Python REPL can be used seamlessly in IPython. @@ -12,10 +17,10 @@ notebook interface or any other frontend using the Jupyter protocol will need to use a newer version of IPykernel. The details of how async code runs in IPykernel will differ between IPython, IPykernel and their versions. -When a supported library is used, IPython will automatically `await` Futures and -Coroutines in the REPL. This will happen if an :ref:`await ` (or any -other async constructs like async-with, async-for) is use at top level scope, or -if any structure valid only in `async def +When a supported library is used, IPython will automatically allow Futures and +Coroutines in the REPL to be ``await`` ed. This will happen if an :ref:`await +` (or any other async constructs like async-with, async-for) is use at +top level scope, or if any structure valid only in `async def `_ function context are present. For example, the following being a syntax error in the Python REPL:: @@ -58,15 +63,13 @@ use the :magic:`%autoawait` magic to toggle the behavior at runtime:: In [1]: %autoawait False In [2]: %autoawait - IPython autoawait is `Off`, and set to use `IPython.core.interactiveshell._asyncio_runner` + IPython autoawait is `Off`, and set to use `asyncio` By default IPython will assume integration with Python's provided :mod:`asyncio`, but integration with other libraries is provided. In particular -we provide experimental integration with the ``curio`` and ``trio`` library, the -later one being necessary if you require the ability to do nested call of -IPython's ``embed()`` functionality. +we provide experimental integration with the ``curio`` and ``trio`` library. You can switch current integration by using the ``c.InteractiveShell.loop_runner`` option or the ``autoawait