Skip to content

Commit

Permalink
gh-83076: 3.8x speed improvement in (Async)Mock instantiation (#100252)
Browse files Browse the repository at this point in the history
Backports: c5726b727e26b81a267933654cf26b760a90d9aa
Signed-off-by: Chris Withers <chris@simplistix.co.uk>
  • Loading branch information
carljm authored and cjw296 committed Dec 28, 2022
1 parent a2cb0be commit 36e4a68
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 16 deletions.
1 change: 1 addition & 0 deletions NEWS.d/2022-12-14-17-37-01.gh-issue-83076.NaYzWT.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Instantiation of ``Mock()`` and ``AsyncMock()`` is now 3.8x faster.
37 changes: 21 additions & 16 deletions mock/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,15 +414,18 @@ class NonCallableMock(Base):
# necessary.
_lock = RLock()

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__})
Expand Down Expand Up @@ -508,10 +511,6 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False,
_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):
_spec_class = spec
Expand All @@ -521,7 +520,13 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False,
_spec_as_instance, _eat_self)
_spec_signature = res and res[1]

spec = dir(spec)
spec_list = dir(spec)

for attr in spec_list:
if iscoroutinefunction(getattr(spec, attr, None)):
_spec_asyncs.append(attr)

spec = spec_list

__dict__ = self.__dict__
__dict__['_spec_class'] = _spec_class
Expand Down Expand Up @@ -1065,9 +1070,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
Expand Down Expand Up @@ -2172,10 +2174,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):
Expand Down Expand Up @@ -2218,6 +2217,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')
Expand All @@ -2235,7 +2238,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
self.__dict__['__name__'] = 'AsyncMock'
Expand Down
13 changes: 13 additions & 0 deletions mock/tests/testasync.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,19 @@ def test_spec_normal_methods_on_class_with_mock(self):
self.assertIsInstance(mock.async_method, AsyncMock)
self.assertIsInstance(mock.normal_method, Mock)

def test_spec_async_attributes_instance(self):
async_instance = AsyncClass()
async_instance.async_func_attr = async_func
async_instance.later_async_func_attr = normal_func

mock_async_instance = Mock(spec_set=async_instance)

async_instance.later_async_func_attr = async_func

self.assertIsInstance(mock_async_instance.async_func_attr, AsyncMock)
# only the shape of the spec at the time of mock construction matters
self.assertNotIsInstance(mock_async_instance.later_async_func_attr, AsyncMock)

def test_spec_mock_type_kw(self):
def inner_test(mock_type):
async_mock = mock_type(spec=async_func)
Expand Down

0 comments on commit 36e4a68

Please sign in to comment.