From 733b7b3b492551bc32111980b4f052e149cd2829 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 14 Dec 2022 11:11:28 -0800 Subject: [PATCH] Port mock performance improvements Summary: Modified port of D35118477 (https://github.com/facebookincubator/cinder/commit/793f7d0324b3327df5c186d301a81bdfa879e5d5) from 3.8, and backport of upstream PR https://github.com/python/cpython/pull/100252 Changes from the 3.8 version: - We don't need to keep `_spec_asyncs` around at all, it's not public API. - Changing the `__code__` attribute of an `AsyncMock` to be a real code object instead of a mock is probably fine, but it's technically backwards incompatible and could break someone's test suite, so I wasn't sure upstream would go for it. Instead we can create the mock object in a more manual way that avoids a lot of the introspection cost (by doing it just once up front.) Reviewed By: mpage Differential Revision: D42039568 fbshipit-source-id: a939f9c7b0c2158ea4d804fdf5d506826531a510 --- Lib/unittest/mock.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 7152f86ed96..01f6aea6a46 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -401,15 +401,18 @@ def __init__(self, /, *args, **kwargs): class NonCallableMock(Base): """A non-callable version of `Mock`""" - def __new__(cls, /, *args, **kw): + def __new__( + cls, spec=None, wraps=None, name=None, spec_set=None, + parent=None, _spec_state=None, _new_name='', _new_parent=None, + _spec_as_instance=False, _eat_self=None, unsafe=False, **kwargs + ): # every instance has its own class # so we can create magic methods on the # class without stomping on other mocks bases = (cls,) if not issubclass(cls, AsyncMockMixin): # Check if spec is an async object or function - bound_args = _MOCK_SIG.bind_partial(cls, *args, **kw).arguments - spec_arg = bound_args.get('spec_set', bound_args.get('spec')) + spec_arg = spec_set or spec if spec_arg is not None and _is_async_obj(spec_arg): bases = (AsyncMockMixin, cls) new = type(cls.__name__, bases, {'__doc__': cls.__doc__}) @@ -490,11 +493,6 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False, _eat_self=False): _spec_class = None _spec_signature = None - _spec_asyncs = [] - - for attr in dir(spec): - if iscoroutinefunction(getattr(spec, attr, None)): - _spec_asyncs.append(attr) if spec is not None and not _is_list(spec): if isinstance(spec, type): @@ -512,7 +510,6 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False, __dict__['_spec_set'] = spec_set __dict__['_spec_signature'] = _spec_signature __dict__['_mock_methods'] = spec - __dict__['_spec_asyncs'] = _spec_asyncs def __get_return_value(self): ret = self._mock_return_value @@ -1001,7 +998,8 @@ def _get_child_mock(self, /, **kw): For non-callable mocks the callable variant will be used (rather than any custom subclass).""" _new_name = kw.get("_new_name") - if _new_name in self.__dict__['_spec_asyncs']: + _spec_val = getattr(self.__dict__["_spec_class"], _new_name, None) + if _spec_val is not None and asyncio.iscoroutinefunction(_spec_val): return AsyncMock(**kw) if self._mock_sealed: @@ -1043,9 +1041,6 @@ def _calls_repr(self, prefix="Calls"): return f"\n{prefix}: {safe_repr(self.mock_calls)}." -_MOCK_SIG = inspect.signature(NonCallableMock.__init__) - - class _AnyComparer(list): """A list which checks if it contains a call which may have an argument of ANY, flipping the components of item and self from @@ -2121,10 +2116,8 @@ def mock_add_spec(self, spec, spec_set=False): class AsyncMagicMixin(MagicMixin): - def __init__(self, /, *args, **kw): - self._mock_set_magics() # make magic work for kwargs in init - _safe_super(AsyncMagicMixin, self).__init__(*args, **kw) - self._mock_set_magics() # fix magic broken by upper level init + pass + class MagicMock(MagicMixin, Mock): """ @@ -2166,6 +2159,10 @@ def __get__(self, obj, _type=None): return self.create_mock() +_CODE_ATTRS = dir(CodeType) +_CODE_SIG = inspect.signature(partial(CodeType.__init__, None)) + + class AsyncMockMixin(Base): await_count = _delegating_property('await_count') await_args = _delegating_property('await_args') @@ -2183,7 +2180,9 @@ def __init__(self, /, *args, **kwargs): self.__dict__['_mock_await_count'] = 0 self.__dict__['_mock_await_args'] = None self.__dict__['_mock_await_args_list'] = _CallList() - code_mock = NonCallableMock(spec_set=CodeType) + code_mock = NonCallableMock(spec_set=_CODE_ATTRS) + code_mock.__dict__["_spec_class"] = CodeType + code_mock.__dict__["_spec_signature"] = _CODE_SIG code_mock.co_flags = inspect.CO_COROUTINE self.__dict__['__code__'] = code_mock