Format: text/x-python
⬇ Download File (4.2 KB)
   1  """
   2  Async helper function that are invalid syntax on Python 3.5 and below.
   3  
   4  This code is best effort, and may have edge cases not behaving as expected. In
   5  particular it contain a number of heuristics to detect whether code is
   6  effectively async and need to run in an event loop or not.
   7  
   8  Some constructs (like top-level `return`, or `yield`) are taken care of
   9  explicitly to actually raise a SyntaxError and stay as close as possible to
  10  Python semantics.
  11  """
  12  
  13  import ast
  14  import asyncio
  15  import inspect
  16  from functools import wraps
  17  
  18  _asyncio_event_loop = None
  19  
  20  
  21  def get_asyncio_loop():
  22      """asyncio has deprecated get_event_loop
  23  
  24      Replicate it here, with our desired semantics:
  25  
  26      - always returns a valid, not-closed loop
  27      - not thread-local like asyncio's,
  28        because we only want one loop for IPython
  29      - if called from inside a coroutine (e.g. in ipykernel),
  30        return the running loop
  31  
  32      .. versionadded:: 8.0
  33      """
  34      try:
  35          return asyncio.get_running_loop()
  36      except RuntimeError:
  37          # not inside a coroutine,
  38          # track our own global
  39          pass
  40  
  41      # not thread-local like asyncio's,
  42      # because we only track one event loop to run for IPython itself,
  43      # always in the main thread.
  44      global _asyncio_event_loop
  45      if _asyncio_event_loop is None or _asyncio_event_loop.is_closed():
  46          _asyncio_event_loop = asyncio.new_event_loop()
  47      return _asyncio_event_loop
  48  
  49  
  50  class _AsyncIORunner:
  51      def __call__(self, coro):
  52          """
  53          Handler for asyncio autoawait
  54          """
  55          return get_asyncio_loop().run_until_complete(coro)
  56  
  57      def __str__(self):
  58          return "asyncio"
  59  
  60  
  61  _asyncio_runner = _AsyncIORunner()
  62  
  63  
  64  class _AsyncIOProxy:
  65      """Proxy-object for an asyncio
  66  
  67      Any coroutine methods will be wrapped in event_loop.run_
  68      """
  69  
  70      def __init__(self, obj, event_loop):
  71          self._obj = obj
  72          self._event_loop = event_loop
  73  
  74      def __repr__(self):
  75          return f"<_AsyncIOProxy({self._obj!r})>"
  76  
  77      def __getattr__(self, key):
  78          attr = getattr(self._obj, key)
  79          if inspect.iscoroutinefunction(attr):
  80              # if it's a coroutine method,
  81              # return a threadsafe wrapper onto the _current_ asyncio loop
  82              @wraps(attr)
  83              def _wrapped(*args, **kwargs):
  84                  concurrent_future = asyncio.run_coroutine_threadsafe(
  85                      attr(*args, **kwargs), self._event_loop
  86                  )
  87                  return asyncio.wrap_future(concurrent_future)
  88  
  89              return _wrapped
  90          else:
  91              return attr
  92  
  93      def __dir__(self):
  94          return dir(self._obj)
  95  
  96  
  97  def _curio_runner(coroutine):
  98      """
  99      handler for curio autoawait
 100      """
 101      import curio
 102  
 103      return curio.run(coroutine)
 104  
 105  
 106  def _trio_runner(async_fn):
 107      import trio
 108  
 109      async def loc(coro):
 110          """
 111          We need the dummy no-op async def to protect from
 112          trio's internal. See https://github.com/python-trio/trio/issues/89
 113          """
 114          return await coro
 115  
 116      return trio.run(loc, async_fn)
 117  
 118  
 119  def _pseudo_sync_runner(coro):
 120      """
 121      A runner that does not really allow async execution, and just advance the coroutine.
 122  
 123      See discussion in https://github.com/python-trio/trio/issues/608,
 124  
 125      Credit to Nathaniel Smith
 126      """
 127      try:
 128          coro.send(None)
 129      except StopIteration as exc:
 130          return exc.value
 131      else:
 132          # TODO: do not raise but return an execution result with the right info.
 133          raise RuntimeError(
 134              "{coro_name!r} needs a real async loop".format(coro_name=coro.__name__)
 135          )
 136  
 137  
 138  def _should_be_async(cell: str) -> bool:
 139      """Detect if a block of code need to be wrapped in an `async def`
 140  
 141      Attempt to parse the block of code, it it compile we're fine.
 142      Otherwise we  wrap if and try to compile.
 143  
 144      If it works, assume it should be async. Otherwise Return False.
 145  
 146      Not handled yet: If the block of code has a return statement as the top
 147      level, it will be seen as async. This is a know limitation.
 148      """
 149      try:
 150          code = compile(
 151              cell, "<>", "exec", flags=getattr(ast, "PyCF_ALLOW_TOP_LEVEL_AWAIT", 0x0)
 152          )
 153          return inspect.CO_COROUTINE & code.co_flags == inspect.CO_COROUTINE
 154      except (SyntaxError, MemoryError):
 155          return False
 156