Skip to content

[3.8] bpo-38108: Makes mock objects inherit from Base (GH-16060) #16470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 19 additions & 36 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ def __new__(cls, /, *args, **kw):
if _is_async_obj(bound_args[spec_arg[0]]):
bases = (AsyncMockMixin, cls,)
new = type(cls.__name__, bases, {'__doc__': cls.__doc__})
instance = object.__new__(new)
instance = _safe_super(NonCallableMock, cls).__new__(new)
return instance


Expand Down Expand Up @@ -995,17 +995,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]
Expand Down Expand Up @@ -1872,6 +1873,7 @@ def _patch_stopall():
"round trunc floor ceil "
"bool next "
"fspath "
"aiter "
)

numerics = (
Expand Down Expand Up @@ -2010,21 +2012,22 @@ 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)
self._mock_set_magics() # fix magic broken by upper level init


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:
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__:
Expand Down Expand Up @@ -2052,33 +2055,13 @@ 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
self._mock_set_magics() # 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() # fix magic broken by upper level init

class MagicMock(MagicMixin, 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
Expand All @@ -2100,7 +2083,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
Expand Down
55 changes: 37 additions & 18 deletions Lib/unittest/test/testmock/testasync.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,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):
class WithAsyncContextManager:
Expand Down Expand Up @@ -402,24 +439,6 @@ 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())
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 inner_test(mock_type):
pc = self.ProductionCode()
Expand Down
3 changes: 0 additions & 3 deletions Lib/unittest/test/testmock/testmagicmethods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.