Skip to content

Commit

Permalink
Port mock performance improvements
Browse files Browse the repository at this point in the history
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: 38779e0
  • Loading branch information
DinoV authored and facebook-github-bot committed Mar 25, 2022
1 parent 3342e16 commit 793f7d0
Showing 1 changed file with 14 additions and 22 deletions.
36 changes: 14 additions & 22 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 793f7d0

Please sign in to comment.