From 1a393123fe761a5d799a541736ced30ff4489ee5 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 17 Jul 2022 12:43:30 +0100 Subject: [PATCH 1/9] support inspect.iscoroutinefunction in create_autospec(async_def) --- Lib/unittest/mock.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index cd46fea5162aba..8ee56cd7ad3cd3 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -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): + _checksig_(*args, **kwargs) + 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 @@ -2681,9 +2708,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) From 56c6ed4a8f747322588cfe26f4aa500073977022 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 17 Jul 2022 12:00:00 +0100 Subject: [PATCH 2/9] alias asyncio.iscoroutinefunction to inspect.iscoroutinefunction --- Lib/asyncio/coroutines.py | 9 ++------- Lib/asyncio/tasks.py | 13 +++---------- Lib/unittest/mock.py | 8 -------- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/Lib/asyncio/coroutines.py b/Lib/asyncio/coroutines.py index 7fda0e449d500a..6ea7826f45696e 100644 --- a/Lib/asyncio/coroutines.py +++ b/Lib/asyncio/coroutines.py @@ -13,14 +13,9 @@ def _is_debug_mode(): bool(os.environ.get('PYTHONASYNCIODEBUG'))) -# A marker for iscoroutinefunction. -_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) + """Alias for inspect.iscoroutinefunction.""" + return inspect.iscoroutinefunction(func) # Prioritize native coroutine check to speed-up diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 3952b5f2a7743d..ec410753ce7d79 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -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 @@ -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') @@ -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 class _GatheringFuture(futures.Future): diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 8ee56cd7ad3cd3..d7da91441bcb99 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -275,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() @@ -2189,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() From 2b837c8166483992f0b1a23a1742acafb66632ac Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 17 Jul 2022 11:06:29 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2022-07-17-11-06-27.gh-issue-94912.jc4y1V.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-07-17-11-06-27.gh-issue-94912.jc4y1V.rst diff --git a/Misc/NEWS.d/next/Library/2022-07-17-11-06-27.gh-issue-94912.jc4y1V.rst b/Misc/NEWS.d/next/Library/2022-07-17-11-06-27.gh-issue-94912.jc4y1V.rst new file mode 100644 index 00000000000000..3e844471f07578 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-07-17-11-06-27.gh-issue-94912.jc4y1V.rst @@ -0,0 +1 @@ +make ``asyncio.iscoroutinefunction`` a deprecated alias of ``inspect.iscoroutinefunction`` and remove ``asyncio.coroutines._is_coroutine`` From aedef3f168bc6633a57646a1b1ba3d38aecd2eb9 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 17 Jul 2022 13:02:10 +0100 Subject: [PATCH 4/9] test that virtual awaitables are no longer supported in asyncio.create_future --- Lib/test/test_asyncio/test_tasks.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index bde4defdf0129e..38a2ff4d7a80ed 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -1,6 +1,6 @@ """Tests for tasks.py.""" -import collections +import collections.abc import contextvars import gc import io @@ -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') From a4c07679f2e67f26611bd18b1c7f8b2f0b7702e4 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 17 Jul 2022 15:14:55 +0100 Subject: [PATCH 5/9] deprecate asyncio.iscoroutinefunction in favour of inspect.iscoroutinefunction --- Lib/asyncio/coroutines.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/asyncio/coroutines.py b/Lib/asyncio/coroutines.py index 6ea7826f45696e..c0a9cceceda00e 100644 --- a/Lib/asyncio/coroutines.py +++ b/Lib/asyncio/coroutines.py @@ -5,6 +5,7 @@ import os import sys import types +import warnings def _is_debug_mode(): @@ -15,6 +16,7 @@ def _is_debug_mode(): def iscoroutinefunction(func): """Alias for inspect.iscoroutinefunction.""" + warnings._deprecated("asyncio.iscoroutinefunction", remove=(3, 14)) return inspect.iscoroutinefunction(func) From 5402f43ce9fa34e3382ec1d022c23aa9accd9eaa Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 17 Jul 2022 15:27:54 +0100 Subject: [PATCH 6/9] avoid internal use of deprecated asyncio.iscoroutinefunction --- Lib/asyncio/base_events.py | 3 ++- Lib/asyncio/unix_events.py | 3 ++- Lib/unittest/mock.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index fa00bf9a2ca090..ab9647da50c1d7 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -18,6 +18,7 @@ import concurrent.futures import functools import heapq +import inspect import itertools import os import socket @@ -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): diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index cf7683fee64621..aa17341ebb875c 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -2,6 +2,7 @@ import errno import io +import inspect import itertools import os import selectors @@ -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) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index d7da91441bcb99..0dbfa874bc76dd 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -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 From dafc2ffe39bc9ee6ad6aa3e235830a79cf2c2da7 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 18 Jul 2022 16:52:04 +0100 Subject: [PATCH 7/9] restore the _is_coroutine marker for Cython --- Lib/asyncio/coroutines.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/asyncio/coroutines.py b/Lib/asyncio/coroutines.py index c0a9cceceda00e..4d324e8aaf1694 100644 --- a/Lib/asyncio/coroutines.py +++ b/Lib/asyncio/coroutines.py @@ -13,11 +13,16 @@ def _is_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): - """Alias for inspect.iscoroutinefunction.""" + """Return True if func is a decorated coroutine function.""" warnings._deprecated("asyncio.iscoroutinefunction", remove=(3, 14)) - return inspect.iscoroutinefunction(func) + return (inspect.iscoroutinefunction(func) or + getattr(func, '_is_coroutine', None) is _is_coroutine) # Prioritize native coroutine check to speed-up From f839f14054c0d82fd4e7d7bc3459c3c7779f87e7 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 18 Jul 2022 16:53:51 +0100 Subject: [PATCH 8/9] remove doc reference to asyncio.iscoroutinefunction --- Doc/library/unittest.mock.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index b768557e6075f6..6c96999be53256 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -863,7 +863,7 @@ object:: call is an awaitable. >>> mock = AsyncMock() - >>> asyncio.iscoroutinefunction(mock) + >>> inspect.iscoroutinefunction(mock) True >>> inspect.isawaitable(mock()) # doctest: +SKIP True From e9d2eb3aa92f15fe7ad603acdcb0d40e33790f02 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 19 Jul 2022 09:04:19 +0100 Subject: [PATCH 9/9] only warn when 'asyncio.coroutines._is_coroutine' flag is used --- Lib/asyncio/coroutines.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/coroutines.py b/Lib/asyncio/coroutines.py index 4d324e8aaf1694..b337df7d77ab94 100644 --- a/Lib/asyncio/coroutines.py +++ b/Lib/asyncio/coroutines.py @@ -20,9 +20,14 @@ def _is_debug_mode(): def iscoroutinefunction(func): """Return True if func is a decorated coroutine function.""" - warnings._deprecated("asyncio.iscoroutinefunction", remove=(3, 14)) - 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