Skip to content

Commit 36e4a68

Browse files
carljmcjw296
authored andcommitted
gh-83076: 3.8x speed improvement in (Async)Mock instantiation (#100252)
Backports: c5726b727e26b81a267933654cf26b760a90d9aa Signed-off-by: Chris Withers <chris@simplistix.co.uk>
1 parent a2cb0be commit 36e4a68

File tree

3 files changed

+35
-16
lines changed

3 files changed

+35
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Instantiation of ``Mock()`` and ``AsyncMock()`` is now 3.8x faster.

mock/mock.py

+21-16
Original file line numberDiff line numberDiff line change
@@ -414,15 +414,18 @@ class NonCallableMock(Base):
414414
# necessary.
415415
_lock = RLock()
416416

417-
def __new__(cls, *args, **kw):
417+
def __new__(
418+
cls, spec=None, wraps=None, name=None, spec_set=None,
419+
parent=None, _spec_state=None, _new_name='', _new_parent=None,
420+
_spec_as_instance=False, _eat_self=None, unsafe=False, **kwargs
421+
):
418422
# every instance has its own class
419423
# so we can create magic methods on the
420424
# class without stomping on other mocks
421425
bases = (cls,)
422426
if not issubclass(cls, AsyncMockMixin):
423427
# Check if spec is an async object or function
424-
bound_args = _MOCK_SIG.bind_partial(cls, *args, **kw).arguments
425-
spec_arg = bound_args.get('spec_set', bound_args.get('spec'))
428+
spec_arg = spec_set or spec
426429
if spec_arg is not None and _is_async_obj(spec_arg):
427430
bases = (AsyncMockMixin, cls)
428431
new = type(cls.__name__, bases, {'__doc__': cls.__doc__})
@@ -508,10 +511,6 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False,
508511
_spec_signature = None
509512
_spec_asyncs = []
510513

511-
for attr in dir(spec):
512-
if iscoroutinefunction(getattr(spec, attr, None)):
513-
_spec_asyncs.append(attr)
514-
515514
if spec is not None and not _is_list(spec):
516515
if isinstance(spec, type):
517516
_spec_class = spec
@@ -521,7 +520,13 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False,
521520
_spec_as_instance, _eat_self)
522521
_spec_signature = res and res[1]
523522

524-
spec = dir(spec)
523+
spec_list = dir(spec)
524+
525+
for attr in spec_list:
526+
if iscoroutinefunction(getattr(spec, attr, None)):
527+
_spec_asyncs.append(attr)
528+
529+
spec = spec_list
525530

526531
__dict__ = self.__dict__
527532
__dict__['_spec_class'] = _spec_class
@@ -1065,9 +1070,6 @@ def _calls_repr(self, prefix="Calls"):
10651070
return f"\n{prefix}: {safe_repr(self.mock_calls)}."
10661071

10671072

1068-
_MOCK_SIG = inspect.signature(NonCallableMock.__init__)
1069-
1070-
10711073
class _AnyComparer(list):
10721074
"""A list which checks if it contains a call which may have an
10731075
argument of ANY, flipping the components of item and self from
@@ -2172,10 +2174,7 @@ def mock_add_spec(self, spec, spec_set=False):
21722174

21732175

21742176
class AsyncMagicMixin(MagicMixin):
2175-
def __init__(self, *args, **kw):
2176-
self._mock_set_magics() # make magic work for kwargs in init
2177-
_safe_super(AsyncMagicMixin, self).__init__(*args, **kw)
2178-
self._mock_set_magics() # fix magic broken by upper level init
2177+
pass
21792178

21802179

21812180
class MagicMock(MagicMixin, Mock):
@@ -2218,6 +2217,10 @@ def __get__(self, obj, _type=None):
22182217
return self.create_mock()
22192218

22202219

2220+
_CODE_ATTRS = dir(CodeType)
2221+
_CODE_SIG = inspect.signature(partial(CodeType.__init__, None))
2222+
2223+
22212224
class AsyncMockMixin(Base):
22222225
await_count = _delegating_property('await_count')
22232226
await_args = _delegating_property('await_args')
@@ -2235,7 +2238,9 @@ def __init__(self, *args, **kwargs):
22352238
self.__dict__['_mock_await_count'] = 0
22362239
self.__dict__['_mock_await_args'] = None
22372240
self.__dict__['_mock_await_args_list'] = _CallList()
2238-
code_mock = NonCallableMock(spec_set=CodeType)
2241+
code_mock = NonCallableMock(spec_set=_CODE_ATTRS)
2242+
code_mock.__dict__["_spec_class"] = CodeType
2243+
code_mock.__dict__["_spec_signature"] = _CODE_SIG
22392244
code_mock.co_flags = inspect.CO_COROUTINE
22402245
self.__dict__['__code__'] = code_mock
22412246
self.__dict__['__name__'] = 'AsyncMock'

mock/tests/testasync.py

+13
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,19 @@ def test_spec_normal_methods_on_class_with_mock(self):
306306
self.assertIsInstance(mock.async_method, AsyncMock)
307307
self.assertIsInstance(mock.normal_method, Mock)
308308

309+
def test_spec_async_attributes_instance(self):
310+
async_instance = AsyncClass()
311+
async_instance.async_func_attr = async_func
312+
async_instance.later_async_func_attr = normal_func
313+
314+
mock_async_instance = Mock(spec_set=async_instance)
315+
316+
async_instance.later_async_func_attr = async_func
317+
318+
self.assertIsInstance(mock_async_instance.async_func_attr, AsyncMock)
319+
# only the shape of the spec at the time of mock construction matters
320+
self.assertNotIsInstance(mock_async_instance.later_async_func_attr, AsyncMock)
321+
309322
def test_spec_mock_type_kw(self):
310323
def inner_test(mock_type):
311324
async_mock = mock_type(spec=async_func)

0 commit comments

Comments
 (0)