From 793f7d0324b3327df5c186d301a81bdfa879e5d5 Mon Sep 17 00:00:00 2001 From: Dino Viehland Date: Fri, 25 Mar 2022 13:15:52 -0700 Subject: [PATCH] Port mock performance improvements Summary: It looks like updating to Mock 3.8 has caused some perf regressions. There's some known issues called out here:https://bugs.python.org/issue38895 - this: * Don't use `inspect` to get `spec` and `spec_sec` - instead now we fully spell out the signature and use the args directly. * provides a variation on not reflecting over all async methods on creation. The `_spec_asyncs` list still exists, but it is just not populated by default. Instead we'll consult it or check if the function is async. * Don't create the mock code object on instantiation of the `AsyncMock` - this is actually a huge portion of the cost of creating `AsyncMock` objects. * Avoid quadruple `self._mock_set_magics()` calls. This is actually done in the `MagicMixin` class so there's no need to do it in the `AsyncMagicMixin` Reviewed By: carljm Differential Revision: D35118477 fbshipit-source-id: 38779e0758fa97c1e467e30456f60df13117e91c --- Lib/unittest/mock.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 3629cf61098..3f55debe5ca 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -398,22 +398,19 @@ 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, AsyncMock): # Check if spec is an async object or function - sig = inspect.signature(NonCallableMock.__init__) - bound_args = sig.bind_partial(cls, *args, **kw).arguments - spec_arg = [ - arg for arg in bound_args.keys() - if arg.startswith('spec') - ] - if spec_arg: + spec_arg = spec_set or spec + if spec_arg is not None: # what if spec_set is different than spec? - if _is_async_obj(bound_args[spec_arg[0]]): + if _is_async_obj(spec_arg): bases = (AsyncMockMixin, cls,) new = type(cls.__name__, bases, {'__doc__': cls.__doc__}) instance = _safe_super(NonCallableMock, cls).__new__(new) @@ -495,10 +492,6 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False, _spec_signature = None _spec_asyncs = [] - for attr in dir(spec): - if asyncio.iscoroutinefunction(getattr(spec, attr, None)): - _spec_asyncs.append(attr) - if spec is not None and not _is_list(spec): if isinstance(spec, type): _spec_class = spec @@ -995,7 +988,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']: + if (_new_name in self.__dict__['_spec_asyncs'] or + asyncio.iscoroutinefunction(getattr(self.__dict__['_spec_class'], _new_name, None))): return AsyncMock(**kw) _type = type(self) @@ -1038,7 +1032,6 @@ def _calls_repr(self, prefix="Calls"): return f"\n{prefix}: {safe_repr(self.mock_calls)}." - def _try_iter(obj): if obj is None: return obj @@ -2044,10 +2037,7 @@ 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): """ @@ -2106,9 +2096,7 @@ 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.co_flags = inspect.CO_COROUTINE - self.__dict__['__code__'] = code_mock + self.__dict__['__code__'] = _CODE_DUMMY async def _execute_mock_call(self, /, *args, **kwargs): # This is nearly just like super(), except for sepcial handling @@ -2727,6 +2715,10 @@ def __init__(self, spec, spec_set=False, parent=None, file_spec = None +async def _dummy_async(): pass +_CODE_DUMMY = _dummy_async.__code__ + + def _to_stream(read_data): if isinstance(read_data, bytes): return io.BytesIO(read_data)