From e1ae6536badc1282620b8a5deb9b5e11c37a9a42 Mon Sep 17 00:00:00 2001 From: Xtreak Date: Sat, 25 May 2019 19:27:41 +0530 Subject: [PATCH 01/11] Refactor create_autospec and async mock setup logic. * Handle late binding while setting attributes on AsyncMock during create_autospec. * Refactor out async mock setup logic outside the function. * Add awaited attribute during setup. * Fix assert_not_awaited error message. --- Lib/unittest/mock.py | 53 +++++++++++++-------- Lib/unittest/test/testmock/testasync.py | 61 ++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 22 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index b14bf01b28fdbd..976bff7a6e3424 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -51,6 +51,13 @@ def _is_async_obj(obj): return False +def _is_async_func(func): + if getattr(func, '__code__', None): + return asyncio.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 @@ -225,6 +232,29 @@ def reset_mock(): mock._mock_delegate = funcopy +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() + mock.awaited = _AwaitEvent(mock) + + for attribute in ('assert_awaited', + 'assert_awaited_once', + 'assert_awaited_with', + 'assert_awaited_once_with', + 'assert_any_await', + 'assert_has_awaits', + 'assert_not_awaited'): + def f(attribute, *args, **kwargs): + return getattr(mock.mock, attribute)(*args, **kwargs) + # setattr(mock, attribute, f) causes late binding + # hence attribute will always be the last value in the loop + # Use partial(f, attribute) to ensure the attribute is bound + # correctly. + setattr(mock, attribute, partial(f, attribute)) + + def _is_magic(name): return '__%s__' % name[2:-2] == name @@ -2151,7 +2181,7 @@ def assert_not_awaited(_mock_self): """ self = _mock_self if self.await_count != 0: - msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once." + msg = (f"Expected {self._mock_name or 'mock'} to have been not awaited." f" Awaited {self.await_count} times.") raise AssertionError(msg) @@ -2457,10 +2487,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, spec = type(spec) is_type = isinstance(spec, type) - if getattr(spec, '__code__', None): - is_async_func = asyncio.iscoroutinefunction(spec) - else: - is_async_func = False + is_async_func = _is_async_obj(spec) _kwargs = {'spec': spec} if spec_set: _kwargs = {'spec_set': spec} @@ -2503,21 +2530,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # recurse for functions mock = _set_signature(mock, spec) if is_async_func: - mock._is_coroutine = asyncio.coroutines._is_coroutine - mock.await_count = 0 - mock.await_args = None - mock.await_args_list = _CallList() - - for a in ('assert_awaited', - 'assert_awaited_once', - 'assert_awaited_with', - 'assert_awaited_once_with', - 'assert_any_await', - 'assert_has_awaits', - 'assert_not_awaited'): - def f(*args, **kwargs): - return getattr(wrapped_mock, a)(*args, **kwargs) - setattr(mock, a, f) + _setup_async_mock(mock) else: _check_signature(spec, mock, is_type, instance) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index a9aa1434b963f1..655054cc164f9d 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -2,7 +2,8 @@ import inspect import unittest -from unittest.mock import call, AsyncMock, patch, MagicMock, create_autospec +from unittest.mock import (call, AsyncMock, patch, MagicMock, create_autospec, + _AwaitEvent) def tearDownModule(): @@ -20,6 +21,9 @@ def normal_method(self): async def async_func(): pass +async def async_func_1(a, b, *, c): + pass + def normal_func(): pass @@ -141,8 +145,28 @@ def test_create_autospec_instance(self): create_autospec(async_func, instance=True) def test_create_autospec(self): - spec = create_autospec(async_func) + spec = create_autospec(async_func_1) + awaitable = spec(1, 2, c=3) + async def main(): + await awaitable + + self.assertEqual(spec.await_count, 0) + self.assertIsNone(spec.await_args) + self.assertEqual(spec.await_args_list, []) + self.assertIsInstance(spec.awaited, _AwaitEvent) + spec.assert_not_awaited() + + asyncio.run(main()) + self.assertTrue(asyncio.iscoroutinefunction(spec)) + self.assertTrue(asyncio.iscoroutine(awaitable)) + self.assertEqual(spec.await_count, 1) + self.assertEqual(spec.await_args, call(1, 2, c=3)) + self.assertEqual(spec.await_args_list, [call(1, 2, c=3)]) + spec.assert_awaited_once() + spec.assert_awaited_once_with(1, 2, c=3) + spec.assert_awaited_with(1, 2, c=3) + spec.assert_awaited() class AsyncSpecTest(unittest.TestCase): @@ -226,6 +250,39 @@ def test_async_attributes_coroutines(MockNormalClass): test_async_attributes_coroutines() + def test_patch_with_autospec(self): + + async def test_async(): + with patch(f"{__name__}.async_func_1", autospec=True) as mock_method: + awaitable = mock_method(1, 2, c=3) + self.assertIsInstance(mock_method.mock, AsyncMock) + + self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + self.assertTrue(asyncio.iscoroutine(awaitable)) + self.assertTrue(inspect.isawaitable(awaitable)) + self.assertEqual(mock_method.await_count, 0) + self.assertEqual(mock_method.await_args_list, []) + self.assertIsNone(mock_method.await_args) + self.assertIsInstance(mock_method.awaited, _AwaitEvent) + mock_method.assert_not_awaited() + + await awaitable + + self.assertEqual(mock_method.await_count, 1) + self.assertEqual(mock_method.await_args, call(1, 2, c=3)) + self.assertEqual(mock_method.await_args_list, [call(1, 2, c=3)]) + mock_method.assert_awaited_once() + mock_method.assert_awaited_once_with(1, 2, c=3) + mock_method.assert_awaited_with(1, 2, c=3) + mock_method.assert_awaited() + + mock_method.reset_mock() + self.assertEqual(mock_method.await_count, 0) + self.assertIsNone(mock_method.await_args) + self.assertEqual(mock_method.await_args_list, []) + + asyncio.run(test_async()) + class AsyncSpecSetTest(unittest.TestCase): def test_is_AsyncMock_patch(self): From 51579820c7dfbad916b4cd3c0e88f61dfb0933e9 Mon Sep 17 00:00:00 2001 From: Xtreak Date: Sat, 25 May 2019 19:35:42 +0530 Subject: [PATCH 02/11] Add new async method implementation to docs --- Doc/library/unittest.mock.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index 163da9aecdbbc5..5e4759f25e71eb 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -1945,7 +1945,7 @@ The full list of supported magic methods is: * Container methods: ``__getitem__``, ``__setitem__``, ``__delitem__``, ``__contains__``, ``__len__``, ``__iter__``, ``__reversed__`` and ``__missing__`` -* Context manager: ``__enter__`` and ``__exit__`` +* Context manager: ``__enter__``, ``__exit__``, ``__aenter`` and ``__aexit__`` * Unary numeric methods: ``__neg__``, ``__pos__`` and ``__invert__`` * The numeric methods (including right hand and in-place variants): ``__add__``, ``__sub__``, ``__mul__``, ``__matmul__``, ``__div__``, ``__truediv__``, @@ -1957,10 +1957,14 @@ The full list of supported magic methods is: * Pickling: ``__reduce__``, ``__reduce_ex__``, ``__getinitargs__``, ``__getnewargs__``, ``__getstate__`` and ``__setstate__`` * File system path representation: ``__fspath__`` +* Asynchronous iteration methods: ``__aiter__``, ``__anext__`` .. versionchanged:: 3.8 Added support for :func:`os.PathLike.__fspath__`. +.. versionchanged:: 3.8 + Added support for ``__aenter__``, ``__aexit__``, ``__aiter__`` and ``__anext__``. + The following methods exist but are *not* supported as they are either in use by mock, can't be set dynamically, or can cause problems: From f5742d7ef0c5d21427a5d156e87b87cfce2843fe Mon Sep 17 00:00:00 2001 From: Xtreak Date: Sat, 25 May 2019 19:40:08 +0530 Subject: [PATCH 03/11] Rename async_func_1 to async_func_args --- Lib/unittest/test/testmock/testasync.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 655054cc164f9d..2286d05d47a225 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -21,7 +21,7 @@ def normal_method(self): async def async_func(): pass -async def async_func_1(a, b, *, c): +async def async_func_args(a, b, *, c): pass def normal_func(): @@ -145,7 +145,7 @@ def test_create_autospec_instance(self): create_autospec(async_func, instance=True) def test_create_autospec(self): - spec = create_autospec(async_func_1) + spec = create_autospec(async_func_args) awaitable = spec(1, 2, c=3) async def main(): await awaitable @@ -253,13 +253,15 @@ def test_async_attributes_coroutines(MockNormalClass): def test_patch_with_autospec(self): async def test_async(): - with patch(f"{__name__}.async_func_1", autospec=True) as mock_method: + with patch(f"{__name__}.async_func_args", autospec=True) as mock_method: awaitable = mock_method(1, 2, c=3) self.assertIsInstance(mock_method.mock, AsyncMock) self.assertTrue(asyncio.iscoroutinefunction(mock_method)) self.assertTrue(asyncio.iscoroutine(awaitable)) self.assertTrue(inspect.isawaitable(awaitable)) + + # Verify the default values during mock setup self.assertEqual(mock_method.await_count, 0) self.assertEqual(mock_method.await_args_list, []) self.assertIsNone(mock_method.await_args) From 0edc64421d1314f2377b7fcd06d2155836a20aa2 Mon Sep 17 00:00:00 2001 From: Xtreak Date: Sat, 25 May 2019 19:43:01 +0530 Subject: [PATCH 04/11] Move patch with autospec to respective group --- Lib/unittest/test/testmock/testasync.py | 70 ++++++++++++------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 2286d05d47a225..0519d59696f6c6 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -168,6 +168,41 @@ async def main(): spec.assert_awaited_with(1, 2, c=3) spec.assert_awaited() + def test_patch_with_autospec(self): + + async def test_async(): + with patch(f"{__name__}.async_func_args", autospec=True) as mock_method: + awaitable = mock_method(1, 2, c=3) + self.assertIsInstance(mock_method.mock, AsyncMock) + + self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + self.assertTrue(asyncio.iscoroutine(awaitable)) + self.assertTrue(inspect.isawaitable(awaitable)) + + # Verify the default values during mock setup + self.assertEqual(mock_method.await_count, 0) + self.assertEqual(mock_method.await_args_list, []) + self.assertIsNone(mock_method.await_args) + self.assertIsInstance(mock_method.awaited, _AwaitEvent) + mock_method.assert_not_awaited() + + await awaitable + + self.assertEqual(mock_method.await_count, 1) + self.assertEqual(mock_method.await_args, call(1, 2, c=3)) + self.assertEqual(mock_method.await_args_list, [call(1, 2, c=3)]) + mock_method.assert_awaited_once() + mock_method.assert_awaited_once_with(1, 2, c=3) + mock_method.assert_awaited_with(1, 2, c=3) + mock_method.assert_awaited() + + mock_method.reset_mock() + self.assertEqual(mock_method.await_count, 0) + self.assertIsNone(mock_method.await_args) + self.assertEqual(mock_method.await_args_list, []) + + asyncio.run(test_async()) + class AsyncSpecTest(unittest.TestCase): def test_spec_as_async_positional_magicmock(self): @@ -250,41 +285,6 @@ def test_async_attributes_coroutines(MockNormalClass): test_async_attributes_coroutines() - def test_patch_with_autospec(self): - - async def test_async(): - with patch(f"{__name__}.async_func_args", autospec=True) as mock_method: - awaitable = mock_method(1, 2, c=3) - self.assertIsInstance(mock_method.mock, AsyncMock) - - self.assertTrue(asyncio.iscoroutinefunction(mock_method)) - self.assertTrue(asyncio.iscoroutine(awaitable)) - self.assertTrue(inspect.isawaitable(awaitable)) - - # Verify the default values during mock setup - self.assertEqual(mock_method.await_count, 0) - self.assertEqual(mock_method.await_args_list, []) - self.assertIsNone(mock_method.await_args) - self.assertIsInstance(mock_method.awaited, _AwaitEvent) - mock_method.assert_not_awaited() - - await awaitable - - self.assertEqual(mock_method.await_count, 1) - self.assertEqual(mock_method.await_args, call(1, 2, c=3)) - self.assertEqual(mock_method.await_args_list, [call(1, 2, c=3)]) - mock_method.assert_awaited_once() - mock_method.assert_awaited_once_with(1, 2, c=3) - mock_method.assert_awaited_with(1, 2, c=3) - mock_method.assert_awaited() - - mock_method.reset_mock() - self.assertEqual(mock_method.await_count, 0) - self.assertIsNone(mock_method.await_args) - self.assertEqual(mock_method.await_args_list, []) - - asyncio.run(test_async()) - class AsyncSpecSetTest(unittest.TestCase): def test_is_AsyncMock_patch(self): From f4508e85091281172e3a1f226c643ec683002963 Mon Sep 17 00:00:00 2001 From: Xtreak Date: Sun, 26 May 2019 00:40:15 +0530 Subject: [PATCH 05/11] Rephrase assert_not_awaited like assert_not_called --- Lib/unittest/mock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 976bff7a6e3424..bc49ba229fbd0b 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2181,7 +2181,7 @@ def assert_not_awaited(_mock_self): """ self = _mock_self if self.await_count != 0: - msg = (f"Expected {self._mock_name or 'mock'} to have been not awaited." + msg = (f"Expected {self._mock_name or 'mock'} to not have been awaited." f" Awaited {self.await_count} times.") raise AssertionError(msg) From 9cd0344acd6b97757b0edb879dc93b84cdf8195b Mon Sep 17 00:00:00 2001 From: Xtreak Date: Sun, 26 May 2019 00:43:47 +0530 Subject: [PATCH 06/11] Remove unused variable --- Lib/unittest/mock.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index bc49ba229fbd0b..30288c92a48358 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2525,7 +2525,6 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, name=_name, **_kwargs) if isinstance(spec, FunctionTypes): - wrapped_mock = mock # should only happen at the top level because we don't # recurse for functions mock = _set_signature(mock, spec) From 1b2ca22e2763fe8b33b47482b2d64a17a5637c68 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sun, 26 May 2019 01:10:52 +0530 Subject: [PATCH 07/11] Fix grammar to use and --- 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 5e4759f25e71eb..36ac24afa2bc07 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -1957,7 +1957,7 @@ The full list of supported magic methods is: * Pickling: ``__reduce__``, ``__reduce_ex__``, ``__getinitargs__``, ``__getnewargs__``, ``__getstate__`` and ``__setstate__`` * File system path representation: ``__fspath__`` -* Asynchronous iteration methods: ``__aiter__``, ``__anext__`` +* Asynchronous iteration methods: ``__aiter__`` and ``__anext__`` .. versionchanged:: 3.8 Added support for :func:`os.PathLike.__fspath__`. From e1dd0ef987ab2c622cfc0eba8f84d8e2689d897d Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sun, 26 May 2019 01:13:17 +0530 Subject: [PATCH 08/11] Use _is_async_func --- Lib/unittest/mock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 30288c92a48358..5e3b0b1474cf30 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2487,7 +2487,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, spec = type(spec) is_type = isinstance(spec, type) - is_async_func = _is_async_obj(spec) + is_async_func = _is_async_func(spec) _kwargs = {'spec': spec} if spec_set: _kwargs = {'spec_set': spec} From 8e6500c6ad3114107faa0cfdb89733b81ec77c75 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sun, 26 May 2019 01:20:19 +0530 Subject: [PATCH 09/11] Add NEWS --- .../next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst diff --git a/Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst b/Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst new file mode 100644 index 00000000000000..63407dc7c41493 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst @@ -0,0 +1,2 @@ +Handle late binding and attribute access in AsyncMock setup for +autospeccing. Document newly implemented async methods in MagicMock. From 260bb94f83923d69edc3eaef975c72c341dc1216 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sun, 26 May 2019 09:05:53 +0530 Subject: [PATCH 10/11] Rename the variable that is also loop variable and move function out of the loop --- Lib/unittest/mock.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 5e3b0b1474cf30..b91afd88dd132e 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -239,6 +239,12 @@ def _setup_async_mock(mock): mock.await_args_list = _CallList() mock.awaited = _AwaitEvent(mock) + # Mock is not configured yet so the attributes are set + # to a function and then the corresponding mock helper function + # is called when the helper is accessed similar to _setup_func. + def wrapper(attr, *args, **kwargs): + return getattr(mock.mock, attr)(*args, **kwargs) + for attribute in ('assert_awaited', 'assert_awaited_once', 'assert_awaited_with', @@ -246,13 +252,12 @@ def _setup_async_mock(mock): 'assert_any_await', 'assert_has_awaits', 'assert_not_awaited'): - def f(attribute, *args, **kwargs): - return getattr(mock.mock, attribute)(*args, **kwargs) - # setattr(mock, attribute, f) causes late binding + + # setattr(mock, attribute, wrapper) causes late binding # hence attribute will always be the last value in the loop - # Use partial(f, attribute) to ensure the attribute is bound + # Use partial(wrapper, attribute) to ensure the attribute is bound # correctly. - setattr(mock, attribute, partial(f, attribute)) + setattr(mock, attribute, partial(wrapper, attribute)) def _is_magic(name): From 6ad528da5b10ec4f08e56779b79ec6960dabd5f5 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Mon, 27 May 2019 16:17:47 +0530 Subject: [PATCH 11/11] Use markup for NEWS --- .../next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst b/Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst index 63407dc7c41493..ace5a3a4417895 100644 --- a/Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst +++ b/Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst @@ -1,2 +1,3 @@ -Handle late binding and attribute access in AsyncMock setup for -autospeccing. Document newly implemented async methods in MagicMock. +Handle late binding and attribute access in :class:`unittest.mock.AsyncMock` +setup for autospeccing. Document newly implemented async methods in +:class:`unittest.mock.MagicMock`.