Skip to content

Commit 1af2bf7

Browse files
committed
asyncio: Support PEP 492. Issue python#24017.
1 parent d7e19bb commit 1af2bf7

File tree

6 files changed

+116
-27
lines changed

6 files changed

+116
-27
lines changed

Lib/asyncio/base_events.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,8 @@ def __init__(self):
191191
self._thread_id = None
192192
self._clock_resolution = time.get_clock_info('monotonic').resolution
193193
self._exception_handler = None
194-
self._debug = (not sys.flags.ignore_environment
195-
and bool(os.environ.get('PYTHONASYNCIODEBUG')))
194+
self.set_debug((not sys.flags.ignore_environment
195+
and bool(os.environ.get('PYTHONASYNCIODEBUG'))))
196196
# In debug mode, if the execution of a callback or a step of a task
197197
# exceed this duration in seconds, the slow callback/task is logged.
198198
self.slow_callback_duration = 0.1
@@ -360,13 +360,18 @@ def close(self):
360360
return
361361
if self._debug:
362362
logger.debug("Close %r", self)
363-
self._closed = True
364-
self._ready.clear()
365-
self._scheduled.clear()
366-
executor = self._default_executor
367-
if executor is not None:
368-
self._default_executor = None
369-
executor.shutdown(wait=False)
363+
try:
364+
self._closed = True
365+
self._ready.clear()
366+
self._scheduled.clear()
367+
executor = self._default_executor
368+
if executor is not None:
369+
self._default_executor = None
370+
executor.shutdown(wait=False)
371+
finally:
372+
# It is important to unregister "sys.coroutine_wrapper"
373+
# if it was registered.
374+
self.set_debug(False)
370375

371376
def is_closed(self):
372377
"""Returns True if the event loop was closed."""
@@ -1199,3 +1204,27 @@ def get_debug(self):
11991204

12001205
def set_debug(self, enabled):
12011206
self._debug = enabled
1207+
wrapper = coroutines.debug_wrapper
1208+
1209+
try:
1210+
set_wrapper = sys.set_coroutine_wrapper
1211+
except AttributeError:
1212+
pass
1213+
else:
1214+
current_wrapper = sys.get_coroutine_wrapper()
1215+
if enabled:
1216+
if current_wrapper not in (None, wrapper):
1217+
warnings.warn(
1218+
"loop.set_debug(True): cannot set debug coroutine "
1219+
"wrapper; another wrapper is already set %r" %
1220+
current_wrapper, RuntimeWarning)
1221+
else:
1222+
set_wrapper(wrapper)
1223+
else:
1224+
if current_wrapper not in (None, wrapper):
1225+
warnings.warn(
1226+
"loop.set_debug(False): cannot unset debug coroutine "
1227+
"wrapper; another wrapper was set %r" %
1228+
current_wrapper, RuntimeWarning)
1229+
else:
1230+
set_wrapper(None)

Lib/asyncio/coroutines.py

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
from .log import logger
1515

1616

17+
_PY35 = sys.version_info >= (3, 5)
18+
19+
1720
# Opcode of "yield from" instruction
1821
_YIELD_FROM = opcode.opmap['YIELD_FROM']
1922

@@ -30,6 +33,27 @@
3033
and bool(os.environ.get('PYTHONASYNCIODEBUG')))
3134

3235

36+
try:
37+
types.coroutine
38+
except AttributeError:
39+
native_coroutine_support = False
40+
else:
41+
native_coroutine_support = True
42+
43+
try:
44+
_iscoroutinefunction = inspect.iscoroutinefunction
45+
except AttributeError:
46+
_iscoroutinefunction = lambda func: False
47+
48+
try:
49+
inspect.CO_COROUTINE
50+
except AttributeError:
51+
_is_native_coro_code = lambda code: False
52+
else:
53+
_is_native_coro_code = lambda code: (code.co_flags &
54+
inspect.CO_COROUTINE)
55+
56+
3357
# Check for CPython issue #21209
3458
def has_yield_from_bug():
3559
class MyGen:
@@ -54,16 +78,27 @@ def yield_from_gen(gen):
5478
del has_yield_from_bug
5579

5680

81+
def debug_wrapper(gen):
82+
# This function is called from 'sys.set_coroutine_wrapper'.
83+
# We only wrap here coroutines defined via 'async def' syntax.
84+
# Generator-based coroutines are wrapped in @coroutine
85+
# decorator.
86+
if _is_native_coro_code(gen.gi_code):
87+
return CoroWrapper(gen, None)
88+
else:
89+
return gen
90+
91+
5792
class CoroWrapper:
5893
# Wrapper for coroutine object in _DEBUG mode.
5994

60-
def __init__(self, gen, func):
61-
assert inspect.isgenerator(gen), gen
95+
def __init__(self, gen, func=None):
96+
assert inspect.isgenerator(gen) or inspect.iscoroutine(gen), gen
6297
self.gen = gen
63-
self.func = func
98+
self.func = func # Used to unwrap @coroutine decorator
6499
self._source_traceback = traceback.extract_stack(sys._getframe(1))
65-
# __name__, __qualname__, __doc__ attributes are set by the coroutine()
66-
# decorator
100+
self.__name__ = getattr(gen, '__name__', None)
101+
self.__qualname__ = getattr(gen, '__qualname__', None)
67102

68103
def __repr__(self):
69104
coro_repr = _format_coroutine(self)
@@ -75,6 +110,9 @@ def __repr__(self):
75110
def __iter__(self):
76111
return self
77112

113+
if _PY35:
114+
__await__ = __iter__ # make compatible with 'await' expression
115+
78116
def __next__(self):
79117
return next(self.gen)
80118

@@ -133,6 +171,14 @@ def coroutine(func):
133171
If the coroutine is not yielded from before it is destroyed,
134172
an error message is logged.
135173
"""
174+
is_coroutine = _iscoroutinefunction(func)
175+
if is_coroutine and _is_native_coro_code(func.__code__):
176+
# In Python 3.5 that's all we need to do for coroutines
177+
# defiend with "async def".
178+
# Wrapping in CoroWrapper will happen via
179+
# 'sys.set_coroutine_wrapper' function.
180+
return func
181+
136182
if inspect.isgeneratorfunction(func):
137183
coro = func
138184
else:
@@ -144,18 +190,22 @@ def coro(*args, **kw):
144190
return res
145191

146192
if not _DEBUG:
147-
wrapper = coro
193+
if native_coroutine_support:
194+
wrapper = types.coroutine(coro)
195+
else:
196+
wrapper = coro
148197
else:
149198
@functools.wraps(func)
150199
def wrapper(*args, **kwds):
151-
w = CoroWrapper(coro(*args, **kwds), func)
200+
w = CoroWrapper(coro(*args, **kwds), func=func)
152201
if w._source_traceback:
153202
del w._source_traceback[-1]
154-
if hasattr(func, '__name__'):
155-
w.__name__ = func.__name__
156-
if hasattr(func, '__qualname__'):
157-
w.__qualname__ = func.__qualname__
158-
w.__doc__ = func.__doc__
203+
# Python < 3.5 does not implement __qualname__
204+
# on generator objects, so we set it manually.
205+
# We use getattr as some callables (such as
206+
# functools.partial may lack __qualname__).
207+
w.__name__ = getattr(func, '__name__', None)
208+
w.__qualname__ = getattr(func, '__qualname__', None)
159209
return w
160210

161211
wrapper._is_coroutine = True # For iscoroutinefunction().
@@ -164,7 +214,8 @@ def wrapper(*args, **kwds):
164214

165215
def iscoroutinefunction(func):
166216
"""Return True if func is a decorated coroutine function."""
167-
return getattr(func, '_is_coroutine', False)
217+
return (getattr(func, '_is_coroutine', False) or
218+
_iscoroutinefunction(func))
168219

169220

170221
_COROUTINE_TYPES = (types.GeneratorType, CoroWrapper)

Lib/asyncio/futures.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
_FINISHED = 'FINISHED'
2020

2121
_PY34 = sys.version_info >= (3, 4)
22+
_PY35 = sys.version_info >= (3, 5)
2223

2324
Error = concurrent.futures._base.Error
2425
CancelledError = concurrent.futures.CancelledError
@@ -387,6 +388,9 @@ def __iter__(self):
387388
assert self.done(), "yield from wasn't used with future"
388389
return self.result() # May raise too.
389390

391+
if _PY35:
392+
__await__ = __iter__ # make compatible with 'await' expression
393+
390394

391395
def wrap_future(fut, *, loop=None):
392396
"""Wrap concurrent.futures.Future object."""

Lib/asyncio/tasks.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import inspect
1212
import linecache
1313
import sys
14+
import types
1415
import traceback
1516
import warnings
1617
import weakref
@@ -73,7 +74,10 @@ def __init__(self, coro, *, loop=None):
7374
super().__init__(loop=loop)
7475
if self._source_traceback:
7576
del self._source_traceback[-1]
76-
self._coro = iter(coro) # Use the iterator just in case.
77+
if coro.__class__ is types.GeneratorType:
78+
self._coro = coro
79+
else:
80+
self._coro = iter(coro) # Use the iterator just in case.
7781
self._fut_waiter = None
7882
self._must_cancel = False
7983
self._loop.call_soon(self._step)
@@ -236,7 +240,7 @@ def _step(self, value=None, exc=None):
236240
elif value is not None:
237241
result = coro.send(value)
238242
else:
239-
result = next(coro)
243+
result = coro.send(None)
240244
except StopIteration as exc:
241245
self.set_result(exc.value)
242246
except futures.CancelledError as exc:

Lib/test/test_asyncio/test_base_events.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ def test_not_implemented(self):
6161
NotImplementedError,
6262
self.loop._make_write_pipe_transport, m, m)
6363
gen = self.loop._make_subprocess_transport(m, m, m, m, m, m, m)
64-
self.assertRaises(NotImplementedError, next, iter(gen))
64+
with self.assertRaises(NotImplementedError):
65+
gen.send(None)
6566

6667
def test_close(self):
6768
self.assertFalse(self.loop.is_closed())

Lib/test/test_asyncio/test_tasks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,7 +1638,7 @@ def foo():
16381638
return a
16391639

16401640
def call(arg):
1641-
cw = asyncio.coroutines.CoroWrapper(foo(), foo)
1641+
cw = asyncio.coroutines.CoroWrapper(foo())
16421642
cw.send(None)
16431643
try:
16441644
cw.send(arg)
@@ -1653,7 +1653,7 @@ def call(arg):
16531653
def test_corowrapper_weakref(self):
16541654
wd = weakref.WeakValueDictionary()
16551655
def foo(): yield from []
1656-
cw = asyncio.coroutines.CoroWrapper(foo(), foo)
1656+
cw = asyncio.coroutines.CoroWrapper(foo())
16571657
wd['cw'] = cw # Would fail without __weakref__ slot.
16581658
cw.gen = None # Suppress warning from __del__.
16591659

0 commit comments

Comments
 (0)