From 7fbd8faf0211e4a761c0d68a10cd13efac21703b Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Tue, 10 Sep 2019 15:33:44 +0100 Subject: [PATCH 01/14] Adds references to AsyncMock in more places. --- Doc/library/unittest.mock.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index 04ff8a61da3c56..9f90b2ff0c8a83 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -215,13 +215,15 @@ return the same mock. Mocks record how you use them, allowing you to make assertions about what your code has done to them. :class:`MagicMock` is a subclass of :class:`Mock` with all the magic methods -pre-created and ready to use. There are also non-callable variants, useful +pre-created and ready to use. :class:`AsyncMock` is a subclass of :class:`Mock` that returns +an asynchronous version of :class:`Mock`. There are also non-callable variants, useful when you are mocking out objects that aren't callable: :class:`NonCallableMock` and :class:`NonCallableMagicMock` The :func:`patch` decorators makes it easy to temporarily replace classes in a particular module with a :class:`Mock` object. By default :func:`patch` will create -a :class:`MagicMock` for you. You can specify an alternative class of :class:`Mock` using +a :class:`MagicMock` for you unless the patched object is an async func, in which case +an :class:`AsyncMock` is returned. You can specify an alternative class of :class:`Mock` using the *new_callable* argument to :func:`patch`. From 8ef74fec06fed628cde94cc35ba3794ea132f953 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 11 Sep 2019 14:33:05 +0100 Subject: [PATCH 02/14] bpo-38093: Correctly returns AsyncMock for async subclasses. --- Lib/unittest/mock.py | 8 ++-- Lib/unittest/test/testmock/testasync.py | 49 +++++++++++++++++++++---- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index c26b367a4dc290..27bd52f83e960c 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -989,9 +989,9 @@ def _get_child_mock(self, /, **kw): _type = type(self) if issubclass(_type, MagicMock) and _new_name in _async_method_magics: klass = AsyncMock - if issubclass(_type, AsyncMockMixin): - klass = MagicMock - if not issubclass(_type, CallableMixin): + elif issubclass(_type, AsyncMockMixin): + klass = AsyncMock + elif not issubclass(_type, CallableMixin): if issubclass(_type, NonCallableMagicMock): klass = MagicMock elif issubclass(_type, NonCallableMock) : @@ -1904,6 +1904,8 @@ def method(self, /, *args, **kw): '__str__': lambda self: object.__str__(self), '__sizeof__': lambda self: object.__sizeof__(self), '__fspath__': lambda self: f"{type(self).__name__}/{self._extract_mock_name()}/{id(self)}", + '__aenter__': lambda self: AsyncMockMixin._mock_call(self), + '__aexit__': lambda self: AsyncMockMixin._mock_call(self) } _return_values = { diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 865cfdc0026465..7641271fafc7c9 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -363,22 +363,58 @@ def test_add_side_effect_iterable(self): class AsyncContextManagerTest(unittest.TestCase): class WithAsyncContextManager: - async def __aenter__(self, *args, **kwargs): return self async def __aexit__(self, *args, **kwargs): pass - def test_magic_methods_are_async_mocks(self): - mock = MagicMock(self.WithAsyncContextManager()) - self.assertIsInstance(mock.__aenter__, AsyncMock) - self.assertIsInstance(mock.__aexit__, AsyncMock) + class WithSyncContextManager: + def __enter__(self, *args, **kwargs): + return self + + def __exit__(self, *args, **kwargs): + pass + + class ProductionCode: + # Example real-world(ish) code + async def main(self): + async with self.session.post('https://python.org') as response: + val = await response.json() + return val + + def test_async_magic_methods_are_async_mocks_with_magicmock(self): + cm_mock = MagicMock(self.WithAsyncContextManager) + self.assertIsInstance(cm_mock.__aenter__, AsyncMock) + self.assertIsInstance(cm_mock.__aexit__, AsyncMock) + + def test_set_return_value_of_aenter_magicmock(self): + pc = self.ProductionCode() + pc.session = MagicMock(name='sessionmock') + cm = MagicMock(name='magic_cm') + response = AsyncMock(name='response') + response.json = AsyncMock(return_value={'json':123}) + cm.__aenter__.return_value = response + pc.session.post.return_value = cm + result = asyncio.run(pc.main()) + self.assertEqual(result, {'json':123}) + + def test_set_return_value_of_aenter_asyncmock(self): + pc = self.ProductionCode() + pc.session = MagicMock(name='sessionmock') + cm = AsyncMock(name='async_cm') + response = AsyncMock(name='response') + response.json = AsyncMock(return_value={'json':123}) + cm.__aenter__.return_value = response + pc.session.post.return_value = cm + result = asyncio.run(pc.main()) + self.assertEqual(result, {'json':123}) def test_mock_supports_async_context_manager(self): called = False instance = self.WithAsyncContextManager() - mock_instance = MagicMock(instance) + # TODO: Horrible error message if you use a MagicMock here + mock_instance = AsyncMock(instance) async def use_context_manager(): nonlocal called @@ -391,7 +427,6 @@ async def use_context_manager(): self.assertTrue(mock_instance.__aenter__.called) self.assertTrue(mock_instance.__aexit__.called) self.assertIsNot(mock_instance, result) - self.assertIsInstance(result, AsyncMock) def test_mock_customize_async_context_manager(self): instance = self.WithAsyncContextManager() From d3f2706914936ad6b48cfa9e41ffa11b79ceb6c1 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 11 Sep 2019 14:35:06 +0100 Subject: [PATCH 03/14] Revert "Adds references to AsyncMock in more places." for a different PR. This reverts commit 7fbd8faf0211e4a761c0d68a10cd13efac21703b. --- Doc/library/unittest.mock.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index 9f90b2ff0c8a83..04ff8a61da3c56 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -215,15 +215,13 @@ return the same mock. Mocks record how you use them, allowing you to make assertions about what your code has done to them. :class:`MagicMock` is a subclass of :class:`Mock` with all the magic methods -pre-created and ready to use. :class:`AsyncMock` is a subclass of :class:`Mock` that returns -an asynchronous version of :class:`Mock`. There are also non-callable variants, useful +pre-created and ready to use. There are also non-callable variants, useful when you are mocking out objects that aren't callable: :class:`NonCallableMock` and :class:`NonCallableMagicMock` The :func:`patch` decorators makes it easy to temporarily replace classes in a particular module with a :class:`Mock` object. By default :func:`patch` will create -a :class:`MagicMock` for you unless the patched object is an async func, in which case -an :class:`AsyncMock` is returned. You can specify an alternative class of :class:`Mock` using +a :class:`MagicMock` for you. You can specify an alternative class of :class:`Mock` using the *new_callable* argument to :func:`patch`. From bd02a3a0ef01002b646fe4ceb3e471264277e9e0 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 11 Sep 2019 14:45:46 +0100 Subject: [PATCH 04/14] Adds news entry Blurb. --- .../next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst diff --git a/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst b/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst new file mode 100644 index 00000000000000..30c15c971c0d87 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst @@ -0,0 +1,3 @@ +Fixes AsyncMock so MagicMocks on AsyncContextManagers do not have to +manually set `__aenter__` and `__aexit__`. Also fixes AsyncMock so they +don't crash when used with AsyncContextManager. From d656592c697ea026048d96c093a43397f9ca70dd Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 11 Sep 2019 15:41:49 +0100 Subject: [PATCH 05/14] Removes async magics from _calculage_return_values. --- Lib/unittest/mock.py | 2 -- Lib/unittest/test/testmock/testasync.py | 9 +++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 27bd52f83e960c..35d38770b06cc9 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1904,8 +1904,6 @@ def method(self, /, *args, **kw): '__str__': lambda self: object.__str__(self), '__sizeof__': lambda self: object.__sizeof__(self), '__fspath__': lambda self: f"{type(self).__name__}/{self._extract_mock_name()}/{id(self)}", - '__aenter__': lambda self: AsyncMockMixin._mock_call(self), - '__aexit__': lambda self: AsyncMockMixin._mock_call(self) } _return_values = { diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 7641271fafc7c9..6a16ba63a7e869 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -378,6 +378,9 @@ def __exit__(self, *args, **kwargs): class ProductionCode: # Example real-world(ish) code + def __init__(self): + self.session = None + async def main(self): async with self.session.post('https://python.org') as response: val = await response.json() @@ -388,6 +391,12 @@ def test_async_magic_methods_are_async_mocks_with_magicmock(self): self.assertIsInstance(cm_mock.__aenter__, AsyncMock) self.assertIsInstance(cm_mock.__aexit__, AsyncMock) + def test_magicmock_has_async_magic_methods(self): + cm = MagicMock(name='magic_cm') + self.assertTrue(hasattr(cm, "__aenter__")) + self.assertTrue(hasattr(cm, "__aexit__")) + self.assertTrue(asyncio.iscoroutinefunction(cm.__aexit__)) + def test_set_return_value_of_aenter_magicmock(self): pc = self.ProductionCode() pc.session = MagicMock(name='sessionmock') From b831e584230f1e0bb75011eb5691f351c1ef795f Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 11 Sep 2019 17:21:19 +0100 Subject: [PATCH 06/14] Fixes aiter, warnings, and example docs. --- Doc/library/unittest.mock-examples.rst | 40 ++++++++++++++++++++++++++ Lib/unittest/mock.py | 13 ++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/Doc/library/unittest.mock-examples.rst b/Doc/library/unittest.mock-examples.rst index 811f0fb1ce9397..34039290b1623d 100644 --- a/Doc/library/unittest.mock-examples.rst +++ b/Doc/library/unittest.mock-examples.rst @@ -12,6 +12,7 @@ .. testsetup:: + import asyncio import unittest from unittest.mock import Mock, MagicMock, patch, call, sentinel @@ -276,6 +277,45 @@ function returns is what the call returns: 2 +Mocking asynchronous iterators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since Python 3.8, ``AsyncMock`` has support to mock :ref:`async-iterators` +through ``__aiter__``. The :attr:`~Mock.return_value` attribute of ``__aiter__`` +can be used to set the return values to be used for iteration. + + >>> mock = AsyncMock() + >>> mock.__aiter__.return_value = [1, 2, 3] + >>> async def main(): + ... return [i async for i in mock] + ... + >>> asyncio.run(main()) + [1, 2, 3] + + +Mocking asynchronous context manager +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since Python 3.8, ``AsyncMock`` has support to mock +:ref:`async-context-managers` through ``__aenter__`` and ``__aexit__``. The +return value of ``__aenter__`` is an async function. + + >>> class AsyncContextManager: + ... async def __aenter__(self): + ... return self + ... async def __aexit__(self, exc_type, exc, tb): + ... pass + ... + >>> mock_instance = AsyncMock(AsyncContextManager()) + >>> async def main(): + ... async with mock_instance as result: + ... pass + ... + >>> asyncio.run(main()) + >>> mock_instance.__aenter__.assert_called_once() + >>> mock_instance.__aexit__.assert_called_once() + + Creating a Mock from an Existing Object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 35d38770b06cc9..f12e415d5dbdd0 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -989,6 +989,9 @@ def _get_child_mock(self, /, **kw): _type = type(self) if issubclass(_type, MagicMock) and _new_name in _async_method_magics: klass = AsyncMock + elif _new_name in _sync_async_magics: + # Special case these ones b/c users will assume they are async, but they are actually sync + klass = MagicMock elif issubclass(_type, AsyncMockMixin): klass = AsyncMock elif not issubclass(_type, CallableMixin): @@ -1868,7 +1871,7 @@ def _patch_stopall(): '__reduce__', '__reduce_ex__', '__getinitargs__', '__getnewargs__', '__getstate__', '__setstate__', '__getformat__', '__setformat__', '__repr__', '__dir__', '__subclasses__', '__format__', - '__getnewargs_ex__', '__aenter__', '__aexit__', '__anext__', '__aiter__', + '__getnewargs_ex__', } @@ -1887,10 +1890,12 @@ def method(self, /, *args, **kw): # Magic methods used for async `with` statements _async_method_magics = {"__aenter__", "__aexit__", "__anext__"} -# `__aiter__` is a plain function but used with async calls -_async_magics = _async_method_magics | {"__aiter__"} +# Magic methods that are only used with async calls but are synchronous functions themselves +_sync_async_magics = {"__aiter__"} +_async_magics = _async_method_magics | _sync_async_magics -_all_magics = _magics | _non_defaults +_all_sync_magics = _magics | _non_defaults +_all_magics = _all_sync_magics | _async_magics _unsupported_magics = { '__getattr__', '__setattr__', From b4e164c9785bc2ce0c6d8c29e5a35325122ae1a5 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 11 Sep 2019 17:35:05 +0100 Subject: [PATCH 07/14] Adds more tests for aiter using AsyncMock. --- Lib/unittest/test/testmock/testasync.py | 53 +++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 5fd7412146620d..c768bf5eb329be 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -518,7 +518,38 @@ async def __anext__(self): raise StopAsyncIteration - def test_mock_aiter_and_anext(self): + def test_aiter_set_return_value(self): + mock_iter = AsyncMock(name="tester") + mock_iter.__aiter__.return_value = [1, 2, 3] + async def main(): + async for i in mock_iter: + return [i async for i in mock_iter] + result = asyncio.run(main()) + self.assertEqual(result, [1, 2, 3]) + + def test_mock_aiter_and_anext_asyncmock(self): + instance = self.WithAsyncIterator() + mock_instance = AsyncMock(instance) + + self.assertEqual(asyncio.iscoroutine(instance.__aiter__), + asyncio.iscoroutine(mock_instance.__aiter__)) + self.assertEqual(asyncio.iscoroutine(instance.__anext__), + asyncio.iscoroutine(mock_instance.__anext__)) + + iterator = instance.__aiter__() + if asyncio.iscoroutine(iterator): + iterator = asyncio.run(iterator) + + mock_iterator = mock_instance.__aiter__() + if asyncio.iscoroutine(mock_iterator): + mock_iterator = asyncio.run(mock_iterator) + + self.assertEqual(asyncio.iscoroutine(iterator.__aiter__), + asyncio.iscoroutine(mock_iterator.__aiter__)) + self.assertEqual(asyncio.iscoroutine(iterator.__anext__), + asyncio.iscoroutine(mock_iterator.__anext__)) + + def test_mock_aiter_and_anext_magicmock(self): instance = self.WithAsyncIterator() mock_instance = MagicMock(instance) @@ -549,20 +580,34 @@ async def iterate(iterator): return accumulator expected = ["FOO", "BAR", "BAZ"] - with self.subTest("iterate through default value"): + with self.subTest("iterate through default value with magicmock"): mock_instance = MagicMock(self.WithAsyncIterator()) self.assertEqual([], asyncio.run(iterate(mock_instance))) - with self.subTest("iterate through set return_value"): + with self.subTest("iterate through default value using asyncmock"): + mock_instance = AsyncMock(self.WithAsyncIterator()) + self.assertEqual([], asyncio.run(iterate(mock_instance))) + + with self.subTest("iterate through set return_value with magicmock"): mock_instance = MagicMock(self.WithAsyncIterator()) mock_instance.__aiter__.return_value = expected[:] self.assertEqual(expected, asyncio.run(iterate(mock_instance))) - with self.subTest("iterate through set return_value iterator"): + with self.subTest("iterate through set return_value with asyncmock"): + mock_instance = AsyncMock(self.WithAsyncIterator()) + mock_instance.__aiter__.return_value = expected[:] + self.assertEqual(expected, asyncio.run(iterate(mock_instance))) + + with self.subTest("iterate through set return_value iterator with magicmock"): mock_instance = MagicMock(self.WithAsyncIterator()) mock_instance.__aiter__.return_value = iter(expected[:]) self.assertEqual(expected, asyncio.run(iterate(mock_instance))) + with self.subTest("iterate through set return_value iterator with asyncmock"): + mock_instance = AsyncMock(self.WithAsyncIterator()) + mock_instance.__aiter__.return_value = iter(expected[:]) + self.assertEqual(expected, asyncio.run(iterate(mock_instance))) + class AsyncMockAssert(unittest.TestCase): def setUp(self): From 14e684e65aec09950eb2a70ed8645ca8e656ca55 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 11 Sep 2019 17:37:35 +0100 Subject: [PATCH 08/14] Updates news to be more concise. --- .../next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst b/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst index 30c15c971c0d87..3b6c0de83f77a9 100644 --- a/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst +++ b/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst @@ -1,3 +1,2 @@ -Fixes AsyncMock so MagicMocks on AsyncContextManagers do not have to -manually set `__aenter__` and `__aexit__`. Also fixes AsyncMock so they -don't crash when used with AsyncContextManager. +Fixes AsyncMock so it doesn't crash when used with AsynContextManagers +or AsyncIterators. From f476b5845a20c7be2c6357c1837a1c8b7b64562f Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 11 Sep 2019 18:28:23 +0100 Subject: [PATCH 09/14] Imports AsyncMock into mock examples. --- Doc/library/unittest.mock-examples.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/unittest.mock-examples.rst b/Doc/library/unittest.mock-examples.rst index 34039290b1623d..f81790dd020025 100644 --- a/Doc/library/unittest.mock-examples.rst +++ b/Doc/library/unittest.mock-examples.rst @@ -14,7 +14,7 @@ import asyncio import unittest - from unittest.mock import Mock, MagicMock, patch, call, sentinel + from unittest.mock import AsyncMock, Mock, MagicMock, patch, call, sentinel class SomeClass: attribute = 'this is a doctest' From 0d3af26b5dc2bb39864409af1d7a020263eea685 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Thu, 12 Sep 2019 11:35:29 +0100 Subject: [PATCH 10/14] Updates docs, adds test. --- Doc/library/unittest.mock-examples.rst | 17 +++++++++-------- Lib/unittest/mock.py | 3 ++- Lib/unittest/test/testmock/testasync.py | 7 +++++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Doc/library/unittest.mock-examples.rst b/Doc/library/unittest.mock-examples.rst index f81790dd020025..2c4d6f606f7ffe 100644 --- a/Doc/library/unittest.mock-examples.rst +++ b/Doc/library/unittest.mock-examples.rst @@ -280,11 +280,12 @@ function returns is what the call returns: Mocking asynchronous iterators ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Since Python 3.8, ``AsyncMock`` has support to mock :ref:`async-iterators` -through ``__aiter__``. The :attr:`~Mock.return_value` attribute of ``__aiter__`` -can be used to set the return values to be used for iteration. +Since Python 3.8, ``AsyncMock`` and ``MagicMock`` have support to mock +:ref:`async-iterators` through ``__aiter__``. The :attr:`~Mock.return_value` +attribute of ``__aiter__`` can be used to set the return values to be used for +iteration. - >>> mock = AsyncMock() + >>> mock = MagicMock() # AsyncMock also works here >>> mock.__aiter__.return_value = [1, 2, 3] >>> async def main(): ... return [i async for i in mock] @@ -296,9 +297,9 @@ can be used to set the return values to be used for iteration. Mocking asynchronous context manager ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Since Python 3.8, ``AsyncMock`` has support to mock -:ref:`async-context-managers` through ``__aenter__`` and ``__aexit__``. The -return value of ``__aenter__`` is an async function. +Since Python 3.8, ``AsyncMock`` and ``MagicMock`` have support to mock +:ref:`async-context-managers` through ``__aenter__`` and ``__aexit__``. +By default, ``__aenter__`` is an ``AsyncMock`` that returns an async function. >>> class AsyncContextManager: ... async def __aenter__(self): @@ -306,7 +307,7 @@ return value of ``__aenter__`` is an async function. ... async def __aexit__(self, exc_type, exc, tb): ... pass ... - >>> mock_instance = AsyncMock(AsyncContextManager()) + >>> mock_instance = MagicMock(AsyncContextManager()) # AsyncMock also works here >>> async def main(): ... async with mock_instance as result: ... pass diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 698201a2e449bc..1c20bef6591c35 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -989,7 +989,8 @@ def _get_child_mock(self, /, **kw): if issubclass(_type, MagicMock) and _new_name in _async_method_magics: klass = AsyncMock elif _new_name in _sync_async_magics: - # Special case these ones b/c users will assume they are async, but they are actually sync + # Special case these ones b/c users will assume they are async, + # but they are actually sync klass = MagicMock elif issubclass(_type, AsyncMockMixin): klass = AsyncMock diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index c768bf5eb329be..101dd1b5c9a6d0 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -410,7 +410,14 @@ def test_magicmock_has_async_magic_methods(self): cm = MagicMock(name='magic_cm') self.assertTrue(hasattr(cm, "__aenter__")) self.assertTrue(hasattr(cm, "__aexit__")) + + def test_magic_methods_are_async_functions(self): + cm = MagicMock(name='magic_cm') + self.assertTrue(asyncio.iscoroutinefunction(cm.__aenter__)) self.assertTrue(asyncio.iscoroutinefunction(cm.__aexit__)) + # These should pass but cause warnings to be raised + # self.assertTrue(inspect.isawaitable(cm.__aenter__())) + # self.assertTrue(inspect.isawaitable(cm.__aexit__())) def test_set_return_value_of_aenter_magicmock(self): pc = self.ProductionCode() From 7d9dfa0af411b23abb09531250394f6eb9be2946 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Thu, 12 Sep 2019 14:38:34 +0100 Subject: [PATCH 11/14] Subtests things and fixes with comments. --- Lib/unittest/test/testmock/testasync.py | 163 +++++++----------- .../test/testmock/testmagicmethods.py | 3 + 2 files changed, 69 insertions(+), 97 deletions(-) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 101dd1b5c9a6d0..6b29742bca70bb 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -419,45 +419,44 @@ def test_magic_methods_are_async_functions(self): # self.assertTrue(inspect.isawaitable(cm.__aenter__())) # self.assertTrue(inspect.isawaitable(cm.__aexit__())) - def test_set_return_value_of_aenter_magicmock(self): - pc = self.ProductionCode() - pc.session = MagicMock(name='sessionmock') - cm = MagicMock(name='magic_cm') - response = AsyncMock(name='response') - response.json = AsyncMock(return_value={'json':123}) - cm.__aenter__.return_value = response - pc.session.post.return_value = cm - result = asyncio.run(pc.main()) - self.assertEqual(result, {'json':123}) - - def test_set_return_value_of_aenter_asyncmock(self): - pc = self.ProductionCode() - pc.session = MagicMock(name='sessionmock') - cm = AsyncMock(name='async_cm') - response = AsyncMock(name='response') - response.json = AsyncMock(return_value={'json':123}) - cm.__aenter__.return_value = response - pc.session.post.return_value = cm - result = asyncio.run(pc.main()) - self.assertEqual(result, {'json':123}) + def test_set_return_value_of_aenter(self): + def inner_test(mock_type): + pc = self.ProductionCode() + pc.session = MagicMock(name='sessionmock') + cm = mock_type(name='magic_cm') + response = AsyncMock(name='response') + response.json = AsyncMock(return_value={'json': 123}) + cm.__aenter__.return_value = response + pc.session.post.return_value = cm + result = asyncio.run(pc.main()) + self.assertEqual(result, {'json': 123}) + + for mock_type in [AsyncMock, MagicMock]: + with self.subTest(f"test set return value of aenter with {mock_type}"): + inner_test(mock_type) def test_mock_supports_async_context_manager(self): - called = False - instance = self.WithAsyncContextManager() - # TODO: Horrible error message if you use a MagicMock here - mock_instance = AsyncMock(instance) + def inner_test(mock_type): + called = False + instance = self.WithAsyncContextManager() + mock_instance = mock_type(instance) + + async def use_context_manager(): + nonlocal called + async with mock_instance as result: + called = True + return result - async def use_context_manager(): - nonlocal called - async with mock_instance as result: - called = True - return result + result = asyncio.run(use_context_manager()) + self.assertTrue(called) + self.assertTrue(mock_instance.__aenter__.called) + self.assertTrue(mock_instance.__aexit__.called) + self.assertIsNot(mock_instance, result) + + for mock_type in [AsyncMock, MagicMock]: + with self.subTest(f"test context manager magics with {mock_type}"): + inner_test(mock_type) - result = asyncio.run(use_context_manager()) - self.assertTrue(called) - self.assertTrue(mock_instance.__aenter__.called) - self.assertTrue(mock_instance.__aexit__.called) - self.assertIsNot(mock_instance, result) def test_mock_customize_async_context_manager(self): instance = self.WithAsyncContextManager() @@ -535,48 +534,21 @@ async def main(): self.assertEqual(result, [1, 2, 3]) def test_mock_aiter_and_anext_asyncmock(self): - instance = self.WithAsyncIterator() - mock_instance = AsyncMock(instance) - - self.assertEqual(asyncio.iscoroutine(instance.__aiter__), - asyncio.iscoroutine(mock_instance.__aiter__)) - self.assertEqual(asyncio.iscoroutine(instance.__anext__), - asyncio.iscoroutine(mock_instance.__anext__)) - - iterator = instance.__aiter__() - if asyncio.iscoroutine(iterator): - iterator = asyncio.run(iterator) - - mock_iterator = mock_instance.__aiter__() - if asyncio.iscoroutine(mock_iterator): - mock_iterator = asyncio.run(mock_iterator) - - self.assertEqual(asyncio.iscoroutine(iterator.__aiter__), - asyncio.iscoroutine(mock_iterator.__aiter__)) - self.assertEqual(asyncio.iscoroutine(iterator.__anext__), - asyncio.iscoroutine(mock_iterator.__anext__)) + def inner_test(mock_type): + instance = self.WithAsyncIterator + mock_instance = mock_type(instance) + # Check that the mock and the real thing bahave the same + # __aiter__ is not actually async, so not a coroutinefunction + self.assertFalse(asyncio.iscoroutinefunction(instance.__aiter__)) + self.assertFalse(asyncio.iscoroutinefunction(mock_instance.__aiter__)) + # __anext__ is async + self.assertTrue(asyncio.iscoroutinefunction(instance.__anext__)) + self.assertTrue(asyncio.iscoroutinefunction(mock_instance.__anext__)) + + for mock_type in [AsyncMock, MagicMock]: + with self.subTest(f"test aiter and anext corourtine with {mock_type}"): + inner_test(mock_type) - def test_mock_aiter_and_anext_magicmock(self): - instance = self.WithAsyncIterator() - mock_instance = MagicMock(instance) - - self.assertEqual(asyncio.iscoroutine(instance.__aiter__), - asyncio.iscoroutine(mock_instance.__aiter__)) - self.assertEqual(asyncio.iscoroutine(instance.__anext__), - asyncio.iscoroutine(mock_instance.__anext__)) - - iterator = instance.__aiter__() - if asyncio.iscoroutine(iterator): - iterator = asyncio.run(iterator) - - mock_iterator = mock_instance.__aiter__() - if asyncio.iscoroutine(mock_iterator): - mock_iterator = asyncio.run(mock_iterator) - - self.assertEqual(asyncio.iscoroutine(iterator.__aiter__), - asyncio.iscoroutine(mock_iterator.__aiter__)) - self.assertEqual(asyncio.iscoroutine(iterator.__anext__), - asyncio.iscoroutine(mock_iterator.__anext__)) def test_mock_async_for(self): async def iterate(iterator): @@ -587,33 +559,30 @@ async def iterate(iterator): return accumulator expected = ["FOO", "BAR", "BAZ"] - with self.subTest("iterate through default value with magicmock"): - mock_instance = MagicMock(self.WithAsyncIterator()) - self.assertEqual([], asyncio.run(iterate(mock_instance))) + def test_default(mock_type): + mock_instance = mock_type(self.WithAsyncIterator()) + self.assertEqual(asyncio.run(iterate(mock_instance)), []) - with self.subTest("iterate through default value using asyncmock"): - mock_instance = AsyncMock(self.WithAsyncIterator()) - self.assertEqual([], asyncio.run(iterate(mock_instance))) - with self.subTest("iterate through set return_value with magicmock"): - mock_instance = MagicMock(self.WithAsyncIterator()) + def test_set_return_value(mock_type): + mock_instance = mock_type(self.WithAsyncIterator()) mock_instance.__aiter__.return_value = expected[:] - self.assertEqual(expected, asyncio.run(iterate(mock_instance))) + self.assertEqual(asyncio.run(iterate(mock_instance)), expected) - with self.subTest("iterate through set return_value with asyncmock"): - mock_instance = AsyncMock(self.WithAsyncIterator()) - mock_instance.__aiter__.return_value = expected[:] - self.assertEqual(expected, asyncio.run(iterate(mock_instance))) - - with self.subTest("iterate through set return_value iterator with magicmock"): - mock_instance = MagicMock(self.WithAsyncIterator()) + def test_set_return_value_iter(mock_type): + mock_instance = mock_type(self.WithAsyncIterator()) mock_instance.__aiter__.return_value = iter(expected[:]) - self.assertEqual(expected, asyncio.run(iterate(mock_instance))) + self.assertEqual(asyncio.run(iterate(mock_instance)), expected) - with self.subTest("iterate through set return_value iterator with asyncmock"): - mock_instance = AsyncMock(self.WithAsyncIterator()) - mock_instance.__aiter__.return_value = iter(expected[:]) - self.assertEqual(expected, asyncio.run(iterate(mock_instance))) + for mock_type in [AsyncMock, MagicMock]: + with self.subTest(f"default value with {mock_type}"): + test_default(mock_type) + + with self.subTest(f"set return_value with {mock_type}"): + test_set_return_value(mock_type) + + with self.subTest(f"set return_value iterator with {mock_type}"): + test_set_return_value_iter(mock_type) class AsyncMockAssert(unittest.TestCase): diff --git a/Lib/unittest/test/testmock/testmagicmethods.py b/Lib/unittest/test/testmock/testmagicmethods.py index 130a3397ba0d8c..5bc1c108678bfc 100644 --- a/Lib/unittest/test/testmock/testmagicmethods.py +++ b/Lib/unittest/test/testmock/testmagicmethods.py @@ -1,3 +1,4 @@ +import asyncio import math import unittest import os @@ -286,6 +287,8 @@ def test_magicmock_defaults(self): self.assertEqual(math.trunc(mock), mock.__trunc__()) self.assertEqual(math.floor(mock), mock.__floor__()) self.assertEqual(math.ceil(mock), mock.__ceil__()) + self.assertTrue(asyncio.iscoroutinefunction(mock.__aexit__)) + self.assertTrue(asyncio.iscoroutinefunction(mock.__aenter__)) # in Python 3 oct and hex use __index__ # so these tests are for __index__ in py3k From d671277344318aa191132355c9c4345cc3643853 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Fri, 13 Sep 2019 11:28:49 +0100 Subject: [PATCH 12/14] Updates docs, takes out commented lines in test. --- Doc/library/unittest.mock-examples.rst | 3 ++- Lib/unittest/test/testmock/testasync.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Doc/library/unittest.mock-examples.rst b/Doc/library/unittest.mock-examples.rst index 2c4d6f606f7ffe..a16882f2d51200 100644 --- a/Doc/library/unittest.mock-examples.rst +++ b/Doc/library/unittest.mock-examples.rst @@ -299,7 +299,8 @@ Mocking asynchronous context manager Since Python 3.8, ``AsyncMock`` and ``MagicMock`` have support to mock :ref:`async-context-managers` through ``__aenter__`` and ``__aexit__``. -By default, ``__aenter__`` is an ``AsyncMock`` that returns an async function. +By default, ``__aenter__`` and ``__aexit__`` are ``AsyncMock`` instances that +return an async function. >>> class AsyncContextManager: ... async def __aenter__(self): diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 6b29742bca70bb..8a712875ce9c11 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -415,9 +415,6 @@ def test_magic_methods_are_async_functions(self): cm = MagicMock(name='magic_cm') self.assertTrue(asyncio.iscoroutinefunction(cm.__aenter__)) self.assertTrue(asyncio.iscoroutinefunction(cm.__aexit__)) - # These should pass but cause warnings to be raised - # self.assertTrue(inspect.isawaitable(cm.__aenter__())) - # self.assertTrue(inspect.isawaitable(cm.__aexit__())) def test_set_return_value_of_aenter(self): def inner_test(mock_type): From bb57b0c0e49fc0a22dae02a8f39e12e0535f5cc5 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Fri, 13 Sep 2019 17:54:49 +0100 Subject: [PATCH 13/14] Updates tests with comments. --- Doc/library/unittest.mock-examples.rst | 6 ++-- Lib/unittest/mock.py | 2 +- Lib/unittest/test/testmock/testasync.py | 27 +++++++++------- .../test/testmock/testmagicmethods.py | 32 ++++++++++++++++++- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/Doc/library/unittest.mock-examples.rst b/Doc/library/unittest.mock-examples.rst index a16882f2d51200..e650bb1e23e03e 100644 --- a/Doc/library/unittest.mock-examples.rst +++ b/Doc/library/unittest.mock-examples.rst @@ -14,7 +14,7 @@ import asyncio import unittest - from unittest.mock import AsyncMock, Mock, MagicMock, patch, call, sentinel + from unittest.mock import Mock, MagicMock, AsyncMock, patch, call, sentinel class SomeClass: attribute = 'this is a doctest' @@ -314,8 +314,8 @@ return an async function. ... pass ... >>> asyncio.run(main()) - >>> mock_instance.__aenter__.assert_called_once() - >>> mock_instance.__aexit__.assert_called_once() + >>> mock_instance.__aenter__.assert_awaited_once() + >>> mock_instance.__aexit__.assert_awaited_once() Creating a Mock from an Existing Object diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 1c20bef6591c35..5fa823d3b65550 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -990,7 +990,7 @@ def _get_child_mock(self, /, **kw): klass = AsyncMock elif _new_name in _sync_async_magics: # Special case these ones b/c users will assume they are async, - # but they are actually sync + # but they are actually sync (ie. __aiter__) klass = MagicMock elif issubclass(_type, AsyncMockMixin): klass = AsyncMock diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 8a712875ce9c11..e5a72391fa02e1 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -402,7 +402,7 @@ async def main(self): return val def test_async_magic_methods_are_async_mocks_with_magicmock(self): - cm_mock = MagicMock(self.WithAsyncContextManager) + cm_mock = MagicMock(self.WithAsyncContextManager()) self.assertIsInstance(cm_mock.__aenter__, AsyncMock) self.assertIsInstance(cm_mock.__aexit__, AsyncMock) @@ -413,6 +413,9 @@ def test_magicmock_has_async_magic_methods(self): def test_magic_methods_are_async_functions(self): cm = MagicMock(name='magic_cm') + self.assertIsInstance(cm.__aenter__, AsyncMock) + self.assertIsInstance(cm.__aexit__, AsyncMock) + # AsyncMocks are also coroutine functions self.assertTrue(asyncio.iscoroutinefunction(cm.__aenter__)) self.assertTrue(asyncio.iscoroutinefunction(cm.__aexit__)) @@ -435,20 +438,23 @@ def inner_test(mock_type): def test_mock_supports_async_context_manager(self): def inner_test(mock_type): called = False - instance = self.WithAsyncContextManager() - mock_instance = mock_type(instance) + cm = self.WithAsyncContextManager() + cm_mock = mock_type(cm) async def use_context_manager(): nonlocal called - async with mock_instance as result: + async with cm_mock as result: called = True return result - result = asyncio.run(use_context_manager()) + cm_result = asyncio.run(use_context_manager()) self.assertTrue(called) - self.assertTrue(mock_instance.__aenter__.called) - self.assertTrue(mock_instance.__aexit__.called) - self.assertIsNot(mock_instance, result) + self.assertTrue(cm_mock.__aenter__.called) + self.assertTrue(cm_mock.__aexit__.called) + cm_mock.__aenter__.assert_awaited() + cm_mock.__aexit__.assert_awaited() + # We mock __aenter__ so it does not return self + self.assertIsNot(cm_mock, cm_result) for mock_type in [AsyncMock, MagicMock]: with self.subTest(f"test context manager magics with {mock_type}"): @@ -525,14 +531,13 @@ def test_aiter_set_return_value(self): mock_iter = AsyncMock(name="tester") mock_iter.__aiter__.return_value = [1, 2, 3] async def main(): - async for i in mock_iter: - return [i async for i in mock_iter] + return [i async for i in mock_iter] result = asyncio.run(main()) self.assertEqual(result, [1, 2, 3]) def test_mock_aiter_and_anext_asyncmock(self): def inner_test(mock_type): - instance = self.WithAsyncIterator + instance = self.WithAsyncIterator() mock_instance = mock_type(instance) # Check that the mock and the real thing bahave the same # __aiter__ is not actually async, so not a coroutinefunction diff --git a/Lib/unittest/test/testmock/testmagicmethods.py b/Lib/unittest/test/testmock/testmagicmethods.py index 5bc1c108678bfc..57f85e951e20ab 100644 --- a/Lib/unittest/test/testmock/testmagicmethods.py +++ b/Lib/unittest/test/testmock/testmagicmethods.py @@ -3,7 +3,7 @@ import unittest import os import sys -from unittest.mock import Mock, MagicMock, _magics +from unittest.mock import AsyncMock, Mock, MagicMock, _magics @@ -272,6 +272,34 @@ def test_magic_mock_equality(self): self.assertEqual(mock != mock, False) + # This should be fixed with issue38163 + @unittest.expectedFailure + def test_asyncmock_defaults(self): + mock = AsyncMock() + self.assertEqual(int(mock), 1) + self.assertEqual(complex(mock), 1j) + self.assertEqual(float(mock), 1.0) + self.assertNotIn(object(), mock) + self.assertEqual(len(mock), 0) + self.assertEqual(list(mock), []) + self.assertEqual(hash(mock), object.__hash__(mock)) + self.assertEqual(str(mock), object.__str__(mock)) + self.assertTrue(bool(mock)) + self.assertEqual(round(mock), mock.__round__()) + self.assertEqual(math.trunc(mock), mock.__trunc__()) + self.assertEqual(math.floor(mock), mock.__floor__()) + self.assertEqual(math.ceil(mock), mock.__ceil__()) + self.assertTrue(asyncio.iscoroutinefunction(mock.__aexit__)) + self.assertTrue(asyncio.iscoroutinefunction(mock.__aenter__)) + self.assertIsInstance(mock.__aenter__, AsyncMock) + self.assertIsInstance(mock.__aexit__, AsyncMock) + + # in Python 3 oct and hex use __index__ + # so these tests are for __index__ in py3k + self.assertEqual(oct(mock), '0o1') + self.assertEqual(hex(mock), '0x1') + # how to test __sizeof__ ? + def test_magicmock_defaults(self): mock = MagicMock() self.assertEqual(int(mock), 1) @@ -289,6 +317,8 @@ def test_magicmock_defaults(self): self.assertEqual(math.ceil(mock), mock.__ceil__()) self.assertTrue(asyncio.iscoroutinefunction(mock.__aexit__)) self.assertTrue(asyncio.iscoroutinefunction(mock.__aenter__)) + self.assertIsInstance(mock.__aenter__, AsyncMock) + self.assertIsInstance(mock.__aexit__, AsyncMock) # in Python 3 oct and hex use __index__ # so these tests are for __index__ in py3k From 8bc3e32471161fa82599b9eb6d092b960b23ddbe Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Tue, 17 Sep 2019 17:51:50 -0700 Subject: [PATCH 14/14] Update Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst Fixes spelling error. Co-Authored-By: Ezio Melotti --- .../next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst b/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst index 3b6c0de83f77a9..24a53013cec785 100644 --- a/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst +++ b/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst @@ -1,2 +1,2 @@ -Fixes AsyncMock so it doesn't crash when used with AsynContextManagers +Fixes AsyncMock so it doesn't crash when used with AsyncContextManagers or AsyncIterators.