Commit 1af2bf75 authored by Yury Selivanov's avatar Yury Selivanov

asyncio: Support PEP 492. Issue #24017.

parent d7e19bb5
...@@ -191,8 +191,8 @@ class BaseEventLoop(events.AbstractEventLoop): ...@@ -191,8 +191,8 @@ class BaseEventLoop(events.AbstractEventLoop):
self._thread_id = None self._thread_id = None
self._clock_resolution = time.get_clock_info('monotonic').resolution self._clock_resolution = time.get_clock_info('monotonic').resolution
self._exception_handler = None self._exception_handler = None
self._debug = (not sys.flags.ignore_environment self.set_debug((not sys.flags.ignore_environment
and bool(os.environ.get('PYTHONASYNCIODEBUG'))) and bool(os.environ.get('PYTHONASYNCIODEBUG'))))
# In debug mode, if the execution of a callback or a step of a task # In debug mode, if the execution of a callback or a step of a task
# exceed this duration in seconds, the slow callback/task is logged. # exceed this duration in seconds, the slow callback/task is logged.
self.slow_callback_duration = 0.1 self.slow_callback_duration = 0.1
...@@ -360,13 +360,18 @@ class BaseEventLoop(events.AbstractEventLoop): ...@@ -360,13 +360,18 @@ class BaseEventLoop(events.AbstractEventLoop):
return return
if self._debug: if self._debug:
logger.debug("Close %r", self) logger.debug("Close %r", self)
self._closed = True try:
self._ready.clear() self._closed = True
self._scheduled.clear() self._ready.clear()
executor = self._default_executor self._scheduled.clear()
if executor is not None: executor = self._default_executor
self._default_executor = None if executor is not None:
executor.shutdown(wait=False) self._default_executor = None
executor.shutdown(wait=False)
finally:
# It is important to unregister "sys.coroutine_wrapper"
# if it was registered.
self.set_debug(False)
def is_closed(self): def is_closed(self):
"""Returns True if the event loop was closed.""" """Returns True if the event loop was closed."""
...@@ -1199,3 +1204,27 @@ class BaseEventLoop(events.AbstractEventLoop): ...@@ -1199,3 +1204,27 @@ class BaseEventLoop(events.AbstractEventLoop):
def set_debug(self, enabled): def set_debug(self, enabled):
self._debug = enabled self._debug = enabled
wrapper = coroutines.debug_wrapper
try:
set_wrapper = sys.set_coroutine_wrapper
except AttributeError:
pass
else:
current_wrapper = sys.get_coroutine_wrapper()
if enabled:
if current_wrapper not in (None, wrapper):
warnings.warn(
"loop.set_debug(True): cannot set debug coroutine "
"wrapper; another wrapper is already set %r" %
current_wrapper, RuntimeWarning)
else:
set_wrapper(wrapper)
else:
if current_wrapper not in (None, wrapper):
warnings.warn(
"loop.set_debug(False): cannot unset debug coroutine "
"wrapper; another wrapper was set %r" %
current_wrapper, RuntimeWarning)
else:
set_wrapper(None)
...@@ -14,6 +14,9 @@ from . import futures ...@@ -14,6 +14,9 @@ from . import futures
from .log import logger from .log import logger
_PY35 = sys.version_info >= (3, 5)
# Opcode of "yield from" instruction # Opcode of "yield from" instruction
_YIELD_FROM = opcode.opmap['YIELD_FROM'] _YIELD_FROM = opcode.opmap['YIELD_FROM']
...@@ -30,6 +33,27 @@ _DEBUG = (not sys.flags.ignore_environment ...@@ -30,6 +33,27 @@ _DEBUG = (not sys.flags.ignore_environment
and bool(os.environ.get('PYTHONASYNCIODEBUG'))) and bool(os.environ.get('PYTHONASYNCIODEBUG')))
try:
types.coroutine
except AttributeError:
native_coroutine_support = False
else:
native_coroutine_support = True
try:
_iscoroutinefunction = inspect.iscoroutinefunction
except AttributeError:
_iscoroutinefunction = lambda func: False
try:
inspect.CO_COROUTINE
except AttributeError:
_is_native_coro_code = lambda code: False
else:
_is_native_coro_code = lambda code: (code.co_flags &
inspect.CO_COROUTINE)
# Check for CPython issue #21209 # Check for CPython issue #21209
def has_yield_from_bug(): def has_yield_from_bug():
class MyGen: class MyGen:
...@@ -54,16 +78,27 @@ _YIELD_FROM_BUG = has_yield_from_bug() ...@@ -54,16 +78,27 @@ _YIELD_FROM_BUG = has_yield_from_bug()
del has_yield_from_bug del has_yield_from_bug
def debug_wrapper(gen):
# This function is called from 'sys.set_coroutine_wrapper'.
# We only wrap here coroutines defined via 'async def' syntax.
# Generator-based coroutines are wrapped in @coroutine
# decorator.
if _is_native_coro_code(gen.gi_code):
return CoroWrapper(gen, None)
else:
return gen
class CoroWrapper: class CoroWrapper:
# Wrapper for coroutine object in _DEBUG mode. # Wrapper for coroutine object in _DEBUG mode.
def __init__(self, gen, func): def __init__(self, gen, func=None):
assert inspect.isgenerator(gen), gen assert inspect.isgenerator(gen) or inspect.iscoroutine(gen), gen
self.gen = gen self.gen = gen
self.func = func self.func = func # Used to unwrap @coroutine decorator
self._source_traceback = traceback.extract_stack(sys._getframe(1)) self._source_traceback = traceback.extract_stack(sys._getframe(1))
# __name__, __qualname__, __doc__ attributes are set by the coroutine() self.__name__ = getattr(gen, '__name__', None)
# decorator self.__qualname__ = getattr(gen, '__qualname__', None)
def __repr__(self): def __repr__(self):
coro_repr = _format_coroutine(self) coro_repr = _format_coroutine(self)
...@@ -75,6 +110,9 @@ class CoroWrapper: ...@@ -75,6 +110,9 @@ class CoroWrapper:
def __iter__(self): def __iter__(self):
return self return self
if _PY35:
__await__ = __iter__ # make compatible with 'await' expression
def __next__(self): def __next__(self):
return next(self.gen) return next(self.gen)
...@@ -133,6 +171,14 @@ def coroutine(func): ...@@ -133,6 +171,14 @@ def coroutine(func):
If the coroutine is not yielded from before it is destroyed, If the coroutine is not yielded from before it is destroyed,
an error message is logged. an error message is logged.
""" """
is_coroutine = _iscoroutinefunction(func)
if is_coroutine and _is_native_coro_code(func.__code__):
# In Python 3.5 that's all we need to do for coroutines
# defiend with "async def".
# Wrapping in CoroWrapper will happen via
# 'sys.set_coroutine_wrapper' function.
return func
if inspect.isgeneratorfunction(func): if inspect.isgeneratorfunction(func):
coro = func coro = func
else: else:
...@@ -144,18 +190,22 @@ def coroutine(func): ...@@ -144,18 +190,22 @@ def coroutine(func):
return res return res
if not _DEBUG: if not _DEBUG:
wrapper = coro if native_coroutine_support:
wrapper = types.coroutine(coro)
else:
wrapper = coro
else: else:
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwds): def wrapper(*args, **kwds):
w = CoroWrapper(coro(*args, **kwds), func) w = CoroWrapper(coro(*args, **kwds), func=func)
if w._source_traceback: if w._source_traceback:
del w._source_traceback[-1] del w._source_traceback[-1]
if hasattr(func, '__name__'): # Python < 3.5 does not implement __qualname__
w.__name__ = func.__name__ # on generator objects, so we set it manually.
if hasattr(func, '__qualname__'): # We use getattr as some callables (such as
w.__qualname__ = func.__qualname__ # functools.partial may lack __qualname__).
w.__doc__ = func.__doc__ w.__name__ = getattr(func, '__name__', None)
w.__qualname__ = getattr(func, '__qualname__', None)
return w return w
wrapper._is_coroutine = True # For iscoroutinefunction(). wrapper._is_coroutine = True # For iscoroutinefunction().
...@@ -164,7 +214,8 @@ def coroutine(func): ...@@ -164,7 +214,8 @@ def coroutine(func):
def iscoroutinefunction(func): def iscoroutinefunction(func):
"""Return True if func is a decorated coroutine function.""" """Return True if func is a decorated coroutine function."""
return getattr(func, '_is_coroutine', False) return (getattr(func, '_is_coroutine', False) or
_iscoroutinefunction(func))
_COROUTINE_TYPES = (types.GeneratorType, CoroWrapper) _COROUTINE_TYPES = (types.GeneratorType, CoroWrapper)
......
...@@ -19,6 +19,7 @@ _CANCELLED = 'CANCELLED' ...@@ -19,6 +19,7 @@ _CANCELLED = 'CANCELLED'
_FINISHED = 'FINISHED' _FINISHED = 'FINISHED'
_PY34 = sys.version_info >= (3, 4) _PY34 = sys.version_info >= (3, 4)
_PY35 = sys.version_info >= (3, 5)
Error = concurrent.futures._base.Error Error = concurrent.futures._base.Error
CancelledError = concurrent.futures.CancelledError CancelledError = concurrent.futures.CancelledError
...@@ -387,6 +388,9 @@ class Future: ...@@ -387,6 +388,9 @@ class Future:
assert self.done(), "yield from wasn't used with future" assert self.done(), "yield from wasn't used with future"
return self.result() # May raise too. return self.result() # May raise too.
if _PY35:
__await__ = __iter__ # make compatible with 'await' expression
def wrap_future(fut, *, loop=None): def wrap_future(fut, *, loop=None):
"""Wrap concurrent.futures.Future object.""" """Wrap concurrent.futures.Future object."""
......
...@@ -11,6 +11,7 @@ import functools ...@@ -11,6 +11,7 @@ import functools
import inspect import inspect
import linecache import linecache
import sys import sys
import types
import traceback import traceback
import warnings import warnings
import weakref import weakref
...@@ -73,7 +74,10 @@ class Task(futures.Future): ...@@ -73,7 +74,10 @@ class Task(futures.Future):
super().__init__(loop=loop) super().__init__(loop=loop)
if self._source_traceback: if self._source_traceback:
del self._source_traceback[-1] del self._source_traceback[-1]
self._coro = iter(coro) # Use the iterator just in case. if coro.__class__ is types.GeneratorType:
self._coro = coro
else:
self._coro = iter(coro) # Use the iterator just in case.
self._fut_waiter = None self._fut_waiter = None
self._must_cancel = False self._must_cancel = False
self._loop.call_soon(self._step) self._loop.call_soon(self._step)
...@@ -236,7 +240,7 @@ class Task(futures.Future): ...@@ -236,7 +240,7 @@ class Task(futures.Future):
elif value is not None: elif value is not None:
result = coro.send(value) result = coro.send(value)
else: else:
result = next(coro) result = coro.send(None)
except StopIteration as exc: except StopIteration as exc:
self.set_result(exc.value) self.set_result(exc.value)
except futures.CancelledError as exc: except futures.CancelledError as exc:
......
...@@ -61,7 +61,8 @@ class BaseEventLoopTests(test_utils.TestCase): ...@@ -61,7 +61,8 @@ class BaseEventLoopTests(test_utils.TestCase):
NotImplementedError, NotImplementedError,
self.loop._make_write_pipe_transport, m, m) self.loop._make_write_pipe_transport, m, m)
gen = self.loop._make_subprocess_transport(m, m, m, m, m, m, m) gen = self.loop._make_subprocess_transport(m, m, m, m, m, m, m)
self.assertRaises(NotImplementedError, next, iter(gen)) with self.assertRaises(NotImplementedError):
gen.send(None)
def test_close(self): def test_close(self):
self.assertFalse(self.loop.is_closed()) self.assertFalse(self.loop.is_closed())
......
...@@ -1638,7 +1638,7 @@ class TaskTests(test_utils.TestCase): ...@@ -1638,7 +1638,7 @@ class TaskTests(test_utils.TestCase):
return a return a
def call(arg): def call(arg):
cw = asyncio.coroutines.CoroWrapper(foo(), foo) cw = asyncio.coroutines.CoroWrapper(foo())
cw.send(None) cw.send(None)
try: try:
cw.send(arg) cw.send(arg)
...@@ -1653,7 +1653,7 @@ class TaskTests(test_utils.TestCase): ...@@ -1653,7 +1653,7 @@ class TaskTests(test_utils.TestCase):
def test_corowrapper_weakref(self): def test_corowrapper_weakref(self):
wd = weakref.WeakValueDictionary() wd = weakref.WeakValueDictionary()
def foo(): yield from [] def foo(): yield from []
cw = asyncio.coroutines.CoroWrapper(foo(), foo) cw = asyncio.coroutines.CoroWrapper(foo())
wd['cw'] = cw # Would fail without __weakref__ slot. wd['cw'] = cw # Would fail without __weakref__ slot.
cw.gen = None # Suppress warning from __del__. cw.gen = None # Suppress warning from __del__.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment