Skip to content

Commit

Permalink
bpo-43216: Remove @asyncio.coroutine (GH-26369)
Browse files Browse the repository at this point in the history
Remove the @asyncio.coroutine decorator
enabling legacy generator-based coroutines to be compatible with async/await
code; remove asyncio.coroutines.CoroWrapper used for wrapping
legacy coroutine objects in the debug mode.

The decorator has been deprecated
since Python 3.8 and the removal was initially scheduled for Python 3.10.
  • Loading branch information
illia-v authored Jul 1, 2021
1 parent 3623aaa commit a1092f6
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 764 deletions.
60 changes: 0 additions & 60 deletions Doc/library/asyncio-task.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,6 @@ other coroutines::
* a *coroutine object*: an object returned by calling a
*coroutine function*.

asyncio also supports legacy :ref:`generator-based
<asyncio_generator_based_coro>` coroutines.


.. rubric:: Tasks

Expand Down Expand Up @@ -1042,60 +1039,3 @@ Task Object
in the :func:`repr` output of a task object.

.. versionadded:: 3.8


.. _asyncio_generator_based_coro:

Generator-based Coroutines
==========================

.. note::

Support for generator-based coroutines is **deprecated** and
is scheduled for removal in Python 3.10.

Generator-based coroutines predate async/await syntax. They are
Python generators that use ``yield from`` expressions to await
on Futures and other coroutines.

Generator-based coroutines should be decorated with
:func:`@asyncio.coroutine <asyncio.coroutine>`, although this is not
enforced.


.. decorator:: coroutine

Decorator to mark generator-based coroutines.

This decorator enables legacy generator-based coroutines to be
compatible with async/await code::

@asyncio.coroutine
def old_style_coroutine():
yield from asyncio.sleep(1)

async def main():
await old_style_coroutine()

This decorator should not be used for :keyword:`async def`
coroutines.

.. deprecated-removed:: 3.8 3.10

Use :keyword:`async def` instead.

.. function:: iscoroutine(obj)

Return ``True`` if *obj* is a :ref:`coroutine object <coroutine>`.

This method is different from :func:`inspect.iscoroutine` because
it returns ``True`` for generator-based coroutines.

.. function:: iscoroutinefunction(func)

Return ``True`` if *func* is a :ref:`coroutine function
<coroutine>`.

This method is different from :func:`inspect.iscoroutinefunction`
because it returns ``True`` for generator-based coroutine functions
decorated with :func:`@coroutine <coroutine>`.
4 changes: 2 additions & 2 deletions Doc/library/collections.abc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ ABC Inherits from Abstract Methods Mixin

.. note::
In CPython, generator-based coroutines (generators decorated with
:func:`types.coroutine` or :func:`asyncio.coroutine`) are
:func:`types.coroutine`) are
*awaitables*, even though they do not have an :meth:`__await__` method.
Using ``isinstance(gencoro, Awaitable)`` for them will return ``False``.
Use :func:`inspect.isawaitable` to detect them.
Expand All @@ -216,7 +216,7 @@ ABC Inherits from Abstract Methods Mixin

.. note::
In CPython, generator-based coroutines (generators decorated with
:func:`types.coroutine` or :func:`asyncio.coroutine`) are
:func:`types.coroutine`) are
*awaitables*, even though they do not have an :meth:`__await__` method.
Using ``isinstance(gencoro, Coroutine)`` for them will return ``False``.
Use :func:`inspect.isawaitable` to detect them.
Expand Down
2 changes: 1 addition & 1 deletion Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2714,7 +2714,7 @@ are awaitable.
.. note::

The :term:`generator iterator` objects returned from generators
decorated with :func:`types.coroutine` or :func:`asyncio.coroutine`
decorated with :func:`types.coroutine`
are also awaitable, but they do not implement :meth:`__await__`.

.. method:: object.__await__(self)
Expand Down
10 changes: 9 additions & 1 deletion Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,15 @@ Deprecated
Removed
=======


* The :func:`@asyncio.coroutine <asyncio.coroutine>` :term:`decorator` enabling
legacy generator-based coroutines to be compatible with async/await code.
The function has been deprecated since Python 3.8 and the removal was
initially scheduled for Python 3.10. Use :keyword:`async def` instead.
(Contributed by Illia Volochii in :issue:`43216`.)

* :class:`asyncio.coroutines.CoroWrapper` used for wrapping legacy
generator-based coroutine objects in the debug mode.
(Contributed by Illia Volochii in :issue:`43216`.)

Porting to Python 3.11
======================
Expand Down
166 changes: 4 additions & 162 deletions Lib/asyncio/coroutines.py
Original file line number Diff line number Diff line change
@@ -1,162 +1,19 @@
__all__ = 'coroutine', 'iscoroutinefunction', 'iscoroutine'
__all__ = 'iscoroutinefunction', 'iscoroutine'

import collections.abc
import functools
import inspect
import os
import sys
import traceback
import types
import warnings

from . import base_futures
from . import constants
from . import format_helpers
from .log import logger


def _is_debug_mode():
# If you set _DEBUG to true, @coroutine will wrap the resulting
# generator objects in a CoroWrapper instance (defined below). That
# instance will log a message when the generator is never iterated
# over, which may happen when you forget to use "await" or "yield from"
# with a coroutine call.
# Note that the value of the _DEBUG flag is taken
# when the decorator is used, so to be of any use it must be set
# before you define your coroutines. A downside of using this feature
# is that tracebacks show entries for the CoroWrapper.__next__ method
# when _DEBUG is true.
# See: https://docs.python.org/3/library/asyncio-dev.html#asyncio-debug-mode.
return sys.flags.dev_mode or (not sys.flags.ignore_environment and
bool(os.environ.get('PYTHONASYNCIODEBUG')))


_DEBUG = _is_debug_mode()


class CoroWrapper:
# Wrapper for coroutine object in _DEBUG mode.

def __init__(self, gen, func=None):
assert inspect.isgenerator(gen) or inspect.iscoroutine(gen), gen
self.gen = gen
self.func = func # Used to unwrap @coroutine decorator
self._source_traceback = format_helpers.extract_stack(sys._getframe(1))
self.__name__ = getattr(gen, '__name__', None)
self.__qualname__ = getattr(gen, '__qualname__', None)

def __repr__(self):
coro_repr = _format_coroutine(self)
if self._source_traceback:
frame = self._source_traceback[-1]
coro_repr += f', created at {frame[0]}:{frame[1]}'

return f'<{self.__class__.__name__} {coro_repr}>'

def __iter__(self):
return self

def __next__(self):
return self.gen.send(None)

def send(self, value):
return self.gen.send(value)

def throw(self, type, value=None, traceback=None):
return self.gen.throw(type, value, traceback)

def close(self):
return self.gen.close()

@property
def gi_frame(self):
return self.gen.gi_frame

@property
def gi_running(self):
return self.gen.gi_running

@property
def gi_code(self):
return self.gen.gi_code

def __await__(self):
return self

@property
def gi_yieldfrom(self):
return self.gen.gi_yieldfrom

def __del__(self):
# Be careful accessing self.gen.frame -- self.gen might not exist.
gen = getattr(self, 'gen', None)
frame = getattr(gen, 'gi_frame', None)
if frame is not None and frame.f_lasti == -1:
msg = f'{self!r} was never yielded from'
tb = getattr(self, '_source_traceback', ())
if tb:
tb = ''.join(traceback.format_list(tb))
msg += (f'\nCoroutine object created at '
f'(most recent call last, truncated to '
f'{constants.DEBUG_STACK_DEPTH} last lines):\n')
msg += tb.rstrip()
logger.error(msg)


def coroutine(func):
"""Decorator to mark coroutines.
If the coroutine is not yielded from before it is destroyed,
an error message is logged.
"""
warnings.warn('"@coroutine" decorator is deprecated since Python 3.8, use "async def" instead',
DeprecationWarning,
stacklevel=2)
if inspect.iscoroutinefunction(func):
# In Python 3.5 that's all we need to do for coroutines
# defined with "async def".
return func

if inspect.isgeneratorfunction(func):
coro = func
else:
@functools.wraps(func)
def coro(*args, **kw):
res = func(*args, **kw)
if (base_futures.isfuture(res) or inspect.isgenerator(res) or
isinstance(res, CoroWrapper)):
res = yield from res
else:
# If 'res' is an awaitable, run it.
try:
await_meth = res.__await__
except AttributeError:
pass
else:
if isinstance(res, collections.abc.Awaitable):
res = yield from await_meth()
return res

coro = types.coroutine(coro)
if not _DEBUG:
wrapper = coro
else:
@functools.wraps(func)
def wrapper(*args, **kwds):
w = CoroWrapper(coro(*args, **kwds), func=func)
if w._source_traceback:
del w._source_traceback[-1]
# Python < 3.5 does not implement __qualname__
# on generator objects, so we set it manually.
# We use getattr as some callables (such as
# functools.partial may lack __qualname__).
w.__name__ = getattr(func, '__name__', None)
w.__qualname__ = getattr(func, '__qualname__', None)
return w

wrapper._is_coroutine = _is_coroutine # For iscoroutinefunction().
return wrapper


# A marker for iscoroutinefunction.
_is_coroutine = object()

Expand All @@ -170,7 +27,7 @@ def iscoroutinefunction(func):
# Prioritize native coroutine check to speed-up
# asyncio.iscoroutine.
_COROUTINE_TYPES = (types.CoroutineType, types.GeneratorType,
collections.abc.Coroutine, CoroWrapper)
collections.abc.Coroutine)
_iscoroutine_typecache = set()


Expand All @@ -193,16 +50,11 @@ def iscoroutine(obj):
def _format_coroutine(coro):
assert iscoroutine(coro)

is_corowrapper = isinstance(coro, CoroWrapper)

def get_name(coro):
# Coroutines compiled with Cython sometimes don't have
# proper __qualname__ or __name__. While that is a bug
# in Cython, asyncio shouldn't crash with an AttributeError
# in its __repr__ functions.
if is_corowrapper:
return format_helpers._format_callback(coro.func, (), {})

if hasattr(coro, '__qualname__') and coro.__qualname__:
coro_name = coro.__qualname__
elif hasattr(coro, '__name__') and coro.__name__:
Expand Down Expand Up @@ -247,18 +99,8 @@ def is_running(coro):
filename = coro_code.co_filename or '<empty co_filename>'

lineno = 0
if (is_corowrapper and
coro.func is not None and
not inspect.isgeneratorfunction(coro.func)):
source = format_helpers._get_function_source(coro.func)
if source is not None:
filename, lineno = source
if coro_frame is None:
coro_repr = f'{coro_name} done, defined at {filename}:{lineno}'
else:
coro_repr = f'{coro_name} running, defined at {filename}:{lineno}'

elif coro_frame is not None:
if coro_frame is not None:
lineno = coro_frame.f_lineno
coro_repr = f'{coro_name} running at {filename}:{lineno}'

Expand Down
6 changes: 2 additions & 4 deletions Lib/test/test_asyncio/test_base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1884,10 +1884,8 @@ def test_accept_connection_exception(self, m_log):
MyProto, sock, None, None, mock.ANY, mock.ANY)

def test_call_coroutine(self):
with self.assertWarns(DeprecationWarning):
@asyncio.coroutine
def simple_coroutine():
pass
async def simple_coroutine():
pass

self.loop.set_debug(True)
coro_func = simple_coroutine
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_asyncio/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import sys
import threading
import time
import types
import errno
import unittest
from unittest import mock
Expand Down Expand Up @@ -2163,8 +2164,7 @@ def test_handle_repr(self):
'<Handle cancelled>')

# decorated function
with self.assertWarns(DeprecationWarning):
cb = asyncio.coroutine(noop)
cb = types.coroutine(noop)
h = asyncio.Handle(cb, (), self.loop)
self.assertEqual(repr(h),
'<Handle noop() at %s:%s>'
Expand Down
Loading

0 comments on commit a1092f6

Please sign in to comment.