Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-94912: deprecate asyncio.iscoroutinefunction when it behaves differently to inspect.iscoroutinefunction #94923

Closed
wants to merge 10 commits into from
Closed
2 changes: 1 addition & 1 deletion Doc/library/unittest.mock.rst
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,7 @@ object::
call is an awaitable.

>>> mock = AsyncMock()
>>> asyncio.iscoroutinefunction(mock)
>>> inspect.iscoroutinefunction(mock)
True
>>> inspect.isawaitable(mock()) # doctest: +SKIP
True
Expand Down
3 changes: 2 additions & 1 deletion Lib/asyncio/base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import concurrent.futures
import functools
import heapq
import inspect
import itertools
import os
import socket
Expand Down Expand Up @@ -766,7 +767,7 @@ def call_soon(self, callback, *args, context=None):

def _check_callback(self, callback, method):
if (coroutines.iscoroutine(callback) or
coroutines.iscoroutinefunction(callback)):
inspect.iscoroutinefunction(callback)):
raise TypeError(
f"coroutines cannot be used with {method}()")
if not callable(callback):
Expand Down
13 changes: 10 additions & 3 deletions Lib/asyncio/coroutines.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@
import os
import sys
import types
import warnings


def _is_debug_mode():
# 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')))


# A marker for iscoroutinefunction.
# slated for removal in 3.14 see https://github.com/python/cpython/pull/94923/
_is_coroutine = object()


def iscoroutinefunction(func):
"""Return True if func is a decorated coroutine function."""
return (inspect.iscoroutinefunction(func) or
getattr(func, '_is_coroutine', None) is _is_coroutine)
if inspect.iscoroutinefunction(func):
return True

if getattr(func, '_is_coroutine', None) is _is_coroutine:
warnings._deprecated("asyncio.coroutines._is_coroutine", remove=(3, 14))
return True

return False


# Prioritize native coroutine check to speed-up
Expand Down
13 changes: 3 additions & 10 deletions Lib/asyncio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from . import events
from . import exceptions
from . import futures
from .coroutines import _is_coroutine

# Helper to generate new task names
# This uses itertools.count() instead of a "+= 1" operation because the latter
Expand Down Expand Up @@ -664,11 +663,9 @@ def _ensure_future(coro_or_future, *, loop=None):
raise ValueError('The future belongs to a different loop than '
'the one specified as the loop argument')
return coro_or_future
called_wrap_awaitable = False
if not coroutines.iscoroutine(coro_or_future):
if inspect.isawaitable(coro_or_future):
coro_or_future = _wrap_awaitable(coro_or_future)
called_wrap_awaitable = True
else:
raise TypeError('An asyncio.Future, a coroutine or an awaitable '
'is required')
Expand All @@ -678,21 +675,17 @@ def _ensure_future(coro_or_future, *, loop=None):
try:
return loop.create_task(coro_or_future)
except RuntimeError:
if not called_wrap_awaitable:
coro_or_future.close()
coro_or_future.close()
raise


@types.coroutine
def _wrap_awaitable(awaitable):
async def _wrap_awaitable(awaitable):
"""Helper for asyncio.ensure_future().

Wraps awaitable (an object with __await__) into a coroutine
that will later be wrapped in a Task by ensure_future().
"""
return (yield from awaitable.__await__())

_wrap_awaitable._is_coroutine = _is_coroutine
return await awaitable
Copy link
Contributor Author

@graingert graingert Jul 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will insist on objects being real awaitables rather than virtual awaitables - eg:

import asyncio.tasks
import collections.abc
import unittest.mock


@collections.abc.Awaitable.register
class sleep_0:
    def __init__(self):
        self.__await__ = lambda: iter((None, ))


async def _wrap_awaitable(awaitable):
    return await awaitable


async def amain():
    await asyncio.ensure_future(sleep_0())  # this works currently but with the new code:
    with unittest.mock.patch("asyncio.tasks._wrap_awaitable", new=_wrap_awaitable):
        await asyncio.ensure_future(sleep_0())  # it will break
asyncio.run(amain())



class _GatheringFuture(futures.Future):
Expand Down
3 changes: 2 additions & 1 deletion Lib/asyncio/unix_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import errno
import io
import inspect
import itertools
import os
import selectors
Expand Down Expand Up @@ -92,7 +93,7 @@ def add_signal_handler(self, sig, callback, *args):
Raise RuntimeError if there is a problem setting up the handler.
"""
if (coroutines.iscoroutine(callback) or
coroutines.iscoroutinefunction(callback)):
inspect.iscoroutinefunction(callback)):
raise TypeError("coroutines cannot be used "
"with add_signal_handler()")
self._check_signal(sig)
Expand Down
17 changes: 16 additions & 1 deletion Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Tests for tasks.py."""

import collections
import collections.abc
import contextvars
import gc
import io
Expand Down Expand Up @@ -286,6 +286,21 @@ async def coro():
loop.run_until_complete(fut)
self.assertEqual(fut.result(), 'ok')

def test_ensure_future_virtual_awaitable(self):
@collections.abc.Awaitable.register
class Aw:
def __init__(self, coro):
self.__await__ = coro.__await__

async def coro():
return 'ok'

loop = asyncio.new_event_loop()
self.set_event_loop(loop)
fut = asyncio.ensure_future(Aw(coro()), loop=loop)
with self.assertRaises(TypeError):
loop.run_until_complete(fut)

def test_ensure_future_neither(self):
with self.assertRaises(TypeError):
asyncio.ensure_future('ok')
Expand Down
42 changes: 31 additions & 11 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import sys
import builtins
import pkgutil
from asyncio import iscoroutinefunction
from inspect import iscoroutinefunction
from types import CodeType, ModuleType, MethodType
from unittest.util import safe_repr
from functools import wraps, partial
Expand Down Expand Up @@ -197,6 +197,33 @@ def checksig(*args, **kwargs):
_setup_func(funcopy, mock, sig)
return funcopy

def _set_async_signature(mock, original, instance=False, is_async_mock=False):
# creates an async function with signature (*args, **kwargs) that delegates to a
# mock. It still does signature checking by calling a lambda with the same
# signature as the original.

skipfirst = isinstance(original, type)
result = _get_signature_object(original, instance, skipfirst)
if result is None:
return mock
func, sig = result
def checksig(*args, **kwargs):
sig.bind(*args, **kwargs)
_copy_func_details(func, checksig)

name = original.__name__
if not name.isidentifier():
name = 'funcopy'
context = {'_checksig_': checksig, 'mock': mock}
src = """async def %s(*args, **kwargs):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to fix #94924

_checksig_(*args, **kwargs)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another very subtle change - before the signature was checked when the function was called - and now the signature is checked when the coro is primed, but I think this is a better behavior and worth it to allow inspect.iscoroutinefunction to work

return await mock(*args, **kwargs)""" % name
exec (src, context)
funcopy = context[name]
_setup_func(funcopy, mock, sig)
_setup_async_mock(funcopy)
return funcopy


def _setup_func(funcopy, mock, sig):
funcopy.mock = mock
Expand Down Expand Up @@ -248,7 +275,6 @@ def reset_mock():


def _setup_async_mock(mock):
mock._is_coroutine = asyncio.coroutines._is_coroutine
mock.await_count = 0
mock.await_args = None
mock.await_args_list = _CallList()
Expand Down Expand Up @@ -2162,13 +2188,6 @@ class AsyncMockMixin(Base):

def __init__(self, /, *args, **kwargs):
super().__init__(*args, **kwargs)
# iscoroutinefunction() checks _is_coroutine property to say if an
# object is a coroutine. Without this check it looks to see if it is a
# function/method, which in this case it is not (since it is an
# AsyncMock).
# It is set through __dict__ because when spec_set is True, this
# attribute is likely undefined.
self.__dict__['_is_coroutine'] = asyncio.coroutines._is_coroutine
self.__dict__['_mock_await_count'] = 0
self.__dict__['_mock_await_args'] = None
self.__dict__['_mock_await_args_list'] = _CallList()
Expand Down Expand Up @@ -2681,9 +2700,10 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
if isinstance(spec, FunctionTypes):
# should only happen at the top level because we don't
# recurse for functions
mock = _set_signature(mock, spec)
if is_async_func:
_setup_async_mock(mock)
mock = _set_async_signature(mock, spec)
else:
mock = _set_signature(mock, spec)
else:
_check_signature(spec, mock, is_type, instance)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
make ``asyncio.iscoroutinefunction`` a deprecated alias of ``inspect.iscoroutinefunction`` and remove ``asyncio.coroutines._is_coroutine``