From 700291c0ee62538f451fc14a8a24542af8a13e0d Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Thu, 12 Sep 2019 15:32:58 +0100 Subject: [PATCH 1/6] Makes mock objects inherit from Base. --- Lib/unittest/mock.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 0cd7af65b94020..cf8dae00f34eb8 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -409,7 +409,7 @@ def __new__(cls, /, *args, **kw): if spec_arg and _is_async_obj(spec_arg): bases = (AsyncMockMixin, cls) new = type(cls.__name__, bases, {'__doc__': cls.__doc__}) - instance = object.__new__(new) + instance = _safe_super(NonCallableMock, cls).__new__(new) return instance @@ -2024,7 +2024,7 @@ def _set_return_value(mock, method, name): -class MagicMixin(object): +class MagicMixin(Base): def __init__(self, /, *args, **kw): self._mock_set_magics() # make magic work for kwargs in init _safe_super(MagicMixin, self).__init__(*args, **kw) @@ -2066,7 +2066,7 @@ def mock_add_spec(self, spec, spec_set=False): self._mock_set_magics() -class AsyncMagicMixin: +class AsyncMagicMixin(MagicMixin): def __init__(self, /, *args, **kw): self._mock_set_async_magics() # make magic work for kwargs in init _safe_super(AsyncMagicMixin, self).__init__(*args, **kw) @@ -2092,7 +2092,7 @@ def _mock_set_async_magics(self): setattr(_type, entry, MagicProxy(entry, self)) -class MagicMock(MagicMixin, AsyncMagicMixin, Mock): +class MagicMock(AsyncMagicMixin, Mock): """ MagicMock is a subclass of Mock with default implementations of most of the magic methods. You can use MagicMock without having to @@ -2114,7 +2114,7 @@ def mock_add_spec(self, spec, spec_set=False): -class MagicProxy(object): +class MagicProxy(Base): def __init__(self, name, parent): self.name = name self.parent = parent From 6e425d0f6203dbda2acb795f0dd1645a1c0a61c1 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Thu, 12 Sep 2019 15:45:20 +0100 Subject: [PATCH 2/6] Reduces duplicate code in _mock_set_async_magics. --- Lib/unittest/mock.py | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index cf8dae00f34eb8..115bb82919baed 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2031,14 +2031,16 @@ def __init__(self, /, *args, **kw): self._mock_set_magics() # fix magic broken by upper level init - def _mock_set_magics(self): + def _mock_set_magics(self, async_magics=False): + orig_magics = _magics if not async_magics else _async_magics + these_magics = orig_magics these_magics = _magics if getattr(self, "_mock_methods", None) is not None: - these_magics = _magics.intersection(self._mock_methods) + these_magics = orig_magics.intersection(self._mock_methods) remove_magics = set() - remove_magics = _magics - these_magics + remove_magics = orig_magics - these_magics for entry in remove_magics: if entry in type(self).__dict__: @@ -2068,29 +2070,9 @@ def mock_add_spec(self, spec, spec_set=False): class AsyncMagicMixin(MagicMixin): def __init__(self, /, *args, **kw): - self._mock_set_async_magics() # make magic work for kwargs in init + self._mock_set_magics(async_magics=True) # make magic work for kwargs in init _safe_super(AsyncMagicMixin, self).__init__(*args, **kw) - self._mock_set_async_magics() # fix magic broken by upper level init - - def _mock_set_async_magics(self): - these_magics = _async_magics - - if getattr(self, "_mock_methods", None) is not None: - these_magics = _async_magics.intersection(self._mock_methods) - remove_magics = _async_magics - these_magics - - for entry in remove_magics: - if entry in type(self).__dict__: - # remove unneeded magic methods - delattr(self, entry) - - # don't overwrite existing attributes if called a second time - these_magics = these_magics - set(type(self).__dict__) - - _type = type(self) - for entry in these_magics: - setattr(_type, entry, MagicProxy(entry, self)) - + self._mock_set_magics(async_magics=True) # fix magic broken by upper level init class MagicMock(AsyncMagicMixin, Mock): """ From c554c1a237ebc986c49fe1f3378796014171f15a Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 25 Sep 2019 21:30:19 -0700 Subject: [PATCH 3/6] Updates AsyncMock subclassing. --- Lib/unittest/mock.py | 19 +++++++-------- Lib/unittest/test/testmock/testasync.py | 23 +++++++++++++++++-- .../test/testmock/testmagicmethods.py | 3 --- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 115bb82919baed..3e959b3429ad0a 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -990,17 +990,18 @@ def _get_child_mock(self, /, **kw): _type = type(self) if issubclass(_type, MagicMock) and _new_name in _async_method_magics: + # Any asynchronous magic becomes an AsyncMock 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 (ie. __aiter__) - klass = MagicMock elif issubclass(_type, AsyncMockMixin): - klass = AsyncMock + if _new_name in _all_sync_magics: + # Any synchronous magic becomes a MagicMock + klass = MagicMock + else: + klass = AsyncMock elif not issubclass(_type, CallableMixin): if issubclass(_type, NonCallableMagicMock): klass = MagicMock - elif issubclass(_type, NonCallableMock) : + elif issubclass(_type, NonCallableMock): klass = Mock else: klass = _type.__mro__[1] @@ -1886,6 +1887,7 @@ def _patch_stopall(): "round trunc floor ceil " "bool next " "fspath " + "aiter " ) numerics = ( @@ -2032,9 +2034,8 @@ def __init__(self, /, *args, **kw): def _mock_set_magics(self, async_magics=False): - orig_magics = _magics if not async_magics else _async_magics + orig_magics = _magics | _async_method_magics these_magics = orig_magics - these_magics = _magics if getattr(self, "_mock_methods", None) is not None: these_magics = orig_magics.intersection(self._mock_methods) @@ -2074,7 +2075,7 @@ def __init__(self, /, *args, **kw): _safe_super(AsyncMagicMixin, self).__init__(*args, **kw) self._mock_set_magics(async_magics=True) # fix magic broken by upper level init -class MagicMock(AsyncMagicMixin, Mock): +class MagicMock(MagicMixin, Mock): """ MagicMock is a subclass of Mock with default implementations of most of the magic methods. You can use MagicMock without having to diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index fde1e4a690b270..5acab79a33149c 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -406,15 +406,34 @@ async def main(self): val = await response.json() return val - def test_async_magic_methods_are_async_mocks_with_magicmock(self): - cm_mock = MagicMock(self.WithAsyncContextManager()) + def test_async_magic_methods_return_async_mocks(self): + cm_mock = MagicMock() self.assertIsInstance(cm_mock.__aenter__, AsyncMock) self.assertIsInstance(cm_mock.__aexit__, AsyncMock) + self.assertIsInstance(cm_mock.__anext__, AsyncMock) + # __aiter__ is actually a synchronous object + # so should return a MagicMock + self.assertIsInstance(cm_mock.__aiter__, MagicMock) + + def test_sync_magic_methods_return_magic_mocks(self): + am_mock = AsyncMock() + self.assertIsInstance(am_mock.__enter__, MagicMock) + self.assertIsInstance(am_mock.__exit__, MagicMock) + self.assertIsInstance(am_mock.__next__, MagicMock) + self.assertIsInstance(am_mock.__len__, MagicMock) 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(hasattr(cm, "__anext__")) + + def test_asyncmock_has_sync_magic_methods(self): + am = AsyncMock(name='async_cm') + self.assertTrue(hasattr(am, "__enter__")) + self.assertTrue(hasattr(am, "__exit__")) + self.assertTrue(hasattr(am, "__next__")) + self.assertTrue(hasattr(am, "__len__")) def test_magic_methods_are_async_functions(self): cm = MagicMock(name='magic_cm') diff --git a/Lib/unittest/test/testmock/testmagicmethods.py b/Lib/unittest/test/testmock/testmagicmethods.py index 57f85e951e20ab..76b3a560de090a 100644 --- a/Lib/unittest/test/testmock/testmagicmethods.py +++ b/Lib/unittest/test/testmock/testmagicmethods.py @@ -271,9 +271,6 @@ def test_magic_mock_equality(self): self.assertEqual(mock == mock, True) 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) From bb9074410a04584176bf67eb75c333d51b890f4f Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 25 Sep 2019 21:37:16 -0700 Subject: [PATCH 4/6] Adds news entry. --- .../next/Library/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst diff --git a/Misc/NEWS.d/next/Library/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst b/Misc/NEWS.d/next/Library/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst new file mode 100644 index 00000000000000..d7eea367e482b3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst @@ -0,0 +1,2 @@ +Any synchronous magic methods on an AsyncMock now return a MagicMock. Any +asynchronous magic methods on a MagicMock now return an AsyncMock. From 94f0c8d31a0ed3b015c4fc9ed03aa523a4942589 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 25 Sep 2019 21:58:55 -0700 Subject: [PATCH 5/6] Create new unit test class. --- Lib/unittest/test/testmock/testasync.py | 74 ++++++++++++------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 5acab79a33149c..624fadca264950 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -379,6 +379,43 @@ def test_add_side_effect_iterable(self): RuntimeError('coroutine raised StopIteration') ) +class AsyncMagicMethods(unittest.TestCase): + def test_async_magic_methods_return_async_mocks(self): + m_mock = MagicMock() + self.assertIsInstance(m_mock.__aenter__, AsyncMock) + self.assertIsInstance(m_mock.__aexit__, AsyncMock) + self.assertIsInstance(m_mock.__anext__, AsyncMock) + # __aiter__ is actually a synchronous object + # so should return a MagicMock + self.assertIsInstance(m_mock.__aiter__, MagicMock) + + def test_sync_magic_methods_return_magic_mocks(self): + a_mock = AsyncMock() + self.assertIsInstance(a_mock.__enter__, MagicMock) + self.assertIsInstance(a_mock.__exit__, MagicMock) + self.assertIsInstance(a_mock.__next__, MagicMock) + self.assertIsInstance(a_mock.__len__, MagicMock) + + def test_magicmock_has_async_magic_methods(self): + m_mock = MagicMock() + self.assertTrue(hasattr(m_mock, "__aenter__")) + self.assertTrue(hasattr(m_mock, "__aexit__")) + self.assertTrue(hasattr(m_mock, "__anext__")) + + def test_asyncmock_has_sync_magic_methods(self): + a_mock = AsyncMock() + self.assertTrue(hasattr(a_mock, "__enter__")) + self.assertTrue(hasattr(a_mock, "__exit__")) + self.assertTrue(hasattr(a_mock, "__next__")) + self.assertTrue(hasattr(a_mock, "__len__")) + + def test_magic_methods_are_async_functions(self): + m_mock = MagicMock() + self.assertIsInstance(m_mock.__aenter__, AsyncMock) + self.assertIsInstance(m_mock.__aexit__, AsyncMock) + # AsyncMocks are also coroutine functions + self.assertTrue(asyncio.iscoroutinefunction(m_mock.__aenter__)) + self.assertTrue(asyncio.iscoroutinefunction(m_mock.__aexit__)) class AsyncContextManagerTest(unittest.TestCase): @@ -406,43 +443,6 @@ async def main(self): val = await response.json() return val - def test_async_magic_methods_return_async_mocks(self): - cm_mock = MagicMock() - self.assertIsInstance(cm_mock.__aenter__, AsyncMock) - self.assertIsInstance(cm_mock.__aexit__, AsyncMock) - self.assertIsInstance(cm_mock.__anext__, AsyncMock) - # __aiter__ is actually a synchronous object - # so should return a MagicMock - self.assertIsInstance(cm_mock.__aiter__, MagicMock) - - def test_sync_magic_methods_return_magic_mocks(self): - am_mock = AsyncMock() - self.assertIsInstance(am_mock.__enter__, MagicMock) - self.assertIsInstance(am_mock.__exit__, MagicMock) - self.assertIsInstance(am_mock.__next__, MagicMock) - self.assertIsInstance(am_mock.__len__, MagicMock) - - 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(hasattr(cm, "__anext__")) - - def test_asyncmock_has_sync_magic_methods(self): - am = AsyncMock(name='async_cm') - self.assertTrue(hasattr(am, "__enter__")) - self.assertTrue(hasattr(am, "__exit__")) - self.assertTrue(hasattr(am, "__next__")) - self.assertTrue(hasattr(am, "__len__")) - - 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__)) - def test_set_return_value_of_aenter(self): def inner_test(mock_type): pc = self.ProductionCode() From 4ca4890914dac1a180284e1aec378ffa69c56617 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Fri, 27 Sep 2019 15:39:49 -0700 Subject: [PATCH 6/6] Removes unneeded kwarg. --- Lib/unittest/mock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 3e959b3429ad0a..27aa3bc65c9a73 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2033,7 +2033,7 @@ def __init__(self, /, *args, **kw): self._mock_set_magics() # fix magic broken by upper level init - def _mock_set_magics(self, async_magics=False): + def _mock_set_magics(self): orig_magics = _magics | _async_method_magics these_magics = orig_magics @@ -2071,9 +2071,9 @@ def mock_add_spec(self, spec, spec_set=False): class AsyncMagicMixin(MagicMixin): def __init__(self, /, *args, **kw): - self._mock_set_magics(async_magics=True) # make magic work for kwargs in init + self._mock_set_magics() # make magic work for kwargs in init _safe_super(AsyncMagicMixin, self).__init__(*args, **kw) - self._mock_set_magics(async_magics=True) # fix magic broken by upper level init + self._mock_set_magics() # fix magic broken by upper level init class MagicMock(MagicMixin, Mock): """