Skip to content
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

gh-83076: 3.8x speed improvement in (Async)Mock instantiation #100252

Merged
merged 9 commits into from
Dec 23, 2022
13 changes: 13 additions & 0 deletions Lib/test/test_unittest/testmock/testasync.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,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
38 changes: 22 additions & 16 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,15 +411,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 @@ -505,10 +508,6 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False,
_spec_signature = None
_spec_asyncs = []
carljm marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -518,7 +517,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 @@ -1057,9 +1062,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 @@ -2138,10 +2140,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):
"""
Expand Down Expand Up @@ -2183,6 +2183,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 @@ -2200,7 +2204,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)
tirkarthi marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Instantiation of ``Mock()`` and ``AsyncMock()`` is now 3.8x faster.