bpo-38108: Makes mock objects inherit from Base (GH-16060)

This commit is contained in:
Lisa Roach 2019-09-28 18:42:44 -07:00 committed by GitHub
parent f185a73249
commit 9a7d951950
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 58 additions and 57 deletions

View File

@ -409,7 +409,7 @@ class NonCallableMock(Base):
if spec_arg and _is_async_obj(spec_arg): if spec_arg and _is_async_obj(spec_arg):
bases = (AsyncMockMixin, cls) bases = (AsyncMockMixin, cls)
new = type(cls.__name__, bases, {'__doc__': cls.__doc__}) new = type(cls.__name__, bases, {'__doc__': cls.__doc__})
instance = object.__new__(new) instance = _safe_super(NonCallableMock, cls).__new__(new)
return instance return instance
@ -990,17 +990,18 @@ class NonCallableMock(Base):
_type = type(self) _type = type(self)
if issubclass(_type, MagicMock) and _new_name in _async_method_magics: if issubclass(_type, MagicMock) and _new_name in _async_method_magics:
# Any asynchronous magic becomes an AsyncMock
klass = 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): 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): elif not issubclass(_type, CallableMixin):
if issubclass(_type, NonCallableMagicMock): if issubclass(_type, NonCallableMagicMock):
klass = MagicMock klass = MagicMock
elif issubclass(_type, NonCallableMock) : elif issubclass(_type, NonCallableMock):
klass = Mock klass = Mock
else: else:
klass = _type.__mro__[1] klass = _type.__mro__[1]
@ -1886,6 +1887,7 @@ magic_methods = (
"round trunc floor ceil " "round trunc floor ceil "
"bool next " "bool next "
"fspath " "fspath "
"aiter "
) )
numerics = ( numerics = (
@ -2024,7 +2026,7 @@ def _set_return_value(mock, method, name):
class MagicMixin(object): class MagicMixin(Base):
def __init__(self, /, *args, **kw): def __init__(self, /, *args, **kw):
self._mock_set_magics() # make magic work for kwargs in init self._mock_set_magics() # make magic work for kwargs in init
_safe_super(MagicMixin, self).__init__(*args, **kw) _safe_super(MagicMixin, self).__init__(*args, **kw)
@ -2032,13 +2034,14 @@ class MagicMixin(object):
def _mock_set_magics(self): def _mock_set_magics(self):
these_magics = _magics orig_magics = _magics | _async_method_magics
these_magics = orig_magics
if getattr(self, "_mock_methods", None) is not None: 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 = set()
remove_magics = _magics - these_magics remove_magics = orig_magics - these_magics
for entry in remove_magics: for entry in remove_magics:
if entry in type(self).__dict__: if entry in type(self).__dict__:
@ -2066,33 +2069,13 @@ class NonCallableMagicMock(MagicMixin, NonCallableMock):
self._mock_set_magics() self._mock_set_magics()
class AsyncMagicMixin: class AsyncMagicMixin(MagicMixin):
def __init__(self, /, *args, **kw): def __init__(self, /, *args, **kw):
self._mock_set_async_magics() # 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) _safe_super(AsyncMagicMixin, self).__init__(*args, **kw)
self._mock_set_async_magics() # fix magic broken by upper level init self._mock_set_magics() # fix magic broken by upper level init
def _mock_set_async_magics(self): class MagicMock(MagicMixin, Mock):
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))
class MagicMock(MagicMixin, AsyncMagicMixin, Mock):
""" """
MagicMock is a subclass of Mock with default implementations MagicMock is a subclass of Mock with default implementations
of most of the magic methods. You can use MagicMock without having to of most of the magic methods. You can use MagicMock without having to
@ -2114,7 +2097,7 @@ class MagicMock(MagicMixin, AsyncMagicMixin, Mock):
class MagicProxy(object): class MagicProxy(Base):
def __init__(self, name, parent): def __init__(self, name, parent):
self.name = name self.name = name
self.parent = parent self.parent = parent

View File

@ -379,6 +379,43 @@ class AsyncArguments(unittest.TestCase):
RuntimeError('coroutine raised StopIteration') 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): class AsyncContextManagerTest(unittest.TestCase):
@ -406,24 +443,6 @@ class AsyncContextManagerTest(unittest.TestCase):
val = await response.json() val = await response.json()
return val 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_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.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 test_set_return_value_of_aenter(self):
def inner_test(mock_type): def inner_test(mock_type):
pc = self.ProductionCode() pc = self.ProductionCode()

View File

@ -271,9 +271,6 @@ class TestMockingMagicMethods(unittest.TestCase):
self.assertEqual(mock == mock, True) self.assertEqual(mock == mock, True)
self.assertEqual(mock != mock, False) self.assertEqual(mock != mock, False)
# This should be fixed with issue38163
@unittest.expectedFailure
def test_asyncmock_defaults(self): def test_asyncmock_defaults(self):
mock = AsyncMock() mock = AsyncMock()
self.assertEqual(int(mock), 1) self.assertEqual(int(mock), 1)

View File

@ -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.