From 4c3f70e918a0e08a27f49709488928464e4a465f Mon Sep 17 00:00:00 2001 From: Matthew Suozzo Date: Sun, 11 Apr 2021 13:18:59 -0400 Subject: [PATCH 1/4] Make create_autospec generate AsyncMocks for awaitable classes. --- Lib/unittest/mock.py | 25 +++++++++++-------------- Lib/unittest/test/testmock/testasync.py | 6 ++++-- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index c6067151de14fee..fc2835641f350cd 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -56,13 +56,6 @@ def _is_async_obj(obj): return iscoroutinefunction(obj) or inspect.isawaitable(obj) -def _is_async_func(func): - if getattr(func, '__code__', None): - return iscoroutinefunction(func) - else: - return False - - def _is_instance_mock(obj): # can't use isinstance on Mock objects because they override __class__ # The base class for all mocks is NonCallableMock @@ -2647,7 +2640,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, if _is_instance_mock(spec): raise InvalidSpecError(f'Cannot autospec a Mock object. ' f'[object={spec!r}]') - is_async_func = _is_async_func(spec) + is_async = _is_async_obj(spec) _kwargs = {'spec': spec} if spec_set: _kwargs = {'spec_set': spec} @@ -2666,15 +2659,18 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # descriptors don't have a spec # because we don't know what type they return _kwargs = {} - elif is_async_func: - if instance: + elif is_async: + if instance and isinstance(spec, FunctionTypes): raise RuntimeError("Instance can not be True when create_autospec " "is mocking an async function") Klass = AsyncMock elif not _callable(spec): Klass = NonCallableMagicMock - elif is_type and instance and not _instance_callable(spec): - Klass = NonCallableMagicMock + elif is_type and instance: + if _is_async_obj(spec.__new__(spec)): + Klass = AsyncMock + elif not _instance_callable(spec): + Klass = NonCallableMagicMock _name = _kwargs.pop('name', _name) @@ -2690,11 +2686,12 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # 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) else: _check_signature(spec, mock, is_type, instance) + if is_async: + _setup_async_mock(mock) + if _parent is not None and not instance: _parent._mock_children[_name] = mock diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index e1866a3492cb508..44fbb73e6f6f3ca 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -199,9 +199,11 @@ def test_create_autospec_instance(self): with self.assertRaises(RuntimeError): create_autospec(async_func, instance=True) - @unittest.skip('Broken test from https://bugs.python.org/issue37251') def test_create_autospec_awaitable_class(self): - self.assertIsInstance(create_autospec(AwaitableClass), AsyncMock) + self.assertIsInstance(create_autospec(AwaitableClass()), AsyncMock) + self.assertIsInstance(create_autospec(AwaitableClass)(), AsyncMock) + self.assertIsInstance(create_autospec(AwaitableClass, instance=True), + AsyncMock) def test_create_autospec(self): spec = create_autospec(async_func_args) From 8eada73c7e0e5fad5333efdec3729a2b31b9d4d9 Mon Sep 17 00:00:00 2001 From: Matthew Suozzo Date: Sun, 11 Apr 2021 22:13:32 -0400 Subject: [PATCH 2/4] Add blurb. --- .../next/Library/2021-04-11-22-13-07.bpo-37251.nE9eLT.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2021-04-11-22-13-07.bpo-37251.nE9eLT.rst diff --git a/Misc/NEWS.d/next/Library/2021-04-11-22-13-07.bpo-37251.nE9eLT.rst b/Misc/NEWS.d/next/Library/2021-04-11-22-13-07.bpo-37251.nE9eLT.rst new file mode 100644 index 000000000000000..03cdb124047db2f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-04-11-22-13-07.bpo-37251.nE9eLT.rst @@ -0,0 +1,3 @@ +Fix :func:`unittest.mock.create_autospec` to properly use +:class:`unittest.mock.AsyncMock` when called with an awaitable class or +object. From 7a047566c547b706cb95b9231ebba1c6838eff63 Mon Sep 17 00:00:00 2001 From: Matthew Suozzo Date: Sun, 11 Apr 2021 22:41:38 -0400 Subject: [PATCH 3/4] Avoid invoking __new__ in the implementation. --- Lib/unittest/mock.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index fc2835641f350cd..598f846c94d32cf 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -24,6 +24,7 @@ import asyncio +import collections import contextlib import io import inspect @@ -31,7 +32,7 @@ import sys import builtins from asyncio import iscoroutinefunction -from types import CodeType, ModuleType, MethodType +from types import CodeType, ModuleType, MethodType, CoroutineType, GeneratorType from unittest.util import safe_repr from functools import wraps, partial @@ -56,6 +57,14 @@ def _is_async_obj(obj): return iscoroutinefunction(obj) or inspect.isawaitable(obj) +def _is_type_awaitable(cls): + """Adaptation of inspect.isawaitable for types.""" + return (issubclass(cls, CoroutineType) or + issubclass(cls, GeneratorType) and + bool(cls.gi_code.co_flags & inspect.CO_ITERABLE_COROUTINE) or + issubclass(cls, collections.abc.Awaitable)) + + def _is_instance_mock(obj): # can't use isinstance on Mock objects because they override __class__ # The base class for all mocks is NonCallableMock @@ -2667,7 +2676,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, elif not _callable(spec): Klass = NonCallableMagicMock elif is_type and instance: - if _is_async_obj(spec.__new__(spec)): + if _is_type_awaitable(spec): Klass = AsyncMock elif not _instance_callable(spec): Klass = NonCallableMagicMock From acf6756a464e6c0b84849fc8f649932f20c17437 Mon Sep 17 00:00:00 2001 From: Matthew Suozzo Date: Mon, 12 Apr 2021 14:22:38 -0400 Subject: [PATCH 4/4] Make AsyncMock awaitable and improve tests. --- Lib/unittest/mock.py | 10 ++++---- Lib/unittest/test/testmock/testasync.py | 31 ++++++++++++++++++------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 598f846c94d32cf..17ebd8bd6a5e07b 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -248,7 +248,7 @@ def reset_mock(): mock._mock_delegate = funcopy -def _setup_async_mock(mock): +def _setup_async_func_mock(mock): mock._is_coroutine = asyncio.coroutines._is_coroutine mock.await_count = 0 mock.await_args = None @@ -2230,6 +2230,9 @@ async def _execute_mock_call(self, /, *args, **kwargs): return self.return_value + def __await__(self): + return self._execute_mock_call().__await__() + def assert_awaited(self): """ Assert that the mock was awaited at least once. @@ -2695,12 +2698,11 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # should only happen at the top level because we don't # recurse for functions mock = _set_signature(mock, spec) + if is_async: + _setup_async_func_mock(mock) else: _check_signature(spec, mock, is_type, instance) - if is_async: - _setup_async_mock(mock) - if _parent is not None and not instance: _parent._mock_children[_name] = mock diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 44fbb73e6f6f3ca..79fca6711504014 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -200,10 +200,25 @@ def test_create_autospec_instance(self): create_autospec(async_func, instance=True) def test_create_autospec_awaitable_class(self): - self.assertIsInstance(create_autospec(AwaitableClass()), AsyncMock) - self.assertIsInstance(create_autospec(AwaitableClass)(), AsyncMock) - self.assertIsInstance(create_autospec(AwaitableClass, instance=True), - AsyncMock) + cases = [ + ('autospec of instance', create_autospec(AwaitableClass())), + ('constructed autospec of type', create_autospec(AwaitableClass)()), + ('instance autospec of type', + create_autospec(AwaitableClass, instance=True)), + ] + for desc, mock_obj in cases: + with self.subTest(f"test async create_autospec output for {desc}"): + self.assertIsInstance(mock_obj, AsyncMock) + async def main(): + await mock_obj + + self.assertEqual(mock_obj.await_count, 0) + mock_obj.assert_not_awaited() + + run(main()) + + self.assertEqual(mock_obj.await_count, 1) + mock_obj.assert_awaited() def test_create_autospec(self): spec = create_autospec(async_func_args) @@ -813,12 +828,10 @@ def test_assert_awaited_but_not_called(self): self.mock.assert_awaited() with self.assertRaises(AssertionError): self.mock.assert_called() - with self.assertRaises(TypeError): - # You cannot await an AsyncMock, it must be a coroutine - run(self._await_coroutine(self.mock)) - with self.assertRaises(AssertionError): - self.mock.assert_awaited() + run(self._await_coroutine(self.mock)) + + self.mock.assert_awaited() with self.assertRaises(AssertionError): self.mock.assert_called()