From 4353041ac52e379cdaf4902896995cc83a37c980 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Mon, 14 May 2018 10:08:38 -0400 Subject: [PATCH 01/24] Initial commmit adding asyncio mock support. --- Lib/unittest/mock.py | 374 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 367 insertions(+), 7 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index db1e642c00b7f9..3653e71a5ed8ae 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -13,6 +13,7 @@ 'ANY', 'call', 'create_autospec', + 'CoroutineMock', 'FILTER_DIR', 'NonCallableMock', 'NonCallableMagicMock', @@ -24,7 +25,7 @@ __version__ = '1.0' - +import asyncio import inspect import pprint import sys @@ -250,7 +251,7 @@ def __init__(self): def __getattr__(self, name): if name == '__bases__': - # Without this help(unittest.mock) raises an exception + # Without this help() raises an exception raise AttributeError return self._sentinels.setdefault(name, _SentinelObject(name)) @@ -357,8 +358,43 @@ def __init__(self, *args, **kwargs): pass +_is_coroutine = asyncio.iscoroutine +_isawaitable = inspect.isawaitable + +def _get_is_coroutine(self): + return self.__dict__['_mock_is_coroutine'] + + +def _set_is_coroutine(self, value): + # property setters and getters are overridden by Mock(), we need to + # update the dict to add values + value = _is_coroutine if bool(value) else False + self.__dict__['_mock_is_coroutine'] = value + + +class IsCoroutineMeta(type): + def __new__(meta, name, base, namespace): + namespace.update({ + '_get_is_coroutine': _get_is_coroutine, + '_set_is_coroutine': _set_is_coroutine, + 'is_coroutine': property(_get_is_coroutine, _set_is_coroutine, + doc="True if the object mocked is a coroutine"), + '_is_coroutine': property(_get_is_coroutine), + }) + + def __setattr__(self, name, value): + + if name == 'is_coroutine': + self._set_is_coroutine(value) + else: + return base[0].__setattr__(self, name, value) -class NonCallableMock(Base): + namespace['__setattr__'] = __setattr__ + + return super().__new__(meta, name, base, namespace) + + +class NonCallableMock(Base, metaclass=IsCoroutineMeta): """A non-callable version of `Mock`""" def __new__(cls, *args, **kw): @@ -372,8 +408,9 @@ def __new__(cls, *args, **kw): def __init__( self, 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 + parent=None, is_coroutine=None, _spec_state=None, _new_name='', + _new_parent=None, _spec_as_instance=False, _eat_self=None, + unsafe=False, **kwargs ): if _new_parent is None: _new_parent = parent @@ -414,6 +451,7 @@ def __init__( _spec_state ) + self._set_is_coroutine(is_coroutine) def attach_mock(self, mock, attribute): """ @@ -925,6 +963,98 @@ def _try_iter(obj): return obj +class _AwaitEvent: + def __init__(self, mock): + self._mock = mock + self._condition = None + + @asyncio.coroutine + def wait(self, skip=0): + """ + Wait for await. + + :param skip: How many awaits will be skipped. + As a result, the mock should be awaited at least + ``skip + 1`` times. + """ + def predicate(mock): + return mock.await_count > skip + + return (yield from self.wait_for(predicate)) + + @asyncio.coroutine + def wait_next(self, skip=0): + """ + Wait for the next await. + + Unlike :meth:`wait` that counts any await, mock has to be awaited once + more, disregarding to the current + :attr:`asynctest.CoroutineMock.await_count`. + + :param skip: How many awaits will be skipped. + As a result, the mock should be awaited at least + ``skip + 1`` more times. + """ + await_count = self._mock.await_count + + def predicate(mock): + return mock.await_count > await_count + skip + + return (yield from self.wait_for(predicate)) + + @asyncio.coroutine + def wait_for(self, predicate): + """ + Wait for a given predicate to become True. + + :param predicate: A callable that receives mock which result + will be interpreted as a boolean value. + The final predicate value is the return value. + """ + condition = self._get_condition() + + try: + yield from condition.acquire() + + def _predicate(): + return predicate(self._mock) + + return (yield from condition.wait_for(_predicate)) + finally: + condition.release() + + @asyncio.coroutine + def _notify(self): + condition = self._get_condition() + + try: + yield from condition.acquire() + condition.notify_all() + finally: + condition.release() + + def _get_condition(self): + """ + Creation of condition is delayed, to minimize the change of using the + wrong loop. + + A user may create a mock with _AwaitEvent before selecting the + execution loop. Requiring a user to delay creation is error-prone and + inflexible. Instead, condition is created when user actually starts to + use the mock. + """ + # No synchronization is needed: + # - asyncio is thread unsafe + # - there are no awaits here, method will be executed without + # switching asyncio context. + if self._condition is None: + self._condition = asyncio.Condition() + + return self._condition + + def __bool__(self): + return self._mock.await_count != 0 + class CallableMixin(Base): @@ -1026,7 +1156,6 @@ def _mock_call(_mock_self, *args, **kwargs): return ret_val - class Mock(CallableMixin, NonCallableMock): """ Create a new `Mock` object. `Mock` takes several optional arguments @@ -1079,6 +1208,231 @@ class or instance) that acts as the specification for the mock object. If """ +class CoroutineMock(Mock): + """ + Enhance :class:`~asynctest.mock.Mock` with features allowing to mock + a coroutine function. + + The :class:`~asynctest.CoroutineMock` object will behave so the object is + recognized as coroutine function, and the result of a call as a coroutine: + + >>> mock = CoroutineMock() + >>> asyncio.iscoroutinefunction(mock) + True + >>> asyncio.iscoroutine(mock()) + True + + + The result of ``mock()`` is a coroutine which will have the outcome of + ``side_effect`` or ``return_value``: + + - if ``side_effect`` is a function, the coroutine will return the result + of that function, + - if ``side_effect`` is an exception, the coroutine will raise the + exception, + - if ``side_effect`` is an iterable, the coroutine will return the next + value of the iterable, however, if the sequence of result is exhausted, + ``StopIteration`` is raised immediately, + - if ``side_effect`` is not defined, the coroutine will return the value + defined by ``return_value``, hence, by default, the coroutine returns + a new :class:`~asynctest.CoroutineMock` object. + + If the outcome of ``side_effect`` or ``return_value`` is a coroutine, the + mock coroutine obtained when the mock object is called will be this + coroutine itself (and not a coroutine returning a coroutine). + + The test author can also specify a wrapped object with ``wraps``. In this + case, the :class:`~asynctest.Mock` object behavior is the same as with an + :class:`.Mock` object: the wrapped object may have methods + defined as coroutine functions. + """ + #: Property which is set when the mock is awaited. Its ``wait`` and + #: ``wait_next`` coroutine methods can be used to synchronize execution. + #: + #: .. versionadded:: 0.12 + awaited = _delegating_property('awaited') + #: Number of times the mock has been awaited. + #: + #: .. versionadded:: 0.12 + await_count = _delegating_property('await_count') + await_args = _delegating_property('await_args') + await_args_list = _delegating_property('await_args_list') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # asyncio.iscoroutinefunction() checks this property to say if an + # object is a coroutine + # It is set through __dict__ because when spec_set is True, this + # attribute is likely undefined. + self.__dict__['_is_coroutine'] = _is_coroutine + self.__dict__['_mock_awaited'] = _AwaitEvent(self) + self.__dict__['_mock_await_count'] = 0 + self.__dict__['_mock_await_args'] = None + self.__dict__['_mock_await_args_list'] = _CallList() + + def _mock_call(_mock_self, *args, **kwargs): + try: + result = super()._mock_call(*args, **kwargs) + _call = _mock_self.call_args + + @asyncio.coroutine + def proxy(): + try: + if _isawaitable(result): + return (yield from result) + else: + return result + finally: + _mock_self.await_count += 1 + _mock_self.await_args = _call + _mock_self.await_args_list.append(_call) + yield from _mock_self.awaited._notify() + + return proxy() + except StopIteration as e: + side_effect = _mock_self.side_effect + if side_effect is not None and not callable(side_effect): + raise + + return asyncio.coroutine(_raise)(e) + except BaseException as e: + return asyncio.coroutine(_raise)(e) + + def assert_awaited(_mock_self): + """ + Assert that the mock was awaited at least once. + + .. versionadded:: 0.12 + """ + self = _mock_self + if self.await_count == 0: + msg = ("Expected '%s' to have been awaited." % + self._mock_name or 'mock') + raise AssertionError(msg) + + def assert_awaited_once(_mock_self, *args, **kwargs): + """ + Assert that the mock was awaited exactly once. + + .. versionadded:: 0.12 + """ + self = _mock_self + if not self.await_count == 1: + msg = ("Expected '%s' to have been awaited once. Awaited %s times." % + (self._mock_name or 'mock', self.await_count)) + raise AssertionError(msg) + + def assert_awaited_with(_mock_self, *args, **kwargs): + """ + Assert that the last await was with the specified arguments. + + .. versionadded:: 0.12 + """ + self = _mock_self + if self.await_args is None: + expected = self._format_mock_call_signature(args, kwargs) + raise AssertionError('Expected await: %s\nNot awaited' % (expected,)) + + def _error_message(): + msg = self._format_mock_failure_message(args, kwargs) + return msg + + expected = self._call_matcher((args, kwargs)) + actual = self._call_matcher(self.await_args) + if expected != actual: + cause = expected if isinstance(expected, Exception) else None + raise AssertionError(_error_message()) from cause + + def assert_awaited_once_with(_mock_self, *args, **kwargs): + """ + Assert that the mock was awaited exactly once and with the specified arguments. + + .. versionadded:: 0.12 + """ + self = _mock_self + if not self.await_count == 1: + msg = ("Expected '%s' to be awaited once. Awaited %s times." % + (self._mock_name or 'mock', self.await_count)) + raise AssertionError(msg) + return self.assert_awaited_with(*args, **kwargs) + + def assert_any_await(_mock_self, *args, **kwargs): + """ + Assert the mock has ever been awaited with the specified arguments. + + .. versionadded:: 0.12 + """ + self = _mock_self + expected = self._call_matcher((args, kwargs)) + actual = [self._call_matcher(c) for c in self.await_args_list] + if expected not in actual: + cause = expected if isinstance(expected, Exception) else None + expected_string = self._format_mock_call_signature(args, kwargs) + raise AssertionError( + '%s await not found' % expected_string + ) from cause + + def assert_has_awaits(_mock_self, calls, any_order=False): + """ + Assert the mock has been awaited with the specified calls. + The :attr:`await_args_list` list is checked for the awaits. + + If `any_order` is False (the default) then the awaits must be + sequential. There can be extra calls before or after the + specified awaits. + + If `any_order` is True then the awaits can be in any order, but + they must all appear in :attr:`await_args_list`. + + .. versionadded:: 0.12 + """ + self = _mock_self + expected = [self._call_matcher(c) for c in calls] + cause = expected if isinstance(expected, Exception) else None + all_awaits = _CallList(self._call_matcher(c) for c in self.await_args_list) + if not any_order: + if expected not in all_awaits: + raise AssertionError( + 'Awaits not found.\nExpected: %r\n' + 'Actual: %r' % (_CallList(calls), self.await_args_list) + ) from cause + return + + all_awaits = list(all_awaits) + + not_found = [] + for kall in expected: + try: + all_awaits.remove(kall) + except ValueError: + not_found.append(kall) + if not_found: + raise AssertionError( + '%r not all found in await list' % (tuple(not_found),) + ) from cause + + def assert_not_awaited(_mock_self): + """ + Assert that the mock was never awaited. + + .. versionadded:: 0.12 + """ + self = _mock_self + if self.await_count != 0: + msg = ("Expected '%s' to not have been awaited. Awaited %s times." % + (self._mock_name or 'mock', self.await_count)) + raise AssertionError(msg) + + def reset_mock(self, *args, **kwargs): + """ + See :func:`.Mock.reset_mock()` + """ + super().reset_mock(*args, **kwargs) + self.awaited = _AwaitEvent(self) + self.await_count = 0 + self.await_args = None + self.await_args_list = _CallList() def _dot_lookup(thing, comp, import_path): try: @@ -1730,7 +2084,7 @@ def _patch_stopall(): '__reduce__', '__reduce_ex__', '__getinitargs__', '__getnewargs__', '__getstate__', '__setstate__', '__getformat__', '__setformat__', '__repr__', '__dir__', '__subclasses__', '__format__', - '__getnewargs_ex__', + '__getnewargs_ex__', '__aenter__', '__aexit__', '__anext__', '__aiter__', } @@ -2156,6 +2510,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, spec = type(spec) is_type = isinstance(spec, type) + is_coroutine_func = asyncio.iscoroutinefunction(spec) _kwargs = {'spec': spec} if spec_set: @@ -2173,6 +2528,11 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # descriptors don't have a spec # because we don't know what type they return _kwargs = {} + elif is_coroutine_func: + if instance: + raise RuntimeError("Instance can not be True when create_autospec " + "is mocking a coroutine function") + Klass = CoroutineMock elif not _callable(spec): Klass = NonCallableMagicMock elif is_type and instance and not _instance_callable(spec): From b83e5a5a7b3facbf5c9aa31d4dc94e6f1ad34a0c Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Sun, 22 Jul 2018 16:26:02 -0700 Subject: [PATCH 02/24] Adding async support to the mock library. --- Lib/inspect.py | 10 +- Lib/unittest/mock.py | 642 +++++++++++++----------- Lib/unittest/test/testmock/testasync.py | 401 +++++++++++++++ Lib/unittest/test/testmock/testmock.py | 5 +- 4 files changed, 768 insertions(+), 290 deletions(-) create mode 100644 Lib/unittest/test/testmock/testasync.py diff --git a/Lib/inspect.py b/Lib/inspect.py index e799a83a74046f..c0b512f98c291a 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -176,13 +176,19 @@ def isgeneratorfunction(object): return bool((isfunction(object) or ismethod(object)) and object.__code__.co_flags & CO_GENERATOR) + +#### LISA this should be in another commit def iscoroutinefunction(object): """Return true if the object is a coroutine function. Coroutine functions are defined with "async def" syntax. """ - return bool((isfunction(object) or ismethod(object)) and - object.__code__.co_flags & CO_COROUTINE) + try: + return bool((isfunction(object) or ismethod(object)) and + object.__code__.co_flags & CO_COROUTINE) + except AttributeError: + pass # FIXME + def isasyncgenfunction(object): """Return true if the object is an asynchronous generator function. diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 3653e71a5ed8ae..65649054a9837e 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -30,7 +30,8 @@ import pprint import sys import builtins -from types import ModuleType +import fnmatch +from types import ModuleType, CodeType from functools import wraps, partial @@ -49,6 +50,11 @@ # Without this, the __class__ properties wouldn't be set correctly _safe_super = super + +def _is_coroutine_obj(obj): + return asyncio.iscoroutinefunction(obj) or asyncio.iscoroutine(obj) + + def _is_instance_mock(obj): # can't use isinstance on Mock objects because they override __class__ # The base class for all mocks is NonCallableMock @@ -358,57 +364,32 @@ def __init__(self, *args, **kwargs): pass -_is_coroutine = asyncio.iscoroutine -_isawaitable = inspect.isawaitable - -def _get_is_coroutine(self): - return self.__dict__['_mock_is_coroutine'] - - -def _set_is_coroutine(self, value): - # property setters and getters are overridden by Mock(), we need to - # update the dict to add values - value = _is_coroutine if bool(value) else False - self.__dict__['_mock_is_coroutine'] = value - - -class IsCoroutineMeta(type): - def __new__(meta, name, base, namespace): - namespace.update({ - '_get_is_coroutine': _get_is_coroutine, - '_set_is_coroutine': _set_is_coroutine, - 'is_coroutine': property(_get_is_coroutine, _set_is_coroutine, - doc="True if the object mocked is a coroutine"), - '_is_coroutine': property(_get_is_coroutine), - }) +_is_coroutine = asyncio.coroutines._is_coroutine - def __setattr__(self, name, value): - - if name == 'is_coroutine': - self._set_is_coroutine(value) - else: - return base[0].__setattr__(self, name, value) - - namespace['__setattr__'] = __setattr__ - - return super().__new__(meta, name, base, namespace) - - -class NonCallableMock(Base, metaclass=IsCoroutineMeta): +class NonCallableMock(Base): """A non-callable version of `Mock`""" def __new__(cls, *args, **kw): # every instance has its own class # so we can create magic methods on the # class without stomping on other mocks - new = type(cls.__name__, (cls,), {'__doc__': cls.__doc__}) + bases = (cls,) + if cls != CoroutineMock: + # Check if spec is a coroutine object or function + sig = inspect.signature(NonCallableMock.__init__) + bound_args = sig.bind_partial(cls, *args, **kw).arguments + spec_arg = fnmatch.filter(bound_args.keys(), 'spec*') + if spec_arg: + # what if spec_set is different than spec? + if _is_coroutine_obj(bound_args[spec_arg[0]]): + bases = (CoroutineMockMixin, cls,) + new = type(cls.__name__, bases, {'__doc__': cls.__doc__}) instance = object.__new__(new) return instance - def __init__( self, spec=None, wraps=None, name=None, spec_set=None, - parent=None, is_coroutine=None, _spec_state=None, _new_name='', + parent=None, _spec_state=None, _new_name='', _new_parent=None, _spec_as_instance=False, _eat_self=None, unsafe=False, **kwargs ): @@ -451,8 +432,6 @@ def __init__( _spec_state ) - self._set_is_coroutine(is_coroutine) - def attach_mock(self, mock, attribute): """ Attach a mock as an attribute of this one, replacing its name and @@ -479,24 +458,29 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False, _eat_self=False): _spec_class = None _spec_signature = None + _spec_coroutines = [] + + for attr in dir(spec): + if asyncio.iscoroutinefunction(getattr(spec, attr)): + _spec_coroutines.append(attr) if spec is not None and not _is_list(spec): - if isinstance(spec, type): - _spec_class = spec - else: - _spec_class = _get_class(spec) - res = _get_signature_object(spec, - _spec_as_instance, _eat_self) - _spec_signature = res and res[1] + if isinstance(spec, type): + _spec_class = spec + else: + _spec_class = _get_class(spec) + res = _get_signature_object(spec, + _spec_as_instance, _eat_self) + _spec_signature = res and res[1] - spec = dir(spec) + spec = dir(spec) __dict__ = self.__dict__ __dict__['_spec_class'] = _spec_class __dict__['_spec_set'] = spec_set __dict__['_spec_signature'] = _spec_signature __dict__['_mock_methods'] = spec - + __dict__['_spec_coroutines'] = _spec_coroutines def __get_return_value(self): ret = self._mock_return_value @@ -929,12 +913,21 @@ def _get_child_mock(self, **kw): child mocks are made. For non-callable mocks the callable variant will be used (rather than - any custom subclass).""" + any custom subclass). + """ + _new_name = kw.get("_new_name") + if _new_name in self.__dict__['_spec_coroutines']: + return CoroutineMock(**kw) + _type = type(self) + if issubclass(_type, MagicMock) and _new_name in async_magic_coroutines: + klass = CoroutineMock + if issubclass(_type, CoroutineMockMixin): + klass = MagicMock if not issubclass(_type, CallableMixin): if issubclass(_type, NonCallableMagicMock): klass = MagicMock - elif issubclass(_type, NonCallableMock) : + elif issubclass(_type, NonCallableMock): klass = Mock else: klass = _type.__mro__[1] @@ -1208,231 +1201,9 @@ class or instance) that acts as the specification for the mock object. If """ -class CoroutineMock(Mock): - """ - Enhance :class:`~asynctest.mock.Mock` with features allowing to mock - a coroutine function. - - The :class:`~asynctest.CoroutineMock` object will behave so the object is - recognized as coroutine function, and the result of a call as a coroutine: - - >>> mock = CoroutineMock() - >>> asyncio.iscoroutinefunction(mock) - True - >>> asyncio.iscoroutine(mock()) - True - - - The result of ``mock()`` is a coroutine which will have the outcome of - ``side_effect`` or ``return_value``: - - - if ``side_effect`` is a function, the coroutine will return the result - of that function, - - if ``side_effect`` is an exception, the coroutine will raise the - exception, - - if ``side_effect`` is an iterable, the coroutine will return the next - value of the iterable, however, if the sequence of result is exhausted, - ``StopIteration`` is raised immediately, - - if ``side_effect`` is not defined, the coroutine will return the value - defined by ``return_value``, hence, by default, the coroutine returns - a new :class:`~asynctest.CoroutineMock` object. - - If the outcome of ``side_effect`` or ``return_value`` is a coroutine, the - mock coroutine obtained when the mock object is called will be this - coroutine itself (and not a coroutine returning a coroutine). - - The test author can also specify a wrapped object with ``wraps``. In this - case, the :class:`~asynctest.Mock` object behavior is the same as with an - :class:`.Mock` object: the wrapped object may have methods - defined as coroutine functions. - """ - #: Property which is set when the mock is awaited. Its ``wait`` and - #: ``wait_next`` coroutine methods can be used to synchronize execution. - #: - #: .. versionadded:: 0.12 - awaited = _delegating_property('awaited') - #: Number of times the mock has been awaited. - #: - #: .. versionadded:: 0.12 - await_count = _delegating_property('await_count') - await_args = _delegating_property('await_args') - await_args_list = _delegating_property('await_args_list') - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # asyncio.iscoroutinefunction() checks this property to say if an - # object is a coroutine - # It is set through __dict__ because when spec_set is True, this - # attribute is likely undefined. - self.__dict__['_is_coroutine'] = _is_coroutine - self.__dict__['_mock_awaited'] = _AwaitEvent(self) - self.__dict__['_mock_await_count'] = 0 - self.__dict__['_mock_await_args'] = None - self.__dict__['_mock_await_args_list'] = _CallList() - - def _mock_call(_mock_self, *args, **kwargs): - try: - result = super()._mock_call(*args, **kwargs) - _call = _mock_self.call_args - - @asyncio.coroutine - def proxy(): - try: - if _isawaitable(result): - return (yield from result) - else: - return result - finally: - _mock_self.await_count += 1 - _mock_self.await_args = _call - _mock_self.await_args_list.append(_call) - yield from _mock_self.awaited._notify() - - return proxy() - except StopIteration as e: - side_effect = _mock_self.side_effect - if side_effect is not None and not callable(side_effect): - raise - - return asyncio.coroutine(_raise)(e) - except BaseException as e: - return asyncio.coroutine(_raise)(e) - - def assert_awaited(_mock_self): - """ - Assert that the mock was awaited at least once. - - .. versionadded:: 0.12 - """ - self = _mock_self - if self.await_count == 0: - msg = ("Expected '%s' to have been awaited." % - self._mock_name or 'mock') - raise AssertionError(msg) - - def assert_awaited_once(_mock_self, *args, **kwargs): - """ - Assert that the mock was awaited exactly once. - - .. versionadded:: 0.12 - """ - self = _mock_self - if not self.await_count == 1: - msg = ("Expected '%s' to have been awaited once. Awaited %s times." % - (self._mock_name or 'mock', self.await_count)) - raise AssertionError(msg) - - def assert_awaited_with(_mock_self, *args, **kwargs): - """ - Assert that the last await was with the specified arguments. - - .. versionadded:: 0.12 - """ - self = _mock_self - if self.await_args is None: - expected = self._format_mock_call_signature(args, kwargs) - raise AssertionError('Expected await: %s\nNot awaited' % (expected,)) - - def _error_message(): - msg = self._format_mock_failure_message(args, kwargs) - return msg +def _raise(exception): + raise exception - expected = self._call_matcher((args, kwargs)) - actual = self._call_matcher(self.await_args) - if expected != actual: - cause = expected if isinstance(expected, Exception) else None - raise AssertionError(_error_message()) from cause - - def assert_awaited_once_with(_mock_self, *args, **kwargs): - """ - Assert that the mock was awaited exactly once and with the specified arguments. - - .. versionadded:: 0.12 - """ - self = _mock_self - if not self.await_count == 1: - msg = ("Expected '%s' to be awaited once. Awaited %s times." % - (self._mock_name or 'mock', self.await_count)) - raise AssertionError(msg) - return self.assert_awaited_with(*args, **kwargs) - - def assert_any_await(_mock_self, *args, **kwargs): - """ - Assert the mock has ever been awaited with the specified arguments. - - .. versionadded:: 0.12 - """ - self = _mock_self - expected = self._call_matcher((args, kwargs)) - actual = [self._call_matcher(c) for c in self.await_args_list] - if expected not in actual: - cause = expected if isinstance(expected, Exception) else None - expected_string = self._format_mock_call_signature(args, kwargs) - raise AssertionError( - '%s await not found' % expected_string - ) from cause - - def assert_has_awaits(_mock_self, calls, any_order=False): - """ - Assert the mock has been awaited with the specified calls. - The :attr:`await_args_list` list is checked for the awaits. - - If `any_order` is False (the default) then the awaits must be - sequential. There can be extra calls before or after the - specified awaits. - - If `any_order` is True then the awaits can be in any order, but - they must all appear in :attr:`await_args_list`. - - .. versionadded:: 0.12 - """ - self = _mock_self - expected = [self._call_matcher(c) for c in calls] - cause = expected if isinstance(expected, Exception) else None - all_awaits = _CallList(self._call_matcher(c) for c in self.await_args_list) - if not any_order: - if expected not in all_awaits: - raise AssertionError( - 'Awaits not found.\nExpected: %r\n' - 'Actual: %r' % (_CallList(calls), self.await_args_list) - ) from cause - return - - all_awaits = list(all_awaits) - - not_found = [] - for kall in expected: - try: - all_awaits.remove(kall) - except ValueError: - not_found.append(kall) - if not_found: - raise AssertionError( - '%r not all found in await list' % (tuple(not_found),) - ) from cause - - def assert_not_awaited(_mock_self): - """ - Assert that the mock was never awaited. - - .. versionadded:: 0.12 - """ - self = _mock_self - if self.await_count != 0: - msg = ("Expected '%s' to not have been awaited. Awaited %s times." % - (self._mock_name or 'mock', self.await_count)) - raise AssertionError(msg) - - def reset_mock(self, *args, **kwargs): - """ - See :func:`.Mock.reset_mock()` - """ - super().reset_mock(*args, **kwargs) - self.awaited = _AwaitEvent(self) - self.await_count = 0 - self.await_args = None - self.await_args_list = _CallList() def _dot_lookup(thing, comp, import_path): try: @@ -1631,8 +1402,10 @@ def __enter__(self): if isinstance(original, type): # If we're patching out a class and there is a spec inherit = True - - Klass = MagicMock + if spec is None and _is_coroutine_obj(original): + Klass = CoroutineMock + else: + Klass = MagicMock _kwargs = {} if new_callable is not None: Klass = new_callable @@ -1644,7 +1417,9 @@ def __enter__(self): not_callable = '__call__' not in this_spec else: not_callable = not callable(this_spec) - if not_callable: + if _is_coroutine_obj(this_spec): + Klass = CoroutineMock + elif not_callable: Klass = NonCallableMagicMock if spec is not None: @@ -2101,6 +1876,14 @@ def method(self, *args, **kw): ' '.join([magic_methods, numerics, inplace, right]).split() } +async_magic_coroutines = ("__aenter__", "__aexit__", "__anext__") +_async_magics = async_magic_coroutines + ("__aiter__", ) + +async_magic_coroutines = set(async_magic_coroutines) +_async_magics = set(_async_magics) + + + _all_magics = _magics | _non_defaults _unsupported_magics = { @@ -2221,7 +2004,6 @@ def _mock_set_magics(self): setattr(_type, entry, MagicProxy(entry, self)) - class NonCallableMagicMock(MagicMixin, NonCallableMock): """A version of `MagicMock` that isn't callable.""" def mock_add_spec(self, spec, spec_set=False): @@ -2234,7 +2016,6 @@ def mock_add_spec(self, spec, spec_set=False): self._mock_set_magics() - class MagicMock(MagicMixin, Mock): """ MagicMock is a subclass of Mock with default implementations @@ -2279,6 +2060,264 @@ def __get__(self, obj, _type=None): return self.create_mock() +class AsyncMagicMixin(object): + def __init__(self, *args, **kw): + self._mock_set_async_magics() # make magic work for kwargs in init + _safe_super(AsyncMagicMixin, self).__init__(*args, **kw) + self._mock_set_async_magics() # fix magic broken by upper level init + + def _mock_set_async_magics(self): + these_magics = _async_magics + + if getattr(self, "_mock_methods", None) is not None: + these_magics = _async_magics.intersection(self._mock_methods) + remove_magics = _async_magics - these_magics + + for entry in remove_magics: + if entry in type(self).__dict__: + # remove unneeded magic methods + delattr(self, entry) + + # don't overwrite existing attributes if called a second time + these_magics = these_magics - set(type(self).__dict__) + + _type = type(self) + for entry in these_magics: + setattr(_type, entry, MagicProxy(entry, self)) + + +class CoroutineMockMixin(Base): + #: Property which is set when the mock is awaited. Its ``wait`` and + #: ``wait_next`` coroutine methods can be used to synchronize execution. + #: + #: .. versionadded:: 0.12 + awaited = _delegating_property('awaited') + #: Number of times the mock has been awaited. + #: + #: .. versionadded:: 0.12 + await_count = _delegating_property('await_count') + await_args = _delegating_property('await_args') + await_args_list = _delegating_property('await_args_list') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # asyncio.iscoroutinefunction() checks this property to say if an + # object is a coroutine + # It is set through __dict__ because when spec_set is True, this + # attribute is likely undefined. + self.__dict__['_is_coroutine'] = _is_coroutine + self.__dict__['_mock_awaited'] = _AwaitEvent(self) + 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 = 0 + self.__dict__['__code__'] = code_mock + + def _mock_call(_mock_self, *args, **kwargs): + try: + result = super()._mock_call(*args, **kwargs) + _call = _mock_self.call_args + + @asyncio.coroutine + def proxy(): + try: + if asyncio.iscoroutine(result): + return (yield from result) + else: + return result + finally: + _mock_self.await_count += 1 + _mock_self.await_args = _call + _mock_self.await_args_list.append(_call) + yield from _mock_self.awaited._notify() + + return proxy() + except StopIteration as e: + side_effect = _mock_self.side_effect + if side_effect is not None and not callable(side_effect): + raise + + return asyncio.coroutine(_raise)(e) + except BaseException as e: + return asyncio.coroutine(_raise)(e) + + def assert_awaited(_mock_self): + """ + Assert that the mock was awaited at least once. + + .. versionadded:: 0.12 + """ + self = _mock_self + if self.await_count == 0: + msg = f"Expected {self._mock_name or 'mock'} to have been awaited." + raise AssertionError(msg) + + def assert_awaited_once(_mock_self, *args, **kwargs): + """ + Assert that the mock was awaited exactly once. + + .. versionadded:: 0.12 + """ + self = _mock_self + if not self.await_count == 1: + msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once.", + f" Awaited {self.await_count} times.") + raise AssertionError(msg) + + def assert_awaited_with(_mock_self, *args, **kwargs): + """ + Assert that the last await was with the specified arguments. + + .. versionadded:: 0.12 + """ + self = _mock_self + if self.await_args is None: + expected = self._format_mock_call_signature(args, kwargs) + raise AssertionError(f'Expected await: {expected}\nNot awaited') + + def _error_message(): + msg = self._format_mock_failure_message(args, kwargs) + return msg + + expected = self._call_matcher((args, kwargs)) + actual = self._call_matcher(self.await_args) + if expected != actual: + cause = expected if isinstance(expected, Exception) else None + raise AssertionError(_error_message()) from cause + + def assert_awaited_once_with(_mock_self, *args, **kwargs): + """ + Assert that the mock was awaited exactly once and with the specified + arguments. + + .. versionadded:: 0.12 + """ + self = _mock_self + if not self.await_count == 1: + msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once.", + f" Awaited {self.await_count} times.") + raise AssertionError(msg) + return self.assert_awaited_with(*args, **kwargs) + + def assert_any_await(_mock_self, *args, **kwargs): + """ + Assert the mock has ever been awaited with the specified arguments. + + .. versionadded:: 0.12 + """ + self = _mock_self + expected = self._call_matcher((args, kwargs)) + actual = [self._call_matcher(c) for c in self.await_args_list] + if expected not in actual: + cause = expected if isinstance(expected, Exception) else None + expected_string = self._format_mock_call_signature(args, kwargs) + raise AssertionError( + '%s await not found' % expected_string + ) from cause + + def assert_has_awaits(_mock_self, calls, any_order=False): + """ + Assert the mock has been awaited with the specified calls. + The :attr:`await_args_list` list is checked for the awaits. + + If `any_order` is False (the default) then the awaits must be + sequential. There can be extra calls before or after the + specified awaits. + + If `any_order` is True then the awaits can be in any order, but + they must all appear in :attr:`await_args_list`. + + .. versionadded:: 0.12 + """ + self = _mock_self + expected = [self._call_matcher(c) for c in calls] + cause = expected if isinstance(expected, Exception) else None + all_awaits = _CallList(self._call_matcher(c) for c in self.await_args_list) + if not any_order: + if expected not in all_awaits: + raise AssertionError( + f'Awaits not found.\nExpected: {_CallList(calls)}\n', + f'Actual: {self.await_args_list}' + ) from cause + return + + all_awaits = list(all_awaits) + + not_found = [] + for kall in expected: + try: + all_awaits.remove(kall) + except ValueError: + not_found.append(kall) + if not_found: + raise AssertionError( + '%r not all found in await list' % (tuple(not_found),) + ) from cause + + def assert_not_awaited(_mock_self): + """ + Assert that the mock was never awaited. + + .. versionadded:: 0.12 + """ + self = _mock_self + if self.await_count != 0: + msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once.", + f" Awaited {self.await_count} times.") + raise AssertionError(msg) + + def reset_mock(self, *args, **kwargs): + """ + See :func:`.Mock.reset_mock()` + """ + super().reset_mock(*args, **kwargs) + self.awaited = _AwaitEvent(self) + self.await_count = 0 + self.await_args = None + self.await_args_list = _CallList() + + +class CoroutineMock(CoroutineMockMixin, AsyncMagicMixin, Mock): + """ + Enhance :class:`Mock` with features allowing to mock + a coroutine function. + + The :class:`CoroutineMock` object will behave so the object is + recognized as coroutine function, and the result of a call as a coroutine: + + >>> mock = CoroutineMock() + >>> asyncio.iscoroutinefunction(mock) + True + >>> asyncio.iscoroutine(mock()) + True + + + The result of ``mock()`` is a coroutine which will have the outcome of + ``side_effect`` or ``return_value``: + + - if ``side_effect`` is a function, the coroutine will return the result + of that function, + - if ``side_effect`` is an exception, the coroutine will raise the + exception, + - if ``side_effect`` is an iterable, the coroutine will return the next + value of the iterable, however, if the sequence of result is exhausted, + ``StopIteration`` is raised immediately, + - if ``side_effect`` is not defined, the coroutine will return the value + defined by ``return_value``, hence, by default, the coroutine returns + a new :class:`CoroutineMock` object. + + If the outcome of ``side_effect`` or ``return_value`` is a coroutine, the + mock coroutine obtained when the mock object is called will be this + coroutine itself (and not a coroutine returning a coroutine). + + The test author can also specify a wrapped object with ``wraps``. In this + case, the :class:`Mock` object behavior is the same as with an + :class:`.Mock` object: the wrapped object may have methods + defined as coroutine functions. + """ + class _ANY(object): "A helper object that compares equal to everything." @@ -2511,7 +2550,6 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, is_type = isinstance(spec, type) is_coroutine_func = asyncio.iscoroutinefunction(spec) - _kwargs = {'spec': spec} if spec_set: _kwargs = {'spec_set': spec} @@ -2551,7 +2589,35 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, if isinstance(spec, FunctionTypes): # should only happen at the top level because we don't # recurse for functions + + # LISA check if the orginal object is a coroutines + # if it is set the _is_coroutine to True + # somewhere around here + # We need to ensure that the child function is a coroutine function, + # without acting like a function mock = _set_signature(mock, spec) + if is_coroutine_func: + # Can't wrap the mock with asyncio.coroutine because it doesn't + # detect a CoroWrapper as an awaitable in debug mode. + # It is safe to do so because the mock object wrapped by + # _set_signature returns the result of the CoroutineMock itself, + # which is a Coroutine (as defined in CoroutineMock._mock_call) + mock._is_coroutine = _is_coroutine + mock.awaited = _AwaitEvent(mock) + mock.await_count = 0 + mock.await_args = None + mock.await_args_list = _CallList() + + for a in ('assert_awaited', + 'assert_awaited_once', + 'assert_awaited_with', + 'assert_awaited_once_with', + 'assert_any_await', + 'assert_has_awaits', + 'assert_not_awaited'): + def f(*args, **kwargs): + return getattr(wrapped_mock, a)(*args, **kwargs) + setattr(mock, a, f) else: _check_signature(spec, mock, is_type, instance) @@ -2595,9 +2661,13 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, skipfirst = _must_skip(spec, entry, is_type) kwargs['_eat_self'] = skipfirst - new = MagicMock(parent=parent, name=entry, _new_name=entry, - _new_parent=parent, - **kwargs) + if asyncio.iscoroutinefunction(original): + child_klass = CoroutineMock + else: + child_klass = MagicMock + new = child_klass(parent=parent, name=entry, _new_name=entry, + _new_parent=parent, + **kwargs) mock._mock_children[entry] = new _check_signature(original, new, skipfirst=skipfirst) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py new file mode 100644 index 00000000000000..2df3a02e41f091 --- /dev/null +++ b/Lib/unittest/test/testmock/testasync.py @@ -0,0 +1,401 @@ +import asyncio +import unittest + +from unittest.mock import call, CoroutineMock, patch, MagicMock + + +class AsyncFoo(object): + def __init__(self, a): + pass + async def coroutine_method(self): + pass + @asyncio.coroutine + def decorated_cr_method(self): + pass + +async def coroutine_func(): + pass + +class NormalFoo(object): + def a(self): + pass + + +async_foo_name = f'{__name__}.AsyncFoo' +normal_foo_name = f'{__name__}.NormalFoo' + + +def run_coroutine(coroutine): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(coroutine) + finally: + loop.close() + + +class CoroutinePatchDecoratorTest(unittest.TestCase): + def test_is_coroutine_function_patch(self): + @patch.object(AsyncFoo, 'coroutine_method') + def test_async(mock_method): + self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + + @patch.object(AsyncFoo, 'decorated_cr_method') + def test_async_decorator(mock_g): + self.assertTrue(asyncio.iscoroutinefunction(mock_g)) + + test_async() + test_async_decorator() + + def test_is_coroutine_patch(self): + @patch.object(AsyncFoo, 'coroutine_method') + def test_async(mock_method): + self.assertTrue(asyncio.iscoroutine(mock_method())) + + @patch.object(AsyncFoo, 'decorated_cr_method') + def test_async_decorator(mock_method): + self.assertTrue(asyncio.iscoroutine(mock_method())) + + @patch(f'{async_foo_name}.coroutine_method') + def test_no_parent_attribute(mock_method): + self.assertTrue(asyncio.iscoroutine(mock_method())) + + test_async() + test_async_decorator() + test_no_parent_attribute() + + def test_is_coroutinemock_patch(self): + @patch.object(AsyncFoo, 'coroutine_method') + def test_async(mock_method): + self.assertTrue(isinstance(mock_method, CoroutineMock)) + + @patch.object(AsyncFoo, 'decorated_cr_method') + def test_async_decorator(mock_method): + self.assertTrue(isinstance(mock_method, CoroutineMock)) + + test_async() + test_async_decorator() + + +class CoroutinePatchCMTest(unittest.TestCase): + def test_is_coroutine_function_cm(self): + def test_async(): + with patch.object(AsyncFoo, 'coroutine_method') as mock_method: + self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + + def test_async_decorator(): + with patch.object(AsyncFoo, 'decorated_cr_method') as mock_method: + self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + + test_async() + test_async_decorator() + + def test_is_coroutine_cm(self): + def test_async(): + with patch.object(AsyncFoo, 'coroutine_method') as mock_method: + self.assertTrue(asyncio.iscoroutine(mock_method())) + + def test_async_decorator(): + with patch.object(AsyncFoo, 'decorated_cr_method') as mock_method: + self.assertTrue(asyncio.iscoroutine(mock_method())) + test_async() + test_async_decorator() + + def test_is_coroutinemock_cm(self): + def test_async(): + with patch.object(AsyncFoo, 'coroutine_method') as mock_method: + self.assertTrue(isinstance(mock_method, CoroutineMock)) + + def test_async_decorator(): + with patch.object(AsyncFoo, 'decorated_cr_method') as mock_method: + self.assertTrue(isinstance(mock_method, CoroutineMock)) + + test_async() + test_async_decorator() + + +class CoroutineMockTest(unittest.TestCase): + def test_iscoroutinefunction(self): + mock = CoroutineMock() + self.assertTrue(asyncio.iscoroutinefunction(mock)) + + def test_iscoroutine(self): + mock = CoroutineMock() + self.assertTrue(asyncio.iscoroutine(mock())) + self.assertIn('assert_awaited', dir(mock)) + + # Should I test making a non-coroutine a coroutine mock? + + +class CoroutineAutospecTest(unittest.TestCase): + def test_is_coroutinemock_patch(self): + @patch(async_foo_name, autospec=True) + def test_async(mock_method): + self.assertTrue(isinstance( + mock_method.decorated_cr_method, + CoroutineMock)) + self.assertTrue(isinstance(mock_method, MagicMock)) + + @patch(async_foo_name, autospec=True) + def test_async_decorator(mock_method): + self.assertTrue(isinstance( + mock_method.decorated_cr_method, + CoroutineMock)) + self.assertTrue(isinstance(mock_method, MagicMock)) + + test_async() + test_async_decorator() + + +class CoroutineSpecTest(unittest.TestCase): + def test_spec_as_coroutine_positional(self): + mock = MagicMock(coroutine_func) + self.assertIsInstance(mock, MagicMock) # Is this what we want? + self.assertTrue(asyncio.iscoroutine(mock())) + + def test_spec_as_coroutine_kw(self): + mock = MagicMock(spec=coroutine_func) + self.assertIsInstance(mock, MagicMock) + self.assertTrue(asyncio.iscoroutine(mock())) + + def test_spec_coroutine_mock(self): + @patch.object(AsyncFoo, 'coroutine_method', spec=True) + def test_async(mock_method): + self.assertTrue(isinstance(mock_method, CoroutineMock)) + + test_async() + + def test_spec_parent_not_coroutine_attribute_is(self): + @patch(async_foo_name, spec=True) + def test_async(mock_method): + self.assertTrue(isinstance(mock_method, MagicMock)) + self.assertTrue(isinstance(mock_method.coroutine_method, + CoroutineMock)) + + test_async() + + def test_target_coroutine_spec_not(self): + @patch.object(AsyncFoo, 'coroutine_method', spec=NormalFoo.a) + def test_async_attribute(mock_method): + self.assertTrue(isinstance(mock_method, MagicMock)) + + test_async_attribute() + + def test_target_not_coroutine_spec_is(self): + @patch.object(NormalFoo, 'a', spec=coroutine_func) + def test_attribute_not_coroutine_spec_is(mock_coroutine_func): + self.assertTrue(isinstance(mock_coroutine_func, CoroutineMock)) + # should check here is there is coroutine functionality + test_attribute_not_coroutine_spec_is() + + def test_spec_coroutine_attributes(self): + @patch(normal_foo_name, spec=AsyncFoo) + def test_coroutine_attributes_coroutines(MockNormalFoo): + self.assertTrue(isinstance(MockNormalFoo.coroutine_method, + CoroutineMock)) + self.assertTrue(isinstance(MockNormalFoo, MagicMock)) + + test_coroutine_attributes_coroutines() + + +class CoroutineSpecSetTest(unittest.TestCase): + def test_is_coroutinemock_patch(self): + @patch.object(AsyncFoo, 'coroutine_method', spec_set=True) + def test_async(coroutine_method): + self.assertTrue(isinstance(coroutine_method, CoroutineMock)) + + def test_is_coroutine_coroutinemock(self): + mock = CoroutineMock(spec_set=AsyncFoo.coroutine_method) + self.assertTrue(asyncio.iscoroutinefunction(mock)) + self.assertTrue(isinstance(mock, CoroutineMock)) + + def test_is_child_coroutinemock(self): + mock = MagicMock(spec_set=AsyncFoo) + self.assertTrue(asyncio.iscoroutinefunction(mock.coroutine_method)) + self.assertTrue(isinstance(mock.coroutine_method, CoroutineMock)) + self.assertTrue(isinstance(mock, MagicMock)) + + +class CoroutineMagicMethods(unittest.TestCase): + class AsyncContextManager: + def __init__(self): + self.entered = False + self.exited = False + + async def __aenter__(self, *args, **kwargs): + self.entered = True + return self + + async def __aexit__(self, *args, **kwargs): + self.exited = True + + class AsyncIterator: + def __init__(self): + self.iter_called = False + self.next_called = False + self.items = ["foo", "NormalFoo", "baz"] + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return self.items.pop() + except IndexError: + pass + + raise StopAsyncIteration + + def test_mock_magic_methods_are_coroutine_mocks(self): + mock_instance = CoroutineMock(spec=self.AsyncContextManager()) + self.assertIsInstance(mock_instance.__aenter__, + CoroutineMock) + self.assertIsInstance(mock_instance.__aexit__, + CoroutineMock) + + def test_mock_aiter_and_anext(self): + instance = self.AsyncIterator() + mock_instance = CoroutineMock(instance) + + self.assertEqual(asyncio.iscoroutine(instance.__aiter__), + asyncio.iscoroutine(mock_instance.__aiter__)) + self.assertEqual(asyncio.iscoroutine(instance.__anext__), + asyncio.iscoroutine(mock_instance.__anext__)) + + iterator = instance.__aiter__() + if asyncio.iscoroutine(iterator): + iterator = run_coroutine(iterator) + + mock_iterator = mock_instance.__aiter__() + if asyncio.iscoroutine(mock_iterator): + mock_iterator = run_coroutine(mock_iterator) + + self.assertEqual(asyncio.iscoroutine(iterator.__aiter__), + asyncio.iscoroutine(mock_iterator.__aiter__)) + self.assertEqual(asyncio.iscoroutine(iterator.__anext__), + asyncio.iscoroutine(mock_iterator.__anext__)) + + +class CoroutineMockAssert(unittest.TestCase): + @asyncio.coroutine + def test_assert_awaited(self): + mock = CoroutineMock() + + with self.assertRaises(AssertionError): + mock.assert_awaited() + + yield from mock() + mock.assert_awaited() + + @asyncio.coroutine + def test_assert_awaited_once(self): + mock = CoroutineMock() + + with self.assertRaises(AssertionError): + mock.assert_awaited_once() + + yield from mock() + mock.assert_awaited_once() + + yield from mock() + with self.assertRaises(AssertionError): + mock.assert_awaited_once() + + @asyncio.coroutine + def test_assert_awaited_with(self): + mock = CoroutineMock() + + with self.assertRaises(AssertionError): + mock.assert_awaited_with('foo') + + yield from mock('foo') + mock.assert_awaited_with('foo') + + yield from mock('NormalFoo') + with self.assertRaises(AssertionError): + mock.assert_awaited_with('foo') + + @asyncio.coroutine + def test_assert_awaited_once_with(self): + mock = CoroutineMock() + + with self.assertRaises(AssertionError): + mock.assert_awaited_once_with('foo') + + yield from mock('foo') + mock.assert_awaited_once_with('foo') + + yield from mock('foo') + with self.assertRaises(AssertionError): + mock.assert_awaited_once_with('foo') + + @asyncio.coroutine + def test_assert_any_wait(self): + mock = CoroutineMock() + + with self.assertRaises(AssertionError): + mock.assert_any_await('NormalFoo') + + yield from mock('foo') + with self.assertRaises(AssertionError): + mock.assert_any_await('NormalFoo') + + yield from mock('NormalFoo') + mock.assert_any_await('NormalFoo') + + yield from mock('baz') + mock.assert_any_await('NormalFoo') + + @asyncio.coroutine + def test_assert_has_awaits(self): + calls = [call('NormalFoo'), call('baz')] + + with self.subTest('any_order=False'): + mock = CoroutineMock() + + with self.assertRaises(AssertionError): + mock.assert_has_awaits(calls) + + yield from mock('foo') + with self.assertRaises(AssertionError): + mock.assert_has_awaits(calls) + + yield from mock('NormalFoo') + with self.assertRaises(AssertionError): + mock.assert_has_awaits(calls) + + yield from mock('baz') + mock.assert_has_awaits(calls) + + yield from mock('qux') + mock.assert_has_awaits(calls) + + with self.subTest('any_order=True'): + mock = CoroutineMock() + + with self.assertRaises(AssertionError): + mock.assert_has_awaits(calls, any_order=True) + + yield from mock('baz') + with self.assertRaises(AssertionError): + mock.assert_has_awaits(calls, any_order=True) + + yield from mock('foo') + with self.assertRaises(AssertionError): + mock.assert_has_awaits(calls, any_order=True) + + yield from mock('NormalFoo') + mock.assert_has_awaits(calls, any_order=True) + + yield from mock('qux') + mock.assert_has_awaits(calls, any_order=True) + + @asyncio.coroutine + def test_assert_not_awaited(self): + mock = CoroutineMock() + + mock.assert_not_awaited() + + yield from mock() + with self.assertRaises(AssertionError): + mock.assert_not_awaited() diff --git a/Lib/unittest/test/testmock/testmock.py b/Lib/unittest/test/testmock/testmock.py index b64c8663d21226..75f174b8aa3928 100644 --- a/Lib/unittest/test/testmock/testmock.py +++ b/Lib/unittest/test/testmock/testmock.py @@ -8,7 +8,7 @@ from unittest.mock import ( call, DEFAULT, patch, sentinel, MagicMock, Mock, NonCallableMock, - NonCallableMagicMock, _CallList, + NonCallableMagicMock, CoroutineMock, _CallList, create_autospec ) @@ -1377,7 +1377,8 @@ def test_mock_add_spec_magic_methods(self): def test_adding_child_mock(self): - for Klass in NonCallableMock, Mock, MagicMock, NonCallableMagicMock: + for Klass in (NonCallableMock, Mock, MagicMock, NonCallableMagicMock, + CoroutineMock): mock = Klass() mock.foo = Mock() From a9ea983a3997a465183008b01d3f7027db6dbc19 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 15 Aug 2018 17:29:15 -0700 Subject: [PATCH 03/24] Removes superfluous changes. --- Lib/unittest/mock.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 65649054a9837e..ac3a06145428de 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -257,7 +257,7 @@ def __init__(self): def __getattr__(self, name): if name == '__bases__': - # Without this help() raises an exception + # Without this help(unittest.mock) raises an exception raise AttributeError return self._sentinels.setdefault(name, _SentinelObject(name)) @@ -465,15 +465,15 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False, _spec_coroutines.append(attr) if spec is not None and not _is_list(spec): - if isinstance(spec, type): - _spec_class = spec - else: - _spec_class = _get_class(spec) - res = _get_signature_object(spec, - _spec_as_instance, _eat_self) - _spec_signature = res and res[1] + if isinstance(spec, type): + _spec_class = spec + else: + _spec_class = _get_class(spec) + res = _get_signature_object(spec, + _spec_as_instance, _eat_self) + _spec_signature = res and res[1] - spec = dir(spec) + spec = dir(spec) __dict__ = self.__dict__ __dict__['_spec_class'] = _spec_class @@ -913,8 +913,7 @@ def _get_child_mock(self, **kw): child mocks are made. For non-callable mocks the callable variant will be used (rather than - any custom subclass). - """ + any custom subclass).""" _new_name = kw.get("_new_name") if _new_name in self.__dict__['_spec_coroutines']: return CoroutineMock(**kw) @@ -1149,6 +1148,7 @@ def _mock_call(_mock_self, *args, **kwargs): return ret_val + class Mock(CallableMixin, NonCallableMock): """ Create a new `Mock` object. `Mock` takes several optional arguments @@ -2004,6 +2004,7 @@ def _mock_set_magics(self): setattr(_type, entry, MagicProxy(entry, self)) + class NonCallableMagicMock(MagicMixin, NonCallableMock): """A version of `MagicMock` that isn't callable.""" def mock_add_spec(self, spec, spec_set=False): @@ -2016,6 +2017,7 @@ def mock_add_spec(self, spec, spec_set=False): self._mock_set_magics() + class MagicMock(MagicMixin, Mock): """ MagicMock is a subclass of Mock with default implementations From 50581e3c92cf91a8406939ed79f0312937966c08 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 15 Aug 2018 17:50:02 -0700 Subject: [PATCH 04/24] Cleans up comments. --- Lib/inspect.py | 3 +-- Lib/unittest/mock.py | 21 --------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index c0b512f98c291a..90d331e9d48cd3 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -177,7 +177,6 @@ def isgeneratorfunction(object): object.__code__.co_flags & CO_GENERATOR) -#### LISA this should be in another commit def iscoroutinefunction(object): """Return true if the object is a coroutine function. @@ -187,7 +186,7 @@ def iscoroutinefunction(object): return bool((isfunction(object) or ismethod(object)) and object.__code__.co_flags & CO_COROUTINE) except AttributeError: - pass # FIXME + pass def isasyncgenfunction(object): diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index ac3a06145428de..3b3c8e9ddb29b5 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2089,14 +2089,7 @@ def _mock_set_async_magics(self): class CoroutineMockMixin(Base): - #: Property which is set when the mock is awaited. Its ``wait`` and - #: ``wait_next`` coroutine methods can be used to synchronize execution. - #: - #: .. versionadded:: 0.12 awaited = _delegating_property('awaited') - #: Number of times the mock has been awaited. - #: - #: .. versionadded:: 0.12 await_count = _delegating_property('await_count') await_args = _delegating_property('await_args') await_args_list = _delegating_property('await_args_list') @@ -2148,8 +2141,6 @@ def proxy(): def assert_awaited(_mock_self): """ Assert that the mock was awaited at least once. - - .. versionadded:: 0.12 """ self = _mock_self if self.await_count == 0: @@ -2159,8 +2150,6 @@ def assert_awaited(_mock_self): def assert_awaited_once(_mock_self, *args, **kwargs): """ Assert that the mock was awaited exactly once. - - .. versionadded:: 0.12 """ self = _mock_self if not self.await_count == 1: @@ -2171,8 +2160,6 @@ def assert_awaited_once(_mock_self, *args, **kwargs): def assert_awaited_with(_mock_self, *args, **kwargs): """ Assert that the last await was with the specified arguments. - - .. versionadded:: 0.12 """ self = _mock_self if self.await_args is None: @@ -2193,8 +2180,6 @@ def assert_awaited_once_with(_mock_self, *args, **kwargs): """ Assert that the mock was awaited exactly once and with the specified arguments. - - .. versionadded:: 0.12 """ self = _mock_self if not self.await_count == 1: @@ -2206,8 +2191,6 @@ def assert_awaited_once_with(_mock_self, *args, **kwargs): def assert_any_await(_mock_self, *args, **kwargs): """ Assert the mock has ever been awaited with the specified arguments. - - .. versionadded:: 0.12 """ self = _mock_self expected = self._call_matcher((args, kwargs)) @@ -2230,8 +2213,6 @@ def assert_has_awaits(_mock_self, calls, any_order=False): If `any_order` is True then the awaits can be in any order, but they must all appear in :attr:`await_args_list`. - - .. versionadded:: 0.12 """ self = _mock_self expected = [self._call_matcher(c) for c in calls] @@ -2261,8 +2242,6 @@ def assert_has_awaits(_mock_self, calls, any_order=False): def assert_not_awaited(_mock_self): """ Assert that the mock was never awaited. - - .. versionadded:: 0.12 """ self = _mock_self if self.await_count != 0: From 96ddb0e32945f835f01a16f6969d533ea9952b77 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Mon, 10 Sep 2018 15:49:04 -0700 Subject: [PATCH 05/24] Fixes inspect and attribute error issues. --- Lib/inspect.py | 7 ++----- Lib/unittest/mock.py | 20 +++++++++++++++----- Lib/unittest/test/testmock/testasync.py | 10 +++++++++- Lib/unittest/test/testmock/testcallable.py | 7 +++++++ Lib/unittest/test/testmock/testhelpers.py | 1 - 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 90d331e9d48cd3..d3931ba0deb9e7 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -182,11 +182,8 @@ def iscoroutinefunction(object): Coroutine functions are defined with "async def" syntax. """ - try: - return bool((isfunction(object) or ismethod(object)) and - object.__code__.co_flags & CO_COROUTINE) - except AttributeError: - pass + return bool((isfunction(object) or ismethod(object)) and + object.__code__.co_flags & CO_COROUTINE) def isasyncgenfunction(object): diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 3b3c8e9ddb29b5..43f116c3fa7b5b 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -52,7 +52,10 @@ def _is_coroutine_obj(obj): - return asyncio.iscoroutinefunction(obj) or asyncio.iscoroutine(obj) + if getattr(obj, '__code__', None): + return asyncio.iscoroutinefunction(obj) or asyncio.iscoroutine(obj) + else: + return False def _is_instance_mock(obj): @@ -461,7 +464,7 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False, _spec_coroutines = [] for attr in dir(spec): - if asyncio.iscoroutinefunction(getattr(spec, attr)): + if asyncio.iscoroutinefunction(getattr(spec, attr, None)): _spec_coroutines.append(attr) if spec is not None and not _is_list(spec): @@ -1054,7 +1057,10 @@ def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, parent=None, _spec_state=None, _new_name='', _new_parent=None, **kwargs): self.__dict__['_mock_return_value'] = return_value - + # Makes inspect.iscoroutinefunction() return False when testing a Mock. + code_mock = NonCallableMock(spec_set=CodeType) + code_mock.co_flags = 0 + self.__dict__['__code__'] = code_mock _safe_super(CallableMixin, self).__init__( spec, wraps, name, spec_set, parent, _spec_state, _new_name, _new_parent, **kwargs @@ -2107,9 +2113,10 @@ def __init__(self, *args, **kwargs): self.__dict__['_mock_await_args'] = None self.__dict__['_mock_await_args_list'] = _CallList() code_mock = NonCallableMock(spec_set=CodeType) - code_mock.co_flags = 0 + code_mock.co_flags = 129 self.__dict__['__code__'] = code_mock + def _mock_call(_mock_self, *args, **kwargs): try: result = super()._mock_call(*args, **kwargs) @@ -2530,7 +2537,10 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, spec = type(spec) is_type = isinstance(spec, type) - is_coroutine_func = asyncio.iscoroutinefunction(spec) + if getattr(spec, '__code__', None): + is_coroutine_func = asyncio.iscoroutinefunction(spec) + else: + is_coroutine_func = False _kwargs = {'spec': spec} if spec_set: _kwargs = {'spec_set': spec} diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 2df3a02e41f091..e5a33c2a2bcf0c 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -1,4 +1,5 @@ import asyncio +import inspect import unittest from unittest.mock import call, CoroutineMock, patch, MagicMock @@ -115,10 +116,16 @@ def test_async_decorator(): class CoroutineMockTest(unittest.TestCase): - def test_iscoroutinefunction(self): + def test_iscoroutinefunction_default(self): mock = CoroutineMock() self.assertTrue(asyncio.iscoroutinefunction(mock)) + def test_iscoroutinefunction_function(self): + async def foo(): pass + mock = CoroutineMock(foo) + self.assertTrue(asyncio.iscoroutinefunction(mock)) + self.assertTrue(inspect.iscoroutinefunction(mock)) + def test_iscoroutine(self): mock = CoroutineMock() self.assertTrue(asyncio.iscoroutine(mock())) @@ -178,6 +185,7 @@ def test_target_coroutine_spec_not(self): @patch.object(AsyncFoo, 'coroutine_method', spec=NormalFoo.a) def test_async_attribute(mock_method): self.assertTrue(isinstance(mock_method, MagicMock)) + self.assertFalse(inspect.iscoroutinefunction(mock_method)) test_async_attribute() diff --git a/Lib/unittest/test/testmock/testcallable.py b/Lib/unittest/test/testmock/testcallable.py index af1ce7ebbae4da..0f0d99f43a7db2 100644 --- a/Lib/unittest/test/testmock/testcallable.py +++ b/Lib/unittest/test/testmock/testcallable.py @@ -2,6 +2,7 @@ # E-mail: fuzzyman AT voidspace DOT org DOT uk # http://www.voidspace.org.uk/python/mock/ +import inspect import unittest from unittest.test.testmock.support import is_instance, X, SomeClass @@ -146,6 +147,12 @@ def test_create_autospec_instance(self): self.assertRaises(TypeError, mock.wibble, 'some', 'args') + def test_inspect_iscoroutine_function(self): + def foo(): pass + + mock = Mock(foo) + self.assertFalse(inspect.iscoroutine(mock)) + if __name__ == "__main__": unittest.main() diff --git a/Lib/unittest/test/testmock/testhelpers.py b/Lib/unittest/test/testmock/testhelpers.py index 7919482ae99c7f..2e19a60001aacd 100644 --- a/Lib/unittest/test/testmock/testhelpers.py +++ b/Lib/unittest/test/testmock/testhelpers.py @@ -698,7 +698,6 @@ class RaiserClass(object): @staticmethod def existing(a, b): return a + b - s = create_autospec(RaiserClass) self.assertRaises(TypeError, lambda x: s.existing(1, 2, 3)) s.existing(1, 2) From a4d4dbceefd66b3485679d5b2dc13ef19b56ce49 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Tue, 11 Sep 2018 15:59:23 -0700 Subject: [PATCH 06/24] Fixes test_unittest changing env because of version issue. --- Lib/unittest/mock.py | 32 +++++++++++++++++++++++++ Lib/unittest/test/testmock/testasync.py | 29 +++++++++++++--------- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 43f116c3fa7b5b..8b45a7e08fc494 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1920,6 +1920,22 @@ def method(self, *args, **kw): '__index__': 1, } +class AsyncIterator: + """ + Wraps an iterator in an asynchronous iterator. + """ + def __init__(self, iterator): + self.iterator = iterator + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self.iterator) + except StopIteration: + pass + raise StopAsyncIteration def _get_eq(self): def __eq__(other): @@ -1950,10 +1966,26 @@ def __iter__(): return iter(ret_val) return __iter__ +def _get_async_iter(mock): + def __aiter__(): + return_value = mock.__aiter__._mock_return_value + if return_value is DEFAULT: + iterator = iter([]) + else: + iterator = iter(return_value) + + return AsyncIterator(iterator) + + if asyncio.iscoroutinefunction(mock.__aiter__): + return asyncio.coroutine(__aiter__) + + return __aiter__ + _side_effect_methods = { '__eq__': _get_eq, '__ne__': _get_ne, '__iter__': _get_iter, + '__aiter__': _get_async_iter } diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index e5a33c2a2bcf0c..80bddf43e78839 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -1,5 +1,6 @@ import asyncio import inspect +import sys import unittest from unittest.mock import call, CoroutineMock, patch, MagicMock @@ -237,7 +238,7 @@ async def __aenter__(self, *args, **kwargs): async def __aexit__(self, *args, **kwargs): self.exited = True - class AsyncIterator: + class AsyncIterator(object): def __init__(self): self.iter_called = False self.next_called = False @@ -254,6 +255,11 @@ async def __anext__(self): raise StopAsyncIteration + # Before 3.7 __aiter__ was a coroutine + class AsyncItertorDeprecated(AsyncIterator): + async def __aiter__(self): + return super().__aiter__() + def test_mock_magic_methods_are_coroutine_mocks(self): mock_instance = CoroutineMock(spec=self.AsyncContextManager()) self.assertIsInstance(mock_instance.__aenter__, @@ -262,26 +268,27 @@ def test_mock_magic_methods_are_coroutine_mocks(self): CoroutineMock) def test_mock_aiter_and_anext(self): - instance = self.AsyncIterator() + if sys.version_info < (3, 7): + instance = self.AsyncItertorDeprecated() + else: + instance = self.AsyncIterator() mock_instance = CoroutineMock(instance) self.assertEqual(asyncio.iscoroutine(instance.__aiter__), asyncio.iscoroutine(mock_instance.__aiter__)) self.assertEqual(asyncio.iscoroutine(instance.__anext__), asyncio.iscoroutine(mock_instance.__anext__)) - - iterator = instance.__aiter__() - if asyncio.iscoroutine(iterator): + if sys.version_info < (3, 7): + iterator = instance.__aiter__() iterator = run_coroutine(iterator) - mock_iterator = mock_instance.__aiter__() - if asyncio.iscoroutine(mock_iterator): + mock_iterator = mock_instance.__aiter__() mock_iterator = run_coroutine(mock_iterator) - self.assertEqual(asyncio.iscoroutine(iterator.__aiter__), - asyncio.iscoroutine(mock_iterator.__aiter__)) - self.assertEqual(asyncio.iscoroutine(iterator.__anext__), - asyncio.iscoroutine(mock_iterator.__anext__)) + self.assertEqual(asyncio.iscoroutine(iterator.__aiter__), + asyncio.iscoroutine(mock_iterator.__aiter__)) + self.assertEqual(asyncio.iscoroutine(iterator.__anext__), + asyncio.iscoroutine(mock_iterator.__anext__)) class CoroutineMockAssert(unittest.TestCase): From bfdd5a7428a1e7d5738ba9f98f7a5c78342f72d6 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 12 Sep 2018 09:02:37 -0700 Subject: [PATCH 07/24] Removes newlines from inspect. --- Lib/inspect.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index d3931ba0deb9e7..e799a83a74046f 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -176,7 +176,6 @@ def isgeneratorfunction(object): return bool((isfunction(object) or ismethod(object)) and object.__code__.co_flags & CO_GENERATOR) - def iscoroutinefunction(object): """Return true if the object is a coroutine function. @@ -185,7 +184,6 @@ def iscoroutinefunction(object): return bool((isfunction(object) or ismethod(object)) and object.__code__.co_flags & CO_COROUTINE) - def isasyncgenfunction(object): """Return true if the object is an asynchronous generator function. From ed7f13cd15410b8cb3b6942ecc1640a8a99e3f81 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 12 Sep 2018 09:07:31 -0700 Subject: [PATCH 08/24] Removes unneeded comment and newlines. --- Lib/unittest/mock.py | 7 ------- Lib/unittest/test/testmock/testhelpers.py | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 8b45a7e08fc494..dfde1a5389ea8c 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -50,7 +50,6 @@ # Without this, the __class__ properties wouldn't be set correctly _safe_super = super - def _is_coroutine_obj(obj): if getattr(obj, '__code__', None): return asyncio.iscoroutinefunction(obj) or asyncio.iscoroutine(obj) @@ -2612,12 +2611,6 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, if isinstance(spec, FunctionTypes): # should only happen at the top level because we don't # recurse for functions - - # LISA check if the orginal object is a coroutines - # if it is set the _is_coroutine to True - # somewhere around here - # We need to ensure that the child function is a coroutine function, - # without acting like a function mock = _set_signature(mock, spec) if is_coroutine_func: # Can't wrap the mock with asyncio.coroutine because it doesn't diff --git a/Lib/unittest/test/testmock/testhelpers.py b/Lib/unittest/test/testmock/testhelpers.py index 2e19a60001aacd..7919482ae99c7f 100644 --- a/Lib/unittest/test/testmock/testhelpers.py +++ b/Lib/unittest/test/testmock/testhelpers.py @@ -698,6 +698,7 @@ class RaiserClass(object): @staticmethod def existing(a, b): return a + b + s = create_autospec(RaiserClass) self.assertRaises(TypeError, lambda x: s.existing(1, 2, 3)) s.existing(1, 2) From 34fa74ef7fb951f991692f412e9c599b112aa75b Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Thu, 13 Sep 2018 20:38:21 -0700 Subject: [PATCH 09/24] Fixes async tests. Removes inspect fix. --- Lib/unittest/mock.py | 11 +- Lib/unittest/test/testmock/testasync.py | 250 ++++++++++-------- Lib/unittest/test/testmock/testcallable.py | 7 - .../2018-09-13-20-33-24.bpo-26467.cahAk3.rst | 2 + 4 files changed, 144 insertions(+), 126 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index dfde1a5389ea8c..fea43b71e88719 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -391,9 +391,8 @@ def __new__(cls, *args, **kw): def __init__( self, 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 + parent=None, _spec_state=None, _new_name='', _new_parent=None, + _spec_as_instance=False, _eat_self=None, unsafe=False, **kwargs ): if _new_parent is None: _new_parent = parent @@ -928,7 +927,7 @@ def _get_child_mock(self, **kw): if not issubclass(_type, CallableMixin): if issubclass(_type, NonCallableMagicMock): klass = MagicMock - elif issubclass(_type, NonCallableMock): + elif issubclass(_type, NonCallableMock) : klass = Mock else: klass = _type.__mro__[1] @@ -1056,10 +1055,6 @@ def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, parent=None, _spec_state=None, _new_name='', _new_parent=None, **kwargs): self.__dict__['_mock_return_value'] = return_value - # Makes inspect.iscoroutinefunction() return False when testing a Mock. - code_mock = NonCallableMock(spec_set=CodeType) - code_mock.co_flags = 0 - self.__dict__['__code__'] = code_mock _safe_super(CallableMixin, self).__init__( spec, wraps, name, spec_set, parent, _spec_state, _new_name, _new_parent, **kwargs diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 80bddf43e78839..b6ae5de7c65884 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -6,7 +6,7 @@ from unittest.mock import call, CoroutineMock, patch, MagicMock -class AsyncFoo(object): +class AsyncClass(object): def __init__(self, a): pass async def coroutine_method(self): @@ -14,17 +14,22 @@ async def coroutine_method(self): @asyncio.coroutine def decorated_cr_method(self): pass + def normal_method(self): + pass async def coroutine_func(): pass -class NormalFoo(object): +def normal_func(): + pass + +class NormalClass(object): def a(self): pass -async_foo_name = f'{__name__}.AsyncFoo' -normal_foo_name = f'{__name__}.NormalFoo' +async_foo_name = f'{__name__}.AsyncClass' +normal_foo_name = f'{__name__}.NormalClass' def run_coroutine(coroutine): @@ -38,11 +43,11 @@ def run_coroutine(coroutine): class CoroutinePatchDecoratorTest(unittest.TestCase): def test_is_coroutine_function_patch(self): - @patch.object(AsyncFoo, 'coroutine_method') + @patch.object(AsyncClass, 'coroutine_method') def test_async(mock_method): self.assertTrue(asyncio.iscoroutinefunction(mock_method)) - @patch.object(AsyncFoo, 'decorated_cr_method') + @patch.object(AsyncClass, 'decorated_cr_method') def test_async_decorator(mock_g): self.assertTrue(asyncio.iscoroutinefunction(mock_g)) @@ -50,11 +55,11 @@ def test_async_decorator(mock_g): test_async_decorator() def test_is_coroutine_patch(self): - @patch.object(AsyncFoo, 'coroutine_method') + @patch.object(AsyncClass, 'coroutine_method') def test_async(mock_method): self.assertTrue(asyncio.iscoroutine(mock_method())) - @patch.object(AsyncFoo, 'decorated_cr_method') + @patch.object(AsyncClass, 'decorated_cr_method') def test_async_decorator(mock_method): self.assertTrue(asyncio.iscoroutine(mock_method())) @@ -67,11 +72,11 @@ def test_no_parent_attribute(mock_method): test_no_parent_attribute() def test_is_coroutinemock_patch(self): - @patch.object(AsyncFoo, 'coroutine_method') + @patch.object(AsyncClass, 'coroutine_method') def test_async(mock_method): self.assertTrue(isinstance(mock_method, CoroutineMock)) - @patch.object(AsyncFoo, 'decorated_cr_method') + @patch.object(AsyncClass, 'decorated_cr_method') def test_async_decorator(mock_method): self.assertTrue(isinstance(mock_method, CoroutineMock)) @@ -82,11 +87,11 @@ def test_async_decorator(mock_method): class CoroutinePatchCMTest(unittest.TestCase): def test_is_coroutine_function_cm(self): def test_async(): - with patch.object(AsyncFoo, 'coroutine_method') as mock_method: + with patch.object(AsyncClass, 'coroutine_method') as mock_method: self.assertTrue(asyncio.iscoroutinefunction(mock_method)) def test_async_decorator(): - with patch.object(AsyncFoo, 'decorated_cr_method') as mock_method: + with patch.object(AsyncClass, 'decorated_cr_method') as mock_method: self.assertTrue(asyncio.iscoroutinefunction(mock_method)) test_async() @@ -94,22 +99,22 @@ def test_async_decorator(): def test_is_coroutine_cm(self): def test_async(): - with patch.object(AsyncFoo, 'coroutine_method') as mock_method: + with patch.object(AsyncClass, 'coroutine_method') as mock_method: self.assertTrue(asyncio.iscoroutine(mock_method())) def test_async_decorator(): - with patch.object(AsyncFoo, 'decorated_cr_method') as mock_method: + with patch.object(AsyncClass, 'decorated_cr_method') as mock_method: self.assertTrue(asyncio.iscoroutine(mock_method())) test_async() test_async_decorator() def test_is_coroutinemock_cm(self): def test_async(): - with patch.object(AsyncFoo, 'coroutine_method') as mock_method: + with patch.object(AsyncClass, 'coroutine_method') as mock_method: self.assertTrue(isinstance(mock_method, CoroutineMock)) def test_async_decorator(): - with patch.object(AsyncFoo, 'decorated_cr_method') as mock_method: + with patch.object(AsyncClass, 'decorated_cr_method') as mock_method: self.assertTrue(isinstance(mock_method, CoroutineMock)) test_async() @@ -132,7 +137,11 @@ def test_iscoroutine(self): self.assertTrue(asyncio.iscoroutine(mock())) self.assertIn('assert_awaited', dir(mock)) - # Should I test making a non-coroutine a coroutine mock? + def test_iscoroutinefunction_normal_function(self): + def foo(): pass + mock = CoroutineMock(foo) + self.assertTrue(asyncio.iscoroutinefunction(mock)) + self.assertTrue(inspect.iscoroutinefunction(mock)) class CoroutineAutospecTest(unittest.TestCase): @@ -140,7 +149,7 @@ def test_is_coroutinemock_patch(self): @patch(async_foo_name, autospec=True) def test_async(mock_method): self.assertTrue(isinstance( - mock_method.decorated_cr_method, + mock_method.coroutine_method, CoroutineMock)) self.assertTrue(isinstance(mock_method, MagicMock)) @@ -151,23 +160,50 @@ def test_async_decorator(mock_method): CoroutineMock)) self.assertTrue(isinstance(mock_method, MagicMock)) + @patch(async_foo_name, autospec=True) + def test_normal_method(mock_method): + self.assertTrue(isinstance( + mock_method.normal_method, + MagicMock)) + test_async() test_async_decorator() + test_normal_method() class CoroutineSpecTest(unittest.TestCase): - def test_spec_as_coroutine_positional(self): + def test_spec_as_coroutine_positional_magicmock(self): mock = MagicMock(coroutine_func) - self.assertIsInstance(mock, MagicMock) # Is this what we want? + self.assertIsInstance(mock, MagicMock) self.assertTrue(asyncio.iscoroutine(mock())) - def test_spec_as_coroutine_kw(self): + def test_spec_as_coroutine_kw_magicmock(self): mock = MagicMock(spec=coroutine_func) self.assertIsInstance(mock, MagicMock) self.assertTrue(asyncio.iscoroutine(mock())) + def test_spec_as_coroutine_kw_coroutinemock(self): + mock = CoroutineMock(spec=coroutine_func) + self.assertIsInstance(mock, CoroutineMock) + self.assertTrue(asyncio.iscoroutine(mock())) + + def test_spec_as_coroutine_positional_coroutinemock(self): + mock = CoroutineMock(coroutine_func) + self.assertIsInstance(mock, CoroutineMock) + self.assertTrue(asyncio.iscoroutine(mock())) + + def test_spec_as_normal_kw_coroutinemock(self): + mock = CoroutineMock(spec=normal_func) + self.assertIsInstance(mock, CoroutineMock) + self.assertTrue(asyncio.iscoroutine(mock())) + + def test_spec_as_normal_positional_coroutinemock(self): + mock = CoroutineMock(normal_func) + self.assertIsInstance(mock, CoroutineMock) + self.assertTrue(asyncio.iscoroutine(mock())) + def test_spec_coroutine_mock(self): - @patch.object(AsyncFoo, 'coroutine_method', spec=True) + @patch.object(AsyncClass, 'coroutine_method', spec=True) def test_async(mock_method): self.assertTrue(isinstance(mock_method, CoroutineMock)) @@ -183,45 +219,49 @@ def test_async(mock_method): test_async() def test_target_coroutine_spec_not(self): - @patch.object(AsyncFoo, 'coroutine_method', spec=NormalFoo.a) + @patch.object(AsyncClass, 'coroutine_method', spec=NormalClass.a) def test_async_attribute(mock_method): self.assertTrue(isinstance(mock_method, MagicMock)) - self.assertFalse(inspect.iscoroutinefunction(mock_method)) + self.assertFalse(inspect.iscoroutine(mock_method)) + self.assertFalse(asyncio.iscoroutine(mock_method)) test_async_attribute() def test_target_not_coroutine_spec_is(self): - @patch.object(NormalFoo, 'a', spec=coroutine_func) + @patch.object(NormalClass, 'a', spec=coroutine_func) def test_attribute_not_coroutine_spec_is(mock_coroutine_func): self.assertTrue(isinstance(mock_coroutine_func, CoroutineMock)) - # should check here is there is coroutine functionality test_attribute_not_coroutine_spec_is() def test_spec_coroutine_attributes(self): - @patch(normal_foo_name, spec=AsyncFoo) - def test_coroutine_attributes_coroutines(MockNormalFoo): - self.assertTrue(isinstance(MockNormalFoo.coroutine_method, + @patch(normal_foo_name, spec=AsyncClass) + def test_coroutine_attributes_coroutines(MockNormalClass): + self.assertTrue(isinstance(MockNormalClass.coroutine_method, CoroutineMock)) - self.assertTrue(isinstance(MockNormalFoo, MagicMock)) + self.assertTrue(isinstance(MockNormalClass, MagicMock)) test_coroutine_attributes_coroutines() class CoroutineSpecSetTest(unittest.TestCase): def test_is_coroutinemock_patch(self): - @patch.object(AsyncFoo, 'coroutine_method', spec_set=True) + @patch.object(AsyncClass, 'coroutine_method', spec_set=True) def test_async(coroutine_method): self.assertTrue(isinstance(coroutine_method, CoroutineMock)) def test_is_coroutine_coroutinemock(self): - mock = CoroutineMock(spec_set=AsyncFoo.coroutine_method) + mock = CoroutineMock(spec_set=AsyncClass.coroutine_method) self.assertTrue(asyncio.iscoroutinefunction(mock)) self.assertTrue(isinstance(mock, CoroutineMock)) def test_is_child_coroutinemock(self): - mock = MagicMock(spec_set=AsyncFoo) + mock = MagicMock(spec_set=AsyncClass) self.assertTrue(asyncio.iscoroutinefunction(mock.coroutine_method)) + self.assertTrue(asyncio.iscoroutinefunction(mock.decorated_cr_method)) + self.assertFalse(asyncio.iscoroutinefunction(mock.normal_method)) self.assertTrue(isinstance(mock.coroutine_method, CoroutineMock)) + self.assertTrue(isinstance(mock.decorated_cr_method, CoroutineMock)) + self.assertTrue(isinstance(mock.normal_method, MagicMock)) self.assertTrue(isinstance(mock, MagicMock)) @@ -292,125 +332,113 @@ def test_mock_aiter_and_anext(self): class CoroutineMockAssert(unittest.TestCase): - @asyncio.coroutine - def test_assert_awaited(self): - mock = CoroutineMock() + def setUp(self): + self.mock = CoroutineMock() + + async def runnable_test(self, *args): + if not args: + await self.mock() + else: + await self.mock(*args) + + def test_assert_awaited(self): with self.assertRaises(AssertionError): - mock.assert_awaited() + self.mock.assert_awaited() - yield from mock() - mock.assert_awaited() + asyncio.run(self.runnable_test()) + self.mock.assert_awaited() - @asyncio.coroutine def test_assert_awaited_once(self): - mock = CoroutineMock() - with self.assertRaises(AssertionError): - mock.assert_awaited_once() + self.mock.assert_awaited_once() - yield from mock() - mock.assert_awaited_once() + asyncio.run(self.runnable_test()) + self.mock.assert_awaited_once() - yield from mock() + asyncio.run(self.runnable_test()) with self.assertRaises(AssertionError): - mock.assert_awaited_once() + self.mock.assert_awaited_once() - @asyncio.coroutine def test_assert_awaited_with(self): - mock = CoroutineMock() - + asyncio.run(self.runnable_test()) with self.assertRaises(AssertionError): - mock.assert_awaited_with('foo') + self.mock.assert_awaited_with('foo') - yield from mock('foo') - mock.assert_awaited_with('foo') + asyncio.run(self.runnable_test('foo')) + self.mock.assert_awaited_with('foo') - yield from mock('NormalFoo') + asyncio.run(self.runnable_test('SomethingElse')) with self.assertRaises(AssertionError): - mock.assert_awaited_with('foo') + self.mock.assert_awaited_with('foo') - @asyncio.coroutine def test_assert_awaited_once_with(self): - mock = CoroutineMock() - with self.assertRaises(AssertionError): - mock.assert_awaited_once_with('foo') + self.mock.assert_awaited_once_with('foo') - yield from mock('foo') - mock.assert_awaited_once_with('foo') + asyncio.run(self.runnable_test('foo')) + self.mock.assert_awaited_once_with('foo') - yield from mock('foo') + asyncio.run(self.runnable_test('foo')) with self.assertRaises(AssertionError): - mock.assert_awaited_once_with('foo') + self.mock.assert_awaited_once_with('foo') - @asyncio.coroutine def test_assert_any_wait(self): - mock = CoroutineMock() - with self.assertRaises(AssertionError): - mock.assert_any_await('NormalFoo') + self.mock.assert_any_await('NormalFoo') - yield from mock('foo') + asyncio.run(self.runnable_test('foo')) with self.assertRaises(AssertionError): - mock.assert_any_await('NormalFoo') + self.mock.assert_any_await('NormalFoo') - yield from mock('NormalFoo') - mock.assert_any_await('NormalFoo') + asyncio.run(self.runnable_test('NormalFoo')) + self.mock.assert_any_await('NormalFoo') - yield from mock('baz') - mock.assert_any_await('NormalFoo') + asyncio.run(self.runnable_test('SomethingElse')) + self.mock.assert_any_await('NormalFoo') - @asyncio.coroutine - def test_assert_has_awaits(self): + def test_assert_has_awaits_no_order(self): calls = [call('NormalFoo'), call('baz')] - with self.subTest('any_order=False'): - mock = CoroutineMock() - - with self.assertRaises(AssertionError): - mock.assert_has_awaits(calls) - - yield from mock('foo') - with self.assertRaises(AssertionError): - mock.assert_has_awaits(calls) + with self.assertRaises(AssertionError): + self.mock.assert_has_awaits(calls) - yield from mock('NormalFoo') - with self.assertRaises(AssertionError): - mock.assert_has_awaits(calls) + asyncio.run(self.runnable_test('foo')) + with self.assertRaises(AssertionError): + self.mock.assert_has_awaits(calls) - yield from mock('baz') - mock.assert_has_awaits(calls) + asyncio.run(self.runnable_test('NormalFoo')) + with self.assertRaises(AssertionError): + self.mock.assert_has_awaits(calls) - yield from mock('qux') - mock.assert_has_awaits(calls) + asyncio.run(self.runnable_test('baz')) + self.mock.assert_has_awaits(calls) - with self.subTest('any_order=True'): - mock = CoroutineMock() + asyncio.run(self.runnable_test('SomethingElse')) + self.mock.assert_has_awaits(calls) - with self.assertRaises(AssertionError): - mock.assert_has_awaits(calls, any_order=True) + def test_assert_has_awaits_ordered(self): + calls = [call('NormalFoo'), call('baz')] + with self.assertRaises(AssertionError): + self.mock.assert_has_awaits(calls, any_order=True) - yield from mock('baz') - with self.assertRaises(AssertionError): - mock.assert_has_awaits(calls, any_order=True) + asyncio.run(self.runnable_test('baz')) + with self.assertRaises(AssertionError): + self.mock.assert_has_awaits(calls, any_order=True) - yield from mock('foo') - with self.assertRaises(AssertionError): - mock.assert_has_awaits(calls, any_order=True) + asyncio.run(self.runnable_test('foo')) + with self.assertRaises(AssertionError): + self.mock.assert_has_awaits(calls, any_order=True) - yield from mock('NormalFoo') - mock.assert_has_awaits(calls, any_order=True) + asyncio.run(self.runnable_test('NormalFoo')) + self.mock.assert_has_awaits(calls, any_order=True) - yield from mock('qux') - mock.assert_has_awaits(calls, any_order=True) + asyncio.run(self.runnable_test('qux')) + self.mock.assert_has_awaits(calls, any_order=True) - @asyncio.coroutine def test_assert_not_awaited(self): - mock = CoroutineMock() - - mock.assert_not_awaited() + self.mock.assert_not_awaited() - yield from mock() + asyncio.run(self.runnable_test()) with self.assertRaises(AssertionError): - mock.assert_not_awaited() + self.mock.assert_not_awaited() diff --git a/Lib/unittest/test/testmock/testcallable.py b/Lib/unittest/test/testmock/testcallable.py index 0f0d99f43a7db2..af1ce7ebbae4da 100644 --- a/Lib/unittest/test/testmock/testcallable.py +++ b/Lib/unittest/test/testmock/testcallable.py @@ -2,7 +2,6 @@ # E-mail: fuzzyman AT voidspace DOT org DOT uk # http://www.voidspace.org.uk/python/mock/ -import inspect import unittest from unittest.test.testmock.support import is_instance, X, SomeClass @@ -147,12 +146,6 @@ def test_create_autospec_instance(self): self.assertRaises(TypeError, mock.wibble, 'some', 'args') - def test_inspect_iscoroutine_function(self): - def foo(): pass - - mock = Mock(foo) - self.assertFalse(inspect.iscoroutine(mock)) - if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst b/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst new file mode 100644 index 00000000000000..eb6651045c83db --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst @@ -0,0 +1,2 @@ +Added CoroutineMock to support using unittest to mock asyncio coroutines. +Patch by Lisa Roach. From 302ef648b854475e9b68e76d869dde01a9b23cb3 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Thu, 13 Sep 2018 22:19:00 -0700 Subject: [PATCH 10/24] Fixes environment test issue. --- Lib/unittest/test/testmock/testasync.py | 47 ++++++++++++++----------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index b6ae5de7c65884..23867e83531dbd 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -335,8 +335,13 @@ class CoroutineMockAssert(unittest.TestCase): def setUp(self): self.mock = CoroutineMock() + self.old_policy = asyncio.events._event_loop_policy - async def runnable_test(self, *args): + def tearDown(self): + # Restore the original event loop policy. + asyncio.events._event_loop_policy = self.old_policy + + async def _runnable_test(self, *args): if not args: await self.mock() else: @@ -346,29 +351,29 @@ def test_assert_awaited(self): with self.assertRaises(AssertionError): self.mock.assert_awaited() - asyncio.run(self.runnable_test()) + asyncio.run(self._runnable_test()) self.mock.assert_awaited() def test_assert_awaited_once(self): with self.assertRaises(AssertionError): self.mock.assert_awaited_once() - asyncio.run(self.runnable_test()) + asyncio.run(self._runnable_test()) self.mock.assert_awaited_once() - asyncio.run(self.runnable_test()) + asyncio.run(self._runnable_test()) with self.assertRaises(AssertionError): self.mock.assert_awaited_once() def test_assert_awaited_with(self): - asyncio.run(self.runnable_test()) + asyncio.run(self._runnable_test()) with self.assertRaises(AssertionError): self.mock.assert_awaited_with('foo') - asyncio.run(self.runnable_test('foo')) + asyncio.run(self._runnable_test('foo')) self.mock.assert_awaited_with('foo') - asyncio.run(self.runnable_test('SomethingElse')) + asyncio.run(self._runnable_test('SomethingElse')) with self.assertRaises(AssertionError): self.mock.assert_awaited_with('foo') @@ -376,10 +381,10 @@ def test_assert_awaited_once_with(self): with self.assertRaises(AssertionError): self.mock.assert_awaited_once_with('foo') - asyncio.run(self.runnable_test('foo')) + asyncio.run(self._runnable_test('foo')) self.mock.assert_awaited_once_with('foo') - asyncio.run(self.runnable_test('foo')) + asyncio.run(self._runnable_test('foo')) with self.assertRaises(AssertionError): self.mock.assert_awaited_once_with('foo') @@ -387,14 +392,14 @@ def test_assert_any_wait(self): with self.assertRaises(AssertionError): self.mock.assert_any_await('NormalFoo') - asyncio.run(self.runnable_test('foo')) + asyncio.run(self._runnable_test('foo')) with self.assertRaises(AssertionError): self.mock.assert_any_await('NormalFoo') - asyncio.run(self.runnable_test('NormalFoo')) + asyncio.run(self._runnable_test('NormalFoo')) self.mock.assert_any_await('NormalFoo') - asyncio.run(self.runnable_test('SomethingElse')) + asyncio.run(self._runnable_test('SomethingElse')) self.mock.assert_any_await('NormalFoo') def test_assert_has_awaits_no_order(self): @@ -403,18 +408,18 @@ def test_assert_has_awaits_no_order(self): with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls) - asyncio.run(self.runnable_test('foo')) + asyncio.run(self._runnable_test('foo')) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls) - asyncio.run(self.runnable_test('NormalFoo')) + asyncio.run(self._runnable_test('NormalFoo')) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls) - asyncio.run(self.runnable_test('baz')) + asyncio.run(self._runnable_test('baz')) self.mock.assert_has_awaits(calls) - asyncio.run(self.runnable_test('SomethingElse')) + asyncio.run(self._runnable_test('SomethingElse')) self.mock.assert_has_awaits(calls) def test_assert_has_awaits_ordered(self): @@ -422,23 +427,23 @@ def test_assert_has_awaits_ordered(self): with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls, any_order=True) - asyncio.run(self.runnable_test('baz')) + asyncio.run(self._runnable_test('baz')) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls, any_order=True) - asyncio.run(self.runnable_test('foo')) + asyncio.run(self._runnable_test('foo')) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls, any_order=True) - asyncio.run(self.runnable_test('NormalFoo')) + asyncio.run(self._runnable_test('NormalFoo')) self.mock.assert_has_awaits(calls, any_order=True) - asyncio.run(self.runnable_test('qux')) + asyncio.run(self._runnable_test('qux')) self.mock.assert_has_awaits(calls, any_order=True) def test_assert_not_awaited(self): self.mock.assert_not_awaited() - asyncio.run(self.runnable_test()) + asyncio.run(self._runnable_test()) with self.assertRaises(AssertionError): self.mock.assert_not_awaited() From bf749ac6b84d4e5d4aecb29f23797f19d927a125 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Fri, 14 Sep 2018 00:15:20 -0700 Subject: [PATCH 11/24] Adds argument tests. --- Lib/unittest/test/testmock/testasync.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 23867e83531dbd..30f83d0025bd3e 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -265,6 +265,22 @@ def test_is_child_coroutinemock(self): self.assertTrue(isinstance(mock, MagicMock)) +class CoroutineArguments(unittest.TestCase): + async def compute(x): + yield x + + def test_add_return_value(self): + mock = CoroutineMock(self.compute, return_value=10) + output = mock(5) + self.assertEqual(output, 10) + + def test_add_side_effect(self): + pass + + def test_add_side_effect_with_exception(self): + pass + + class CoroutineMagicMethods(unittest.TestCase): class AsyncContextManager: def __init__(self): From 30b64b53956d7eff6a3e353eff5026d18bf28e9a Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Fri, 14 Sep 2018 00:20:02 -0700 Subject: [PATCH 12/24] Adding the side_effect exception test. --- Lib/unittest/test/testmock/testasync.py | 27 +++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 30f83d0025bd3e..f9bc802d31631d 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -266,19 +266,30 @@ def test_is_child_coroutinemock(self): class CoroutineArguments(unittest.TestCase): - async def compute(x): - yield x + # I want to add more tests here with more complicate use-cases. + def setUp(self): + self.old_policy = asyncio.events._event_loop_policy + + def tearDown(self): + # Restore the original event loop policy. + asyncio.events._event_loop_policy = self.old_policy def test_add_return_value(self): - mock = CoroutineMock(self.compute, return_value=10) - output = mock(5) + async def addition(self, var): + return var + 1 + + mock = CoroutineMock(addition, return_value=10) + output = asyncio.run(mock(5)) + self.assertEqual(output, 10) - def test_add_side_effect(self): - pass + def test_add_side_effect_exception(self): + async def addition(self, var): + return var + 1 - def test_add_side_effect_with_exception(self): - pass + mock = CoroutineMock(addition, side_effect=Exception('err')) + with self.assertRaises(Exception): + asyncio.run(mock(5)) class CoroutineMagicMethods(unittest.TestCase): From 5edac2a053256898c5ec392f2368268a57ddd863 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Tue, 7 May 2019 11:16:14 -0400 Subject: [PATCH 13/24] Changes CoroutineMock to AsyncMock. Removes old-style coroutine references. --- Lib/unittest/mock.py | 364 ++++++++++-------------- Lib/unittest/test/testmock/testasync.py | 298 +++++++------------ Lib/unittest/test/testmock/testmock.py | 4 +- 3 files changed, 254 insertions(+), 412 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index fea43b71e88719..b18d6688c36257 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -13,7 +13,7 @@ 'ANY', 'call', 'create_autospec', - 'CoroutineMock', + 'AsyncMock', 'FILTER_DIR', 'NonCallableMock', 'NonCallableMagicMock', @@ -50,9 +50,9 @@ # Without this, the __class__ properties wouldn't be set correctly _safe_super = super -def _is_coroutine_obj(obj): +def _is_async_obj(obj): if getattr(obj, '__code__', None): - return asyncio.iscoroutinefunction(obj) or asyncio.iscoroutine(obj) + return asyncio.iscoroutinefunction(obj) or inspect.isawaitable(obj) else: return False @@ -366,8 +366,6 @@ def __init__(self, *args, **kwargs): pass -_is_coroutine = asyncio.coroutines._is_coroutine - class NonCallableMock(Base): """A non-callable version of `Mock`""" @@ -376,15 +374,15 @@ def __new__(cls, *args, **kw): # so we can create magic methods on the # class without stomping on other mocks bases = (cls,) - if cls != CoroutineMock: - # Check if spec is a coroutine object or function + if 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 = fnmatch.filter(bound_args.keys(), 'spec*') if spec_arg: # what if spec_set is different than spec? - if _is_coroutine_obj(bound_args[spec_arg[0]]): - bases = (CoroutineMockMixin, cls,) + if _is_async_obj(bound_args[spec_arg[0]]): + bases = (AsyncMockMixin, cls,) new = type(cls.__name__, bases, {'__doc__': cls.__doc__}) instance = object.__new__(new) return instance @@ -459,11 +457,11 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False, _eat_self=False): _spec_class = None _spec_signature = None - _spec_coroutines = [] + _spec_asyncs = [] for attr in dir(spec): if asyncio.iscoroutinefunction(getattr(spec, attr, None)): - _spec_coroutines.append(attr) + _spec_asyncs.append(attr) if spec is not None and not _is_list(spec): if isinstance(spec, type): @@ -481,7 +479,7 @@ def _mock_add_spec(self, spec, spec_set, _spec_as_instance=False, __dict__['_spec_set'] = spec_set __dict__['_spec_signature'] = _spec_signature __dict__['_mock_methods'] = spec - __dict__['_spec_coroutines'] = _spec_coroutines + __dict__['_spec_asyncs'] = _spec_asyncs def __get_return_value(self): ret = self._mock_return_value @@ -916,13 +914,13 @@ 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_coroutines']: - return CoroutineMock(**kw) + if _new_name in self.__dict__['_spec_asyncs']: + return AsyncMock(**kw) _type = type(self) - if issubclass(_type, MagicMock) and _new_name in async_magic_coroutines: - klass = CoroutineMock - if issubclass(_type, CoroutineMockMixin): + if issubclass(_type, MagicMock) and _new_name in _async_method_magics: + klass = AsyncMock + if issubclass(_type, AsyncMockMixin): klass = MagicMock if not issubclass(_type, CallableMixin): if issubclass(_type, NonCallableMagicMock): @@ -956,99 +954,6 @@ def _try_iter(obj): return obj -class _AwaitEvent: - def __init__(self, mock): - self._mock = mock - self._condition = None - - @asyncio.coroutine - def wait(self, skip=0): - """ - Wait for await. - - :param skip: How many awaits will be skipped. - As a result, the mock should be awaited at least - ``skip + 1`` times. - """ - def predicate(mock): - return mock.await_count > skip - - return (yield from self.wait_for(predicate)) - - @asyncio.coroutine - def wait_next(self, skip=0): - """ - Wait for the next await. - - Unlike :meth:`wait` that counts any await, mock has to be awaited once - more, disregarding to the current - :attr:`asynctest.CoroutineMock.await_count`. - - :param skip: How many awaits will be skipped. - As a result, the mock should be awaited at least - ``skip + 1`` more times. - """ - await_count = self._mock.await_count - - def predicate(mock): - return mock.await_count > await_count + skip - - return (yield from self.wait_for(predicate)) - - @asyncio.coroutine - def wait_for(self, predicate): - """ - Wait for a given predicate to become True. - - :param predicate: A callable that receives mock which result - will be interpreted as a boolean value. - The final predicate value is the return value. - """ - condition = self._get_condition() - - try: - yield from condition.acquire() - - def _predicate(): - return predicate(self._mock) - - return (yield from condition.wait_for(_predicate)) - finally: - condition.release() - - @asyncio.coroutine - def _notify(self): - condition = self._get_condition() - - try: - yield from condition.acquire() - condition.notify_all() - finally: - condition.release() - - def _get_condition(self): - """ - Creation of condition is delayed, to minimize the change of using the - wrong loop. - - A user may create a mock with _AwaitEvent before selecting the - execution loop. Requiring a user to delay creation is error-prone and - inflexible. Instead, condition is created when user actually starts to - use the mock. - """ - # No synchronization is needed: - # - asyncio is thread unsafe - # - there are no awaits here, method will be executed without - # switching asyncio context. - if self._condition is None: - self._condition = asyncio.Condition() - - return self._condition - - def __bool__(self): - return self._mock.await_count != 0 - - class CallableMixin(Base): def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, @@ -1201,10 +1106,6 @@ class or instance) that acts as the specification for the mock object. If """ -def _raise(exception): - raise exception - - def _dot_lookup(thing, comp, import_path): try: return getattr(thing, comp) @@ -1402,8 +1303,8 @@ def __enter__(self): if isinstance(original, type): # If we're patching out a class and there is a spec inherit = True - if spec is None and _is_coroutine_obj(original): - Klass = CoroutineMock + if spec is None and _is_async_obj(original): + Klass = AsyncMock else: Klass = MagicMock _kwargs = {} @@ -1417,8 +1318,8 @@ def __enter__(self): not_callable = '__call__' not in this_spec else: not_callable = not callable(this_spec) - if _is_coroutine_obj(this_spec): - Klass = CoroutineMock + if _is_async_obj(this_spec): + Klass = AsyncMock elif not_callable: Klass = NonCallableMagicMock @@ -1876,13 +1777,10 @@ def method(self, *args, **kw): ' '.join([magic_methods, numerics, inplace, right]).split() } -async_magic_coroutines = ("__aenter__", "__aexit__", "__anext__") -_async_magics = async_magic_coroutines + ("__aiter__", ) - -async_magic_coroutines = set(async_magic_coroutines) -_async_magics = set(_async_magics) - - +# Magic methods used for async `with` statements +_async_method_magics = {"__aenter__", "__aexit__", "__anext__"} +# `__aiter__` is a plain function but used with async calls +_async_magics = _async_method_magics | {"__aiter__"} _all_magics = _magics | _non_defaults @@ -1914,22 +1812,6 @@ def method(self, *args, **kw): '__index__': 1, } -class AsyncIterator: - """ - Wraps an iterator in an asynchronous iterator. - """ - def __init__(self, iterator): - self.iterator = iterator - - def __aiter__(self): - return self - - async def __anext__(self): - try: - return next(self.iterator) - except StopIteration: - pass - raise StopAsyncIteration def _get_eq(self): def __eq__(other): @@ -1960,19 +1842,12 @@ def __iter__(): return iter(ret_val) return __iter__ -def _get_async_iter(mock): +def _get_async_iter(self): def __aiter__(): - return_value = mock.__aiter__._mock_return_value - if return_value is DEFAULT: - iterator = iter([]) - else: - iterator = iter(return_value) - - return AsyncIterator(iterator) - - if asyncio.iscoroutinefunction(mock.__aiter__): - return asyncio.coroutine(__aiter__) - + ret_val = self.__aiter__._mock_return_value + if ret_val is DEFAULT: + return AsyncIterator([]) + return AsyncIterator(ret_val) return __aiter__ _side_effect_methods = { @@ -2094,7 +1969,7 @@ def __get__(self, obj, _type=None): return self.create_mock() -class AsyncMagicMixin(object): +class AsyncMagicMixin: def __init__(self, *args, **kw): self._mock_set_async_magics() # make magic work for kwargs in init _safe_super(AsyncMagicMixin, self).__init__(*args, **kw) @@ -2120,7 +1995,7 @@ def _mock_set_async_magics(self): setattr(_type, entry, MagicProxy(entry, self)) -class CoroutineMockMixin(Base): +class AsyncMockMixin(Base): awaited = _delegating_property('awaited') await_count = _delegating_property('await_count') await_args = _delegating_property('await_args') @@ -2128,48 +2003,46 @@ class CoroutineMockMixin(Base): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - - # asyncio.iscoroutinefunction() checks this property to say if an - # object is a coroutine + # asyncio.iscoroutinefunction() checks _is_coroutine property to say if an + # object is a coroutine. Without this check it looks to see if it is a + # function/method, which in this case it is not (since it is an + # AsyncMock). # It is set through __dict__ because when spec_set is True, this # attribute is likely undefined. - self.__dict__['_is_coroutine'] = _is_coroutine + self.__dict__['_is_coroutine'] = asyncio.coroutines._is_coroutine self.__dict__['_mock_awaited'] = _AwaitEvent(self) 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 = 129 + code_mock.co_flags = inspect.CO_COROUTINE self.__dict__['__code__'] = code_mock - - def _mock_call(_mock_self, *args, **kwargs): + async def _mock_call(_mock_self, *args, **kwargs): + self = _mock_self try: result = super()._mock_call(*args, **kwargs) - _call = _mock_self.call_args - - @asyncio.coroutine - def proxy(): - try: - if asyncio.iscoroutine(result): - return (yield from result) - else: - return result - finally: - _mock_self.await_count += 1 - _mock_self.await_args = _call - _mock_self.await_args_list.append(_call) - yield from _mock_self.awaited._notify() - - return proxy() - except StopIteration as e: - side_effect = _mock_self.side_effect + except (BaseException, StopIteration) as e: + side_effect = self.side_effect if side_effect is not None and not callable(side_effect): raise + return _raise(e) + + _call = self.call_args + + async def proxy(): + try: + if inspect.isawaitable(result): + return await result + else: + return result + finally: + self.await_count += 1 + self.await_args = _call + self.await_args_list.append(_call) + await self.awaited._notify() - return asyncio.coroutine(_raise)(e) - except BaseException as e: - return asyncio.coroutine(_raise)(e) + return await proxy() def assert_awaited(_mock_self): """ @@ -2287,49 +2160,49 @@ def reset_mock(self, *args, **kwargs): See :func:`.Mock.reset_mock()` """ super().reset_mock(*args, **kwargs) - self.awaited = _AwaitEvent(self) self.await_count = 0 self.await_args = None self.await_args_list = _CallList() -class CoroutineMock(CoroutineMockMixin, AsyncMagicMixin, Mock): +class AsyncMock(AsyncMockMixin, AsyncMagicMixin, Mock): """ Enhance :class:`Mock` with features allowing to mock - a coroutine function. + a async function. - The :class:`CoroutineMock` object will behave so the object is - recognized as coroutine function, and the result of a call as a coroutine: + The :class:`AsyncMock` object will behave so the object is + recognized as async function, and the result of a call as a async: - >>> mock = CoroutineMock() + >>> mock = AsyncMock() >>> asyncio.iscoroutinefunction(mock) True - >>> asyncio.iscoroutine(mock()) + >>> inspect.isawaitable(mock()) True - The result of ``mock()`` is a coroutine which will have the outcome of - ``side_effect`` or ``return_value``: + The result of ``mock()`` is an async function which will have the outcome + of ``side_effect`` or ``return_value``: - - if ``side_effect`` is a function, the coroutine will return the result - of that function, - - if ``side_effect`` is an exception, the coroutine will raise the + - if ``side_effect`` is a function, the async function will return the + result of that function, + - if ``side_effect`` is an exception, the async function will raise the exception, - - if ``side_effect`` is an iterable, the coroutine will return the next - value of the iterable, however, if the sequence of result is exhausted, - ``StopIteration`` is raised immediately, - - if ``side_effect`` is not defined, the coroutine will return the value - defined by ``return_value``, hence, by default, the coroutine returns - a new :class:`CoroutineMock` object. - - If the outcome of ``side_effect`` or ``return_value`` is a coroutine, the - mock coroutine obtained when the mock object is called will be this - coroutine itself (and not a coroutine returning a coroutine). + - if ``side_effect`` is an iterable, the async function will return the + next value of the iterable, however, if the sequence of result is + exhausted, ``StopIteration`` is raised immediately, + - if ``side_effect`` is not defined, the async function will return the + value defined by ``return_value``, hence, by default, the async function + returns a new :class:`AsyncMock` object. + + If the outcome of ``side_effect`` or ``return_value`` is an async function, the + mock async function obtained when the mock object is called will be this + async function itself (and not an async function returning an async + function). The test author can also specify a wrapped object with ``wraps``. In this case, the :class:`Mock` object behavior is the same as with an :class:`.Mock` object: the wrapped object may have methods - defined as coroutine functions. + defined as async function functions. """ @@ -2564,9 +2437,9 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, is_type = isinstance(spec, type) if getattr(spec, '__code__', None): - is_coroutine_func = asyncio.iscoroutinefunction(spec) + is_async_func = asyncio.iscoroutinefunction(spec) else: - is_coroutine_func = False + is_async_func = False _kwargs = {'spec': spec} if spec_set: _kwargs = {'spec_set': spec} @@ -2583,11 +2456,11 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # descriptors don't have a spec # because we don't know what type they return _kwargs = {} - elif is_coroutine_func: + elif is_async_func: if instance: raise RuntimeError("Instance can not be True when create_autospec " - "is mocking a coroutine function") - Klass = CoroutineMock + "is mocking an async function") + Klass = AsyncMock elif not _callable(spec): Klass = NonCallableMagicMock elif is_type and instance and not _instance_callable(spec): @@ -2604,17 +2477,12 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, name=_name, **_kwargs) if isinstance(spec, FunctionTypes): + wrapped_mock = mock # should only happen at the top level because we don't # recurse for functions mock = _set_signature(mock, spec) - if is_coroutine_func: - # Can't wrap the mock with asyncio.coroutine because it doesn't - # detect a CoroWrapper as an awaitable in debug mode. - # It is safe to do so because the mock object wrapped by - # _set_signature returns the result of the CoroutineMock itself, - # which is a Coroutine (as defined in CoroutineMock._mock_call) + if is_async_func: mock._is_coroutine = _is_coroutine - mock.awaited = _AwaitEvent(mock) mock.await_count = 0 mock.await_args = None mock.await_args_list = _CallList() @@ -2673,7 +2541,7 @@ def f(*args, **kwargs): skipfirst = _must_skip(spec, entry, is_type) kwargs['_eat_self'] = skipfirst if asyncio.iscoroutinefunction(original): - child_klass = CoroutineMock + child_klass = AsyncMock else: child_klass = MagicMock new = child_klass(parent=parent, name=entry, _new_name=entry, @@ -2883,3 +2751,61 @@ def seal(mock): continue if m._mock_new_parent is mock: seal(m) + + +async def _raise(exception): + raise exception + + +class AsyncIterator: + """ + Wraps an iterator in an asynchronous iterator. + """ + def __init__(self, iterator): + self.iterator = iterator + code_mock = NonCallableMock(spec_set=CodeType) + code_mock.co_flags = inspect.CO_ITERATOR_COROUTINE + self.__dict__['__code__'] = code_mock + + def __aiter__(self): + return self + + # cannot use async before 3.7 + async def __anext__(self): + try: + return next(self.iterator) + except StopIteration: + pass + raise StopAsyncIteration + + +class _AwaitEvent: + def __init__(self, mock): + self._mock = mock + self._condition = None + + async def _notify(self): + condition = self._get_condition() + try: + await condition.acquire() + condition.notify_all() + finally: + condition.release() + + def _get_condition(self): + """ + Creation of condition is delayed, to minimize the chance of using the + wrong loop. + A user may create a mock with _AwaitEvent before selecting the + execution loop. Requiring a user to delay creation is error-prone and + inflexible. Instead, condition is created when user actually starts to + use the mock. + """ + # No synchronization is needed: + # - asyncio is thread unsafe + # - there are no awaits here, method will be executed without + # switching asyncio context. + if self._condition is None: + self._condition = asyncio.Condition() + + return self._condition diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index f9bc802d31631d..2821b5aa252692 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -1,23 +1,19 @@ import asyncio import inspect -import sys import unittest -from unittest.mock import call, CoroutineMock, patch, MagicMock +from unittest.mock import call, AsyncMock, patch, MagicMock - -class AsyncClass(object): - def __init__(self, a): - pass - async def coroutine_method(self): +# TODO: lisa move 3.7 specific syntax to its own file +class AsyncClass: + def __init__(self): pass - @asyncio.coroutine - def decorated_cr_method(self): + async def async_method(self): pass def normal_method(self): pass -async def coroutine_func(): +async def async_func(): pass def normal_func(): @@ -32,132 +28,91 @@ def a(self): normal_foo_name = f'{__name__}.NormalClass' -def run_coroutine(coroutine): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - return loop.run_until_complete(coroutine) - finally: - loop.close() - - -class CoroutinePatchDecoratorTest(unittest.TestCase): +class AsyncPatchDecoratorTest(unittest.TestCase): def test_is_coroutine_function_patch(self): - @patch.object(AsyncClass, 'coroutine_method') + # @patch.object(AsyncClass, 'normal_method') + # def test_normal_method(mock_method): + # import types + # self.assertTrue(isinstance(mock_method, types.MethodType)) + @patch.object(AsyncClass, 'async_method') def test_async(mock_method): self.assertTrue(asyncio.iscoroutinefunction(mock_method)) - - @patch.object(AsyncClass, 'decorated_cr_method') - def test_async_decorator(mock_g): - self.assertTrue(asyncio.iscoroutinefunction(mock_g)) - + # test_normal_method() test_async() - test_async_decorator() - def test_is_coroutine_patch(self): - @patch.object(AsyncClass, 'coroutine_method') + def test_is_async_patch(self): + @patch.object(AsyncClass, 'async_method') def test_async(mock_method): - self.assertTrue(asyncio.iscoroutine(mock_method())) + self.assertTrue(inspect.isawaitable(mock_method())) - @patch.object(AsyncClass, 'decorated_cr_method') - def test_async_decorator(mock_method): - self.assertTrue(asyncio.iscoroutine(mock_method())) - - @patch(f'{async_foo_name}.coroutine_method') + @patch(f'{async_foo_name}.async_method') def test_no_parent_attribute(mock_method): - self.assertTrue(asyncio.iscoroutine(mock_method())) + self.assertTrue(inspect.isawaitable(mock_method())) test_async() - test_async_decorator() test_no_parent_attribute() - def test_is_coroutinemock_patch(self): - @patch.object(AsyncClass, 'coroutine_method') + def test_is_AsyncMock_patch(self): + @patch.object(AsyncClass, 'async_method') def test_async(mock_method): - self.assertTrue(isinstance(mock_method, CoroutineMock)) - - @patch.object(AsyncClass, 'decorated_cr_method') - def test_async_decorator(mock_method): - self.assertTrue(isinstance(mock_method, CoroutineMock)) + self.assertTrue(isinstance(mock_method, AsyncMock)) test_async() - test_async_decorator() -class CoroutinePatchCMTest(unittest.TestCase): - def test_is_coroutine_function_cm(self): +class AsyncPatchCMTest(unittest.TestCase): + def test_is_async_function_cm(self): def test_async(): - with patch.object(AsyncClass, 'coroutine_method') as mock_method: - self.assertTrue(asyncio.iscoroutinefunction(mock_method)) - - def test_async_decorator(): - with patch.object(AsyncClass, 'decorated_cr_method') as mock_method: + with patch.object(AsyncClass, 'async_method') as mock_method: self.assertTrue(asyncio.iscoroutinefunction(mock_method)) test_async() - test_async_decorator() - def test_is_coroutine_cm(self): + def test_is_async_cm(self): def test_async(): - with patch.object(AsyncClass, 'coroutine_method') as mock_method: - self.assertTrue(asyncio.iscoroutine(mock_method())) + with patch.object(AsyncClass, 'async_method') as mock_method: + self.assertTrue(inspect.isawaitable(mock_method())) - def test_async_decorator(): - with patch.object(AsyncClass, 'decorated_cr_method') as mock_method: - self.assertTrue(asyncio.iscoroutine(mock_method())) test_async() - test_async_decorator() - def test_is_coroutinemock_cm(self): + def test_is_AsyncMock_cm(self): def test_async(): - with patch.object(AsyncClass, 'coroutine_method') as mock_method: - self.assertTrue(isinstance(mock_method, CoroutineMock)) - - def test_async_decorator(): - with patch.object(AsyncClass, 'decorated_cr_method') as mock_method: - self.assertTrue(isinstance(mock_method, CoroutineMock)) + with patch.object(AsyncClass, 'async_method') as mock_method: + self.assertTrue(isinstance(mock_method, AsyncMock)) test_async() - test_async_decorator() -class CoroutineMockTest(unittest.TestCase): +class AsyncMockTest(unittest.TestCase): def test_iscoroutinefunction_default(self): - mock = CoroutineMock() + mock = AsyncMock() self.assertTrue(asyncio.iscoroutinefunction(mock)) def test_iscoroutinefunction_function(self): async def foo(): pass - mock = CoroutineMock(foo) + mock = AsyncMock(foo) self.assertTrue(asyncio.iscoroutinefunction(mock)) self.assertTrue(inspect.iscoroutinefunction(mock)) - def test_iscoroutine(self): - mock = CoroutineMock() - self.assertTrue(asyncio.iscoroutine(mock())) + def test_isawaitable(self): + mock = AsyncMock() + self.assertTrue(inspect.isawaitable(mock())) self.assertIn('assert_awaited', dir(mock)) def test_iscoroutinefunction_normal_function(self): def foo(): pass - mock = CoroutineMock(foo) + mock = AsyncMock(foo) self.assertTrue(asyncio.iscoroutinefunction(mock)) self.assertTrue(inspect.iscoroutinefunction(mock)) -class CoroutineAutospecTest(unittest.TestCase): - def test_is_coroutinemock_patch(self): +class AsyncAutospecTest(unittest.TestCase): + def test_is_AsyncMock_patch(self): @patch(async_foo_name, autospec=True) def test_async(mock_method): self.assertTrue(isinstance( - mock_method.coroutine_method, - CoroutineMock)) - self.assertTrue(isinstance(mock_method, MagicMock)) - - @patch(async_foo_name, autospec=True) - def test_async_decorator(mock_method): - self.assertTrue(isinstance( - mock_method.decorated_cr_method, - CoroutineMock)) + mock_method.async_method, + AsyncMock)) self.assertTrue(isinstance(mock_method, MagicMock)) @patch(async_foo_name, autospec=True) @@ -167,106 +122,103 @@ def test_normal_method(mock_method): MagicMock)) test_async() - test_async_decorator() test_normal_method() -class CoroutineSpecTest(unittest.TestCase): - def test_spec_as_coroutine_positional_magicmock(self): - mock = MagicMock(coroutine_func) +class AsyncSpecTest(unittest.TestCase): + def test_spec_as_async_positional_magicmock(self): + mock = MagicMock(async_func) self.assertIsInstance(mock, MagicMock) - self.assertTrue(asyncio.iscoroutine(mock())) + self.assertTrue(inspect.isawaitable(mock())) - def test_spec_as_coroutine_kw_magicmock(self): - mock = MagicMock(spec=coroutine_func) + def test_spec_as_async_kw_magicmock(self): + mock = MagicMock(spec=async_func) self.assertIsInstance(mock, MagicMock) - self.assertTrue(asyncio.iscoroutine(mock())) - - def test_spec_as_coroutine_kw_coroutinemock(self): - mock = CoroutineMock(spec=coroutine_func) - self.assertIsInstance(mock, CoroutineMock) - self.assertTrue(asyncio.iscoroutine(mock())) - - def test_spec_as_coroutine_positional_coroutinemock(self): - mock = CoroutineMock(coroutine_func) - self.assertIsInstance(mock, CoroutineMock) - self.assertTrue(asyncio.iscoroutine(mock())) - - def test_spec_as_normal_kw_coroutinemock(self): - mock = CoroutineMock(spec=normal_func) - self.assertIsInstance(mock, CoroutineMock) - self.assertTrue(asyncio.iscoroutine(mock())) - - def test_spec_as_normal_positional_coroutinemock(self): - mock = CoroutineMock(normal_func) - self.assertIsInstance(mock, CoroutineMock) - self.assertTrue(asyncio.iscoroutine(mock())) - - def test_spec_coroutine_mock(self): - @patch.object(AsyncClass, 'coroutine_method', spec=True) + self.assertTrue(inspect.isawaitable(mock())) + + def test_spec_as_async_kw_AsyncMock(self): + mock = AsyncMock(spec=async_func) + self.assertIsInstance(mock, AsyncMock) + self.assertTrue(inspect.isawaitable(mock())) + + def test_spec_as_async_positional_AsyncMock(self): + mock = AsyncMock(async_func) + self.assertIsInstance(mock, AsyncMock) + self.assertTrue(inspect.isawaitable(mock())) + + def test_spec_as_normal_kw_AsyncMock(self): + mock = AsyncMock(spec=normal_func) + self.assertIsInstance(mock, AsyncMock) + self.assertTrue(inspect.isawaitable(mock())) + + def test_spec_as_normal_positional_AsyncMock(self): + mock = AsyncMock(normal_func) + self.assertIsInstance(mock, AsyncMock) + self.assertTrue(inspect.isawaitable(mock())) + + def test_spec_async_mock(self): + @patch.object(AsyncClass, 'async_method', spec=True) def test_async(mock_method): - self.assertTrue(isinstance(mock_method, CoroutineMock)) + self.assertTrue(isinstance(mock_method, AsyncMock)) test_async() - def test_spec_parent_not_coroutine_attribute_is(self): + def test_spec_parent_not_async_attribute_is(self): @patch(async_foo_name, spec=True) def test_async(mock_method): self.assertTrue(isinstance(mock_method, MagicMock)) - self.assertTrue(isinstance(mock_method.coroutine_method, - CoroutineMock)) + self.assertTrue(isinstance(mock_method.async_method, + AsyncMock)) test_async() - def test_target_coroutine_spec_not(self): - @patch.object(AsyncClass, 'coroutine_method', spec=NormalClass.a) + def test_target_async_spec_not(self): + @patch.object(AsyncClass, 'async_method', spec=NormalClass.a) def test_async_attribute(mock_method): self.assertTrue(isinstance(mock_method, MagicMock)) - self.assertFalse(inspect.iscoroutine(mock_method)) - self.assertFalse(asyncio.iscoroutine(mock_method)) + self.assertFalse(inspect.iscoroutine(mock_method)) # TODO: lisa? + self.assertFalse(inspect.isawaitable(mock_method)) test_async_attribute() - def test_target_not_coroutine_spec_is(self): - @patch.object(NormalClass, 'a', spec=coroutine_func) - def test_attribute_not_coroutine_spec_is(mock_coroutine_func): - self.assertTrue(isinstance(mock_coroutine_func, CoroutineMock)) - test_attribute_not_coroutine_spec_is() + def test_target_not_async_spec_is(self): + @patch.object(NormalClass, 'a', spec=async_func) + def test_attribute_not_async_spec_is(mock_async_func): + self.assertTrue(isinstance(mock_async_func, AsyncMock)) + test_attribute_not_async_spec_is() - def test_spec_coroutine_attributes(self): + def test_spec_async_attributes(self): @patch(normal_foo_name, spec=AsyncClass) - def test_coroutine_attributes_coroutines(MockNormalClass): - self.assertTrue(isinstance(MockNormalClass.coroutine_method, - CoroutineMock)) + def test_async_attributes_coroutines(MockNormalClass): + self.assertTrue(isinstance(MockNormalClass.async_method, + AsyncMock)) self.assertTrue(isinstance(MockNormalClass, MagicMock)) - test_coroutine_attributes_coroutines() + test_async_attributes_coroutines() -class CoroutineSpecSetTest(unittest.TestCase): - def test_is_coroutinemock_patch(self): - @patch.object(AsyncClass, 'coroutine_method', spec_set=True) - def test_async(coroutine_method): - self.assertTrue(isinstance(coroutine_method, CoroutineMock)) +class AsyncSpecSetTest(unittest.TestCase): + def test_is_AsyncMock_patch(self): + @patch.object(AsyncClass, 'async_method', spec_set=True) + def test_async(async_method): + self.assertTrue(isinstance(async_method, AsyncMock)) - def test_is_coroutine_coroutinemock(self): - mock = CoroutineMock(spec_set=AsyncClass.coroutine_method) + def test_is_async_AsyncMock(self): + mock = AsyncMock(spec_set=AsyncClass.async_method) self.assertTrue(asyncio.iscoroutinefunction(mock)) - self.assertTrue(isinstance(mock, CoroutineMock)) + self.assertTrue(isinstance(mock, AsyncMock)) - def test_is_child_coroutinemock(self): + def test_is_child_AsyncMock(self): mock = MagicMock(spec_set=AsyncClass) - self.assertTrue(asyncio.iscoroutinefunction(mock.coroutine_method)) - self.assertTrue(asyncio.iscoroutinefunction(mock.decorated_cr_method)) + self.assertTrue(asyncio.iscoroutinefunction(mock.async_method)) self.assertFalse(asyncio.iscoroutinefunction(mock.normal_method)) - self.assertTrue(isinstance(mock.coroutine_method, CoroutineMock)) - self.assertTrue(isinstance(mock.decorated_cr_method, CoroutineMock)) + self.assertTrue(isinstance(mock.async_method, AsyncMock)) self.assertTrue(isinstance(mock.normal_method, MagicMock)) self.assertTrue(isinstance(mock, MagicMock)) -class CoroutineArguments(unittest.TestCase): - # I want to add more tests here with more complicate use-cases. +class AsyncArguments(unittest.TestCase): + # I want to add more tests here with more complicated use-cases. def setUp(self): self.old_policy = asyncio.events._event_loop_policy @@ -278,7 +230,7 @@ def test_add_return_value(self): async def addition(self, var): return var + 1 - mock = CoroutineMock(addition, return_value=10) + mock = AsyncMock(addition, return_value=10) output = asyncio.run(mock(5)) self.assertEqual(output, 10) @@ -286,13 +238,12 @@ async def addition(self, var): def test_add_side_effect_exception(self): async def addition(self, var): return var + 1 - - mock = CoroutineMock(addition, side_effect=Exception('err')) + mock = AsyncMock(addition, side_effect=Exception('err')) with self.assertRaises(Exception): asyncio.run(mock(5)) -class CoroutineMagicMethods(unittest.TestCase): +class AsyncMagicMethods(unittest.TestCase): class AsyncContextManager: def __init__(self): self.entered = False @@ -322,46 +273,11 @@ async def __anext__(self): raise StopAsyncIteration - # Before 3.7 __aiter__ was a coroutine - class AsyncItertorDeprecated(AsyncIterator): - async def __aiter__(self): - return super().__aiter__() - - def test_mock_magic_methods_are_coroutine_mocks(self): - mock_instance = CoroutineMock(spec=self.AsyncContextManager()) - self.assertIsInstance(mock_instance.__aenter__, - CoroutineMock) - self.assertIsInstance(mock_instance.__aexit__, - CoroutineMock) - - def test_mock_aiter_and_anext(self): - if sys.version_info < (3, 7): - instance = self.AsyncItertorDeprecated() - else: - instance = self.AsyncIterator() - mock_instance = CoroutineMock(instance) - - self.assertEqual(asyncio.iscoroutine(instance.__aiter__), - asyncio.iscoroutine(mock_instance.__aiter__)) - self.assertEqual(asyncio.iscoroutine(instance.__anext__), - asyncio.iscoroutine(mock_instance.__anext__)) - if sys.version_info < (3, 7): - iterator = instance.__aiter__() - iterator = run_coroutine(iterator) - - mock_iterator = mock_instance.__aiter__() - mock_iterator = run_coroutine(mock_iterator) - - self.assertEqual(asyncio.iscoroutine(iterator.__aiter__), - asyncio.iscoroutine(mock_iterator.__aiter__)) - self.assertEqual(asyncio.iscoroutine(iterator.__anext__), - asyncio.iscoroutine(mock_iterator.__anext__)) - -class CoroutineMockAssert(unittest.TestCase): +class AsyncMockAssert(unittest.TestCase): def setUp(self): - self.mock = CoroutineMock() + self.mock = AsyncMock() self.old_policy = asyncio.events._event_loop_policy def tearDown(self): diff --git a/Lib/unittest/test/testmock/testmock.py b/Lib/unittest/test/testmock/testmock.py index 75f174b8aa3928..5aceca356b98d1 100644 --- a/Lib/unittest/test/testmock/testmock.py +++ b/Lib/unittest/test/testmock/testmock.py @@ -8,7 +8,7 @@ from unittest.mock import ( call, DEFAULT, patch, sentinel, MagicMock, Mock, NonCallableMock, - NonCallableMagicMock, CoroutineMock, _CallList, + NonCallableMagicMock, AsyncMock, _CallList, create_autospec ) @@ -1378,7 +1378,7 @@ def test_mock_add_spec_magic_methods(self): def test_adding_child_mock(self): for Klass in (NonCallableMock, Mock, MagicMock, NonCallableMagicMock, - CoroutineMock): + AsyncMock): mock = Klass() mock.foo = Mock() From 24920a638058a0be6993900d750590692ab404eb Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Tue, 7 May 2019 11:42:58 -0400 Subject: [PATCH 14/24] Changes fnmatch to list comp. --- Lib/unittest/mock.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index b18d6688c36257..ccf1ad7713f3a3 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -30,7 +30,6 @@ import pprint import sys import builtins -import fnmatch from types import ModuleType, CodeType from functools import wraps, partial @@ -366,6 +365,7 @@ def __init__(self, *args, **kwargs): pass + class NonCallableMock(Base): """A non-callable version of `Mock`""" @@ -378,7 +378,10 @@ def __new__(cls, *args, **kw): # 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 = fnmatch.filter(bound_args.keys(), 'spec*') + spec_arg = [ + arg for arg in bound_args.keys() + if arg.startswith('spec') + ] if spec_arg: # what if spec_set is different than spec? if _is_async_obj(bound_args[spec_arg[0]]): @@ -387,6 +390,7 @@ def __new__(cls, *args, **kw): instance = object.__new__(new) return instance + def __init__( self, spec=None, wraps=None, name=None, spec_set=None, parent=None, _spec_state=None, _new_name='', _new_parent=None, @@ -431,6 +435,7 @@ def __init__( _spec_state ) + def attach_mock(self, mock, attribute): """ Attach a mock as an attribute of this one, replacing its name and From c0a88a90416225294f3913e03a64acfdf00fadf7 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Tue, 7 May 2019 14:47:27 -0400 Subject: [PATCH 15/24] Updates news with AsyncMock name change. --- Lib/unittest/test/testmock/testasync.py | 6 ------ .../next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 2821b5aa252692..47a2f475b02ae7 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -4,7 +4,6 @@ from unittest.mock import call, AsyncMock, patch, MagicMock -# TODO: lisa move 3.7 specific syntax to its own file class AsyncClass: def __init__(self): pass @@ -30,14 +29,9 @@ def a(self): class AsyncPatchDecoratorTest(unittest.TestCase): def test_is_coroutine_function_patch(self): - # @patch.object(AsyncClass, 'normal_method') - # def test_normal_method(mock_method): - # import types - # self.assertTrue(isinstance(mock_method, types.MethodType)) @patch.object(AsyncClass, 'async_method') def test_async(mock_method): self.assertTrue(asyncio.iscoroutinefunction(mock_method)) - # test_normal_method() test_async() def test_is_async_patch(self): diff --git a/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst b/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst index eb6651045c83db..8f74adbfafa200 100644 --- a/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst +++ b/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst @@ -1,2 +1,3 @@ -Added CoroutineMock to support using unittest to mock asyncio coroutines. + +Addedi AsyncMock to support using unittest to mock asyncio coroutines. Patch by Lisa Roach. From f9bee6ebd1d23bf0d47475af4141892bec53a5b9 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Tue, 7 May 2019 14:50:49 -0400 Subject: [PATCH 16/24] Removes extraneous comments. --- Lib/unittest/mock.py | 1 - Lib/unittest/test/testmock/testasync.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 074228f96d9472..633950874b6b2c 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -26,7 +26,6 @@ __version__ = '1.0' import asyncio -import io import inspect import pprint import sys diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 47a2f475b02ae7..4ae06997452eec 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -170,7 +170,7 @@ def test_target_async_spec_not(self): @patch.object(AsyncClass, 'async_method', spec=NormalClass.a) def test_async_attribute(mock_method): self.assertTrue(isinstance(mock_method, MagicMock)) - self.assertFalse(inspect.iscoroutine(mock_method)) # TODO: lisa? + self.assertFalse(inspect.iscoroutine(mock_method)) self.assertFalse(inspect.isawaitable(mock_method)) test_async_attribute() @@ -212,7 +212,6 @@ def test_is_child_AsyncMock(self): class AsyncArguments(unittest.TestCase): - # I want to add more tests here with more complicated use-cases. def setUp(self): self.old_policy = asyncio.events._event_loop_policy From 81ad0d1a7bf681b01db85899b2c807385db1099c Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 8 May 2019 15:57:58 -0400 Subject: [PATCH 17/24] Fixes RunTime warnings and missing io import. --- Lib/unittest/mock.py | 3 +- Lib/unittest/test/testmock/testasync.py | 68 +++++++++++++++++++++---- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 633950874b6b2c..c75dd915d37b9c 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -26,6 +26,7 @@ __version__ = '1.0' import asyncio +import io import inspect import pprint import sys @@ -2024,7 +2025,7 @@ async def _mock_call(_mock_self, *args, **kwargs): side_effect = self.side_effect if side_effect is not None and not callable(side_effect): raise - return _raise(e) + return await _raise(e) _call = self.call_args diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 4ae06997452eec..ceb0b2510b07be 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -28,6 +28,14 @@ def a(self): class AsyncPatchDecoratorTest(unittest.TestCase): + def setUp(self): + # Prevents altering the execution environment + self.old_policy = asyncio.events._event_loop_policy + + def tearDown(self): + # Restore the original event loop policy. + asyncio.events._event_loop_policy = self.old_policy + def test_is_coroutine_function_patch(self): @patch.object(AsyncClass, 'async_method') def test_async(mock_method): @@ -37,11 +45,15 @@ def test_async(mock_method): def test_is_async_patch(self): @patch.object(AsyncClass, 'async_method') def test_async(mock_method): - self.assertTrue(inspect.isawaitable(mock_method())) + m = mock_method() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) @patch(f'{async_foo_name}.async_method') def test_no_parent_attribute(mock_method): - self.assertTrue(inspect.isawaitable(mock_method())) + m = mock_method() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) test_async() test_no_parent_attribute() @@ -55,6 +67,13 @@ def test_async(mock_method): class AsyncPatchCMTest(unittest.TestCase): + def setUp(self): + self.old_policy = asyncio.events._event_loop_policy + + def tearDown(self): + # Restore the original event loop policy. + asyncio.events._event_loop_policy = self.old_policy + def test_is_async_function_cm(self): def test_async(): with patch.object(AsyncClass, 'async_method') as mock_method: @@ -65,7 +84,9 @@ def test_async(): def test_is_async_cm(self): def test_async(): with patch.object(AsyncClass, 'async_method') as mock_method: - self.assertTrue(inspect.isawaitable(mock_method())) + m = mock_method() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) test_async() @@ -78,6 +99,13 @@ def test_async(): class AsyncMockTest(unittest.TestCase): + def setUp(self): + self.old_policy = asyncio.events._event_loop_policy + + def tearDown(self): + # Restore the original event loop policy. + asyncio.events._event_loop_policy = self.old_policy + def test_iscoroutinefunction_default(self): mock = AsyncMock() self.assertTrue(asyncio.iscoroutinefunction(mock)) @@ -90,7 +118,9 @@ async def foo(): pass def test_isawaitable(self): mock = AsyncMock() - self.assertTrue(inspect.isawaitable(mock())) + m = mock() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) self.assertIn('assert_awaited', dir(mock)) def test_iscoroutinefunction_normal_function(self): @@ -120,35 +150,53 @@ def test_normal_method(mock_method): class AsyncSpecTest(unittest.TestCase): + def setUp(self): + self.old_policy = asyncio.events._event_loop_policy + + def tearDown(self): + # Restore the original event loop policy. + asyncio.events._event_loop_policy = self.old_policy def test_spec_as_async_positional_magicmock(self): mock = MagicMock(async_func) self.assertIsInstance(mock, MagicMock) - self.assertTrue(inspect.isawaitable(mock())) + m = mock() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) def test_spec_as_async_kw_magicmock(self): mock = MagicMock(spec=async_func) self.assertIsInstance(mock, MagicMock) - self.assertTrue(inspect.isawaitable(mock())) + m = mock() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) def test_spec_as_async_kw_AsyncMock(self): mock = AsyncMock(spec=async_func) self.assertIsInstance(mock, AsyncMock) - self.assertTrue(inspect.isawaitable(mock())) + m = mock() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) def test_spec_as_async_positional_AsyncMock(self): mock = AsyncMock(async_func) self.assertIsInstance(mock, AsyncMock) - self.assertTrue(inspect.isawaitable(mock())) + m = mock() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) def test_spec_as_normal_kw_AsyncMock(self): mock = AsyncMock(spec=normal_func) self.assertIsInstance(mock, AsyncMock) - self.assertTrue(inspect.isawaitable(mock())) + m = mock() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) def test_spec_as_normal_positional_AsyncMock(self): mock = AsyncMock(normal_func) self.assertIsInstance(mock, AsyncMock) - self.assertTrue(inspect.isawaitable(mock())) + m = mock() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) def test_spec_async_mock(self): @patch.object(AsyncClass, 'async_method', spec=True) From c2601049bea2f8335fe074a7ee76690fa081f2b6 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Wed, 8 May 2019 16:48:41 -0400 Subject: [PATCH 18/24] Changes check to use issubclass instead of !=. --- Lib/unittest/mock.py | 2 +- .../next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index c75dd915d37b9c..f1687ae8d559df 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -364,7 +364,7 @@ def __new__(cls, *args, **kw): # so we can create magic methods on the # class without stomping on other mocks bases = (cls,) - if cls != AsyncMock: + 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 diff --git a/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst b/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst index 8f74adbfafa200..ede9f7253d58ea 100644 --- a/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst +++ b/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst @@ -1,3 +1,3 @@ -Addedi AsyncMock to support using unittest to mock asyncio coroutines. +Added AsyncMock to support using unittest to mock asyncio coroutines. Patch by Lisa Roach. From ae13db16c2469ef7471e0087911f6bd796d3d965 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Mon, 13 May 2019 10:43:09 -0700 Subject: [PATCH 19/24] Adds AsyncMock docs and tests for iterators and context managers. --- Doc/library/unittest.mock.rst | 159 ++++++++++++++++ Lib/unittest/mock.py | 79 ++++---- Lib/unittest/test/testmock/testasync.py | 172 +++++++++++++++++- .../2018-09-13-20-33-24.bpo-26467.cahAk3.rst | 1 - 4 files changed, 364 insertions(+), 47 deletions(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index ed00ee6d0c2d83..a8d05b216c0628 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -851,6 +851,165 @@ object:: >>> p.assert_called_once_with() +.. class:: AsyncMock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, unsafe=False, **kwargs) + + An asynchronous version of :class:`Mock`. The :class:`AsyncMock` object will + behave so the object is recognized as an async function, and the result of a + call is an awaitable. + + >>> mock = AsyncMock() + >>> asyncio.iscoroutinefunction(mock) + True + >>> inspect.isawaitable(mock()) + True + + The result of ``mock()`` is an async function which will have the outcome + of ``side_effect`` or ``return_value``: + + - if ``side_effect`` is a function, the async function will return the + result of that function, + - if ``side_effect`` is an exception, the async function will raise the + exception, + - if ``side_effect`` is an iterable, the async function will return the + next value of the iterable, however, if the sequence of result is + exhausted, ``StopIteration`` is raised immediately, + - if ``side_effect`` is not defined, the async function will return the + value defined by ``return_value``, hence, by default, the async function + returns a new :class:`AsyncMock` object. + + + Setting the *spec* of a :class:`Mock` or :class:`MagicMock` to an async function + will result in a coroutine object being returned after calling. + + >>> async def async_func(): pass + ... + >>> mock = MagicMock(async_func) + >>> mock + + >>> mock() + + + .. method:: assert_awaited() + + Assert that the mock was awaited at least once. + + >>> mock = AsyncMock() + >>> async def main(): + ... await mock() + ... + >>> asyncio.run(main()) + >>> mock.assert_awaited() + >>> mock_2 = AsyncMock() + >>> mock_2.assert_awaited() + Traceback (most recent call last): + ... + AssertionError: Expected mock to have been awaited. + + .. method:: assert_awaited_once() + + Assert that the mock was awaited exactly once. + + >>> mock = AsyncMock() + >>> async def main(): + ... await mock() + ... + >>> asyncio.run(main()) + >>> mock.assert_awaited_once() + >>> asyncio.run(main()) + >>> mock.method.assert_awaited_once() + Traceback (most recent call last): + ... + AssertionError: Expected mock to have been awaited once. Awaited 2 times. + + .. method:: assert_awaited_with(*args, **kwargs) + + Assert that the last await was with the specified arguments. + + >>> mock = AsyncMock() + >>> async def main(*args, **kwargs): + ... await mock(*args, **kwargs) + ... + >>> asyncio.run(main('foo', bar='bar')) + >>> mock.assert_awaited_with('foo', bar='bar') + >>> mock.assert_awaited_with('other') + Traceback (most recent call last): + ... + AssertionError: expected call not found. + Expected: mock('other') + Actual: mock('foo', bar='bar') + + .. method:: assert_awaited_once_with(*args, **kwargs) + + Assert that the mock was awaited exactly once and with the specified + arguments. + + >>> mock = AsyncMock() + >>> async def main(*args, **kwargs): + ... await mock(*args, **kwargs) + ... + >>> asyncio.run(main('foo', bar='bar')) + >>> mock.assert_awaited_once_with('foo', bar='bar') + >>> asyncio.run(main('foo', bar='bar')) + >>> mock.assert_awaited_once_with('foo', bar='bar') + Traceback (most recent call last): + ... + AssertionError: Expected mock to have been awaited once. Awaited 2 times. + + .. method:: assert_any_await(*args, **kwargs) + + Assert the mock has ever been awaited with the specified arguments. + + >>> mock = AsyncMock() + >>> async def main(*args, **kwargs): + ... await mock(*args, **kwargs) + ... + >>> asyncio.run(main('foo', bar='bar')) + >>> asyncio.run(main('hello')) + >>> mock.assert_any_await('foo', bar='bar') + >>> mock.assert_any_await('other') + Traceback (most recent call last): + ... + AssertionError: mock('other') await not found + + .. method:: assert_has_awaits(calls, any_order=False) + + Assert the mock has been awaited with the specified calls. + The :attr:`await_args_list` list is checked for the awaits. + + If `any_order` is False (the default) then the awaits must be + sequential. There can be extra calls before or after the + specified awaits. + + If `any_order` is True then the awaits can be in any order, but + they must all appear in :attr:`await_args_list`. + + >>> mock = AsyncMock() + >>> async def main(*args, **kwargs): + ... await mock(*args, **kwargs) + ... + >>> calls = [call("foo"), call("bar")] + >>> mock.assert_has_calls(calls) + Traceback (most recent call last): + ... + AssertionError: Calls not found. + Expected: [call('foo'), call('bar')] + >>> asyncio.run(main('foo')) + >>> asyncio.run(main('bar')) + >>> mock.assert_has_calls(calls) + + .. method:: assert_not_awaited() + + Assert that the mock was never awaited. + + >>> mock = AsyncMock() + >>> mock.assert_not_awaited() + + .. method:: reset_mock(*args, **kwargs) + + See :func:`Mock.reset_mock`. Also sets :attr:`await_count` to 0, + :attr:`await_args` to None, and clears the :attr:`await_args_list`. + + Calling ~~~~~~~ diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index f1687ae8d559df..181a195afd8c38 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1818,6 +1818,7 @@ def method(self, *args, **kw): '__float__': 1.0, '__bool__': True, '__index__': 1, + '__aexit__': False, } @@ -1854,8 +1855,8 @@ def _get_async_iter(self): def __aiter__(): ret_val = self.__aiter__._mock_return_value if ret_val is DEFAULT: - return AsyncIterator([]) - return AsyncIterator(ret_val) + return _AsyncIterator(iter([])) + return _AsyncIterator(iter(ret_val)) return __aiter__ _side_effect_methods = { @@ -1927,8 +1928,33 @@ def mock_add_spec(self, spec, spec_set=False): self._mock_set_magics() +class AsyncMagicMixin: + def __init__(self, *args, **kw): + self._mock_set_async_magics() # make magic work for kwargs in init + _safe_super(AsyncMagicMixin, self).__init__(*args, **kw) + self._mock_set_async_magics() # fix magic broken by upper level init + + def _mock_set_async_magics(self): + these_magics = _async_magics + + if getattr(self, "_mock_methods", None) is not None: + these_magics = _async_magics.intersection(self._mock_methods) + remove_magics = _async_magics - these_magics + + for entry in remove_magics: + if entry in type(self).__dict__: + # remove unneeded magic methods + delattr(self, entry) + + # don't overwrite existing attributes if called a second time + these_magics = these_magics - set(type(self).__dict__) -class MagicMock(MagicMixin, Mock): + _type = type(self) + for entry in these_magics: + setattr(_type, entry, MagicProxy(entry, self)) + + +class MagicMock(MagicMixin, AsyncMagicMixin, Mock): """ MagicMock is a subclass of Mock with default implementations of most of the magic methods. You can use MagicMock without having to @@ -1968,32 +1994,6 @@ def __get__(self, obj, _type=None): return self.create_mock() -class AsyncMagicMixin: - def __init__(self, *args, **kw): - self._mock_set_async_magics() # make magic work for kwargs in init - _safe_super(AsyncMagicMixin, self).__init__(*args, **kw) - self._mock_set_async_magics() # fix magic broken by upper level init - - def _mock_set_async_magics(self): - these_magics = _async_magics - - if getattr(self, "_mock_methods", None) is not None: - these_magics = _async_magics.intersection(self._mock_methods) - remove_magics = _async_magics - these_magics - - for entry in remove_magics: - if entry in type(self).__dict__: - # remove unneeded magic methods - delattr(self, entry) - - # don't overwrite existing attributes if called a second time - these_magics = these_magics - set(type(self).__dict__) - - _type = type(self) - for entry in these_magics: - setattr(_type, entry, MagicProxy(entry, self)) - - class AsyncMockMixin(Base): awaited = _delegating_property('awaited') await_count = _delegating_property('await_count') @@ -2052,13 +2052,13 @@ def assert_awaited(_mock_self): msg = f"Expected {self._mock_name or 'mock'} to have been awaited." raise AssertionError(msg) - def assert_awaited_once(_mock_self, *args, **kwargs): + def assert_awaited_once(_mock_self): """ Assert that the mock was awaited exactly once. """ self = _mock_self if not self.await_count == 1: - msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once.", + msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once." f" Awaited {self.await_count} times.") raise AssertionError(msg) @@ -2088,7 +2088,7 @@ def assert_awaited_once_with(_mock_self, *args, **kwargs): """ self = _mock_self if not self.await_count == 1: - msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once.", + msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once." f" Awaited {self.await_count} times.") raise AssertionError(msg) return self.assert_awaited_with(*args, **kwargs) @@ -2150,7 +2150,7 @@ def assert_not_awaited(_mock_self): """ self = _mock_self if self.await_count != 0: - msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once.", + msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once." f" Awaited {self.await_count} times.") raise AssertionError(msg) @@ -2167,10 +2167,10 @@ def reset_mock(self, *args, **kwargs): class AsyncMock(AsyncMockMixin, AsyncMagicMixin, Mock): """ Enhance :class:`Mock` with features allowing to mock - a async function. + an async function. The :class:`AsyncMock` object will behave so the object is - recognized as async function, and the result of a call as a async: + recognized as an async function, and the result of a call is an awaitable: >>> mock = AsyncMock() >>> asyncio.iscoroutinefunction(mock) @@ -2193,8 +2193,8 @@ class AsyncMock(AsyncMockMixin, AsyncMagicMixin, Mock): value defined by ``return_value``, hence, by default, the async function returns a new :class:`AsyncMock` object. - If the outcome of ``side_effect`` or ``return_value`` is an async function, the - mock async function obtained when the mock object is called will be this + If the outcome of ``side_effect`` or ``return_value`` is an async function, + the mock async function obtained when the mock object is called will be this async function itself (and not an async function returning an async function). @@ -2757,20 +2757,19 @@ async def _raise(exception): raise exception -class AsyncIterator: +class _AsyncIterator: """ Wraps an iterator in an asynchronous iterator. """ def __init__(self, iterator): self.iterator = iterator code_mock = NonCallableMock(spec_set=CodeType) - code_mock.co_flags = inspect.CO_ITERATOR_COROUTINE + code_mock.co_flags = inspect.CO_ITERABLE_COROUTINE self.__dict__['__code__'] = code_mock def __aiter__(self): return self - # cannot use async before 3.7 async def __anext__(self): try: return next(self.iterator) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index ceb0b2510b07be..2674ffa7b73aed 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -129,6 +129,10 @@ def foo(): pass self.assertTrue(asyncio.iscoroutinefunction(mock)) self.assertTrue(inspect.iscoroutinefunction(mock)) + def test_future_isfuture(self): + mock = AsyncMock(asyncio.Future()) + self.assertIsInstance(mock, asyncio.Future) + class AsyncAutospecTest(unittest.TestCase): def test_is_AsyncMock_patch(self): @@ -156,6 +160,7 @@ def setUp(self): def tearDown(self): # Restore the original event loop policy. asyncio.events._event_loop_policy = self.old_policy + def test_spec_as_async_positional_magicmock(self): mock = MagicMock(async_func) self.assertIsInstance(mock, MagicMock) @@ -277,15 +282,42 @@ async def addition(self, var): self.assertEqual(output, 10) def test_add_side_effect_exception(self): - async def addition(self, var): + async def addition(var): return var + 1 mock = AsyncMock(addition, side_effect=Exception('err')) with self.assertRaises(Exception): asyncio.run(mock(5)) + def test_add_side_effect_function(self): + async def addition(var): + return var + 1 + mock = AsyncMock(side_effect=addition) + result = asyncio.run(mock(5)) + self.assertEqual(result, 6) + + def test_add_side_effect_iterable(self): + vals = [1, 2, 3] + mock = AsyncMock(side_effect=vals) + for item in vals: + self.assertEqual(item, asyncio.run(mock())) + + with self.assertRaises(RuntimeError) as e: + asyncio.run(mock()) + self.assertEqual( + e.exception, + RuntimeError('coroutine raised StopIteration') + ) + + +class AsyncContextManagerTest(unittest.TestCase): + def setUp(self): + self.old_policy = asyncio.events._event_loop_policy + + def tearDown(self): + # Restore the original event loop policy. + asyncio.events._event_loop_policy = self.old_policy -class AsyncMagicMethods(unittest.TestCase): - class AsyncContextManager: + class WithAsyncContextManager: def __init__(self): self.entered = False self.exited = False @@ -297,10 +329,93 @@ async def __aenter__(self, *args, **kwargs): async def __aexit__(self, *args, **kwargs): self.exited = True - class AsyncIterator(object): + def test_magic_methods_are_async_mocks(self): + mock = MagicMock(self.WithAsyncContextManager()) + self.assertIsInstance(mock.__aenter__, AsyncMock) + self.assertIsInstance(mock.__aexit__, AsyncMock) + + def test_mock_supports_async_context_manager(self): + called = False + instance = self.WithAsyncContextManager() + mock_instance = MagicMock(instance) + + async def use_context_manager(): + nonlocal called + async with mock_instance as result: + called = True + return result + + result = asyncio.run(use_context_manager()) + self.assertFalse(instance.entered) + self.assertFalse(instance.exited) + self.assertTrue(called) + self.assertTrue(mock_instance.entered) + self.assertTrue(mock_instance.exited) + self.assertTrue(mock_instance.__aenter__.called) + self.assertTrue(mock_instance.__aexit__.called) + self.assertIsNot(mock_instance, result) + self.assertIsInstance(result, AsyncMock) + + def test_mock_customize_async_context_manager(self): + instance = self.WithAsyncContextManager() + mock_instance = MagicMock(instance) + + expected_result = object() + mock_instance.__aenter__.return_value = expected_result + + async def use_context_manager(): + async with mock_instance as result: + return result + + self.assertIs(asyncio.run(use_context_manager()), expected_result) + + def test_mock_customize_async_context_manager_with_coroutine(self): + enter_called = False + exit_called = False + + async def enter_coroutine(*args): + nonlocal enter_called + enter_called = True + + async def exit_coroutine(*args): + nonlocal exit_called + exit_called = True + + instance = self.WithAsyncContextManager() + mock_instance = MagicMock(instance) + + mock_instance.__aenter__ = enter_coroutine + mock_instance.__aexit__ = exit_coroutine + + async def use_context_manager(): + async with mock_instance: + pass + + asyncio.run(use_context_manager()) + self.assertTrue(enter_called) + self.assertTrue(exit_called) + + def test_context_manager_raise_exception_by_default(self): + async def raise_in(context_manager): + async with context_manager: + raise TypeError() + + instance = self.WithAsyncContextManager() + mock_instance = MagicMock(instance) + with self.assertRaises(TypeError): + asyncio.run(raise_in(mock_instance)) + + +class AsyncIteratorTest(unittest.TestCase): + def setUp(self): + self.old_policy = asyncio.events._event_loop_policy + + def tearDown(self): + # Restore the original event loop policy. + asyncio.events._event_loop_policy = self.old_policy + + class WithAsyncIterator(object): def __init__(self): - self.iter_called = False - self.next_called = False self.items = ["foo", "NormalFoo", "baz"] def __aiter__(self): @@ -314,6 +429,51 @@ async def __anext__(self): raise StopAsyncIteration + def test_mock_aiter_and_anext(self): + instance = self.WithAsyncIterator() + mock_instance = MagicMock(instance) + + self.assertEqual(asyncio.iscoroutine(instance.__aiter__), + asyncio.iscoroutine(mock_instance.__aiter__)) + self.assertEqual(asyncio.iscoroutine(instance.__anext__), + asyncio.iscoroutine(mock_instance.__anext__)) + + iterator = instance.__aiter__() + if asyncio.iscoroutine(iterator): + iterator = asyncio.run(iterator) + + mock_iterator = mock_instance.__aiter__() + if asyncio.iscoroutine(mock_iterator): + mock_iterator = asyncio.run(mock_iterator) + + self.assertEqual(asyncio.iscoroutine(iterator.__aiter__), + asyncio.iscoroutine(mock_iterator.__aiter__)) + self.assertEqual(asyncio.iscoroutine(iterator.__anext__), + asyncio.iscoroutine(mock_iterator.__anext__)) + + # def test_mock_async_for(self): + # async def iterate(iterator): + # accumulator = [] + # async for item in iterator: + # accumulator.append(item) + # + # return accumulator + # + # expected = ["FOO", "BAR", "BAZ"] + # with self.subTest("iterate through default value"): + # mock_instance = MagicMock(self.WithAsyncIterator()) + # self.assertEqual([], asyncio.run(iterate(mock_instance))) + # + # with self.subTest("iterate through set return_value"): + # mock_instance = MagicMock(self.WithAsyncIterator()) + # mock_instance.__aiter__.return_value = expected[:] + # self.assertEqual(expected, asyncio.run(iterate(mock_instance))) + # + # with self.subTest("iterate through set return_value iterator"): + # mock_instance = MagicMock(self.WithAsyncIterator()) + # mock_instance.__aiter__.return_value = iter(expected[:]) + # self.assertEqual(expected, asyncio.run(iterate(mock_instance))) + class AsyncMockAssert(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst b/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst index ede9f7253d58ea..4cf3f2ae7ef7e6 100644 --- a/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst +++ b/Misc/NEWS.d/next/Library/2018-09-13-20-33-24.bpo-26467.cahAk3.rst @@ -1,3 +1,2 @@ - Added AsyncMock to support using unittest to mock asyncio coroutines. Patch by Lisa Roach. From 68dff1b3e66ecfe02fa4dc70cb8f6cb2180df7f2 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Mon, 13 May 2019 10:44:38 -0700 Subject: [PATCH 20/24] Uncomments commented out test. --- Lib/unittest/test/testmock/testasync.py | 44 ++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 2674ffa7b73aed..e1665996590bc0 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -451,28 +451,28 @@ def test_mock_aiter_and_anext(self): self.assertEqual(asyncio.iscoroutine(iterator.__anext__), asyncio.iscoroutine(mock_iterator.__anext__)) - # def test_mock_async_for(self): - # async def iterate(iterator): - # accumulator = [] - # async for item in iterator: - # accumulator.append(item) - # - # return accumulator - # - # expected = ["FOO", "BAR", "BAZ"] - # with self.subTest("iterate through default value"): - # mock_instance = MagicMock(self.WithAsyncIterator()) - # self.assertEqual([], asyncio.run(iterate(mock_instance))) - # - # with self.subTest("iterate through set return_value"): - # mock_instance = MagicMock(self.WithAsyncIterator()) - # mock_instance.__aiter__.return_value = expected[:] - # self.assertEqual(expected, asyncio.run(iterate(mock_instance))) - # - # with self.subTest("iterate through set return_value iterator"): - # mock_instance = MagicMock(self.WithAsyncIterator()) - # mock_instance.__aiter__.return_value = iter(expected[:]) - # self.assertEqual(expected, asyncio.run(iterate(mock_instance))) + def test_mock_async_for(self): + async def iterate(iterator): + accumulator = [] + async for item in iterator: + accumulator.append(item) + + return accumulator + + expected = ["FOO", "BAR", "BAZ"] + with self.subTest("iterate through default value"): + mock_instance = MagicMock(self.WithAsyncIterator()) + self.assertEqual([], asyncio.run(iterate(mock_instance))) + + with self.subTest("iterate through set return_value"): + mock_instance = MagicMock(self.WithAsyncIterator()) + mock_instance.__aiter__.return_value = expected[:] + self.assertEqual(expected, asyncio.run(iterate(mock_instance))) + + with self.subTest("iterate through set return_value iterator"): + mock_instance = MagicMock(self.WithAsyncIterator()) + mock_instance.__aiter__.return_value = iter(expected[:]) + self.assertEqual(expected, asyncio.run(iterate(mock_instance))) class AsyncMockAssert(unittest.TestCase): From 64301e2b0207542fae47e710fd32dd414b2d9db6 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Thu, 16 May 2019 21:17:56 -0700 Subject: [PATCH 21/24] Fixes based on comments. --- Doc/library/unittest.mock.rst | 64 ++++++++++++- Doc/whatsnew/3.8.rst | 4 + Lib/unittest/mock.py | 9 +- Lib/unittest/test/testmock/testasync.py | 116 ++++++++---------------- 4 files changed, 103 insertions(+), 90 deletions(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index a8d05b216c0628..9510d2e11d77fd 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -201,9 +201,11 @@ The Mock Class .. testsetup:: + import asyncio + import inspect import unittest from unittest.mock import sentinel, DEFAULT, ANY - from unittest.mock import patch, call, Mock, MagicMock, PropertyMock + from unittest.mock import patch, call, Mock, MagicMock, PropertyMock, AsyncMock from unittest.mock import mock_open :class:`Mock` is a flexible mock object intended to replace the use of stubs and @@ -885,9 +887,9 @@ object:: ... >>> mock = MagicMock(async_func) >>> mock - + >>> mock() - + .. method:: assert_awaited() @@ -976,11 +978,11 @@ object:: Assert the mock has been awaited with the specified calls. The :attr:`await_args_list` list is checked for the awaits. - If `any_order` is False (the default) then the awaits must be + If *any_order* is False (the default) then the awaits must be sequential. There can be extra calls before or after the specified awaits. - If `any_order` is True then the awaits can be in any order, but + If *any_order* is True then the awaits can be in any order, but they must all appear in :attr:`await_args_list`. >>> mock = AsyncMock() @@ -1009,6 +1011,58 @@ object:: See :func:`Mock.reset_mock`. Also sets :attr:`await_count` to 0, :attr:`await_args` to None, and clears the :attr:`await_args_list`. + .. attribute:: await_count + + An integer keeping track of how many times the mock object has been awaited. + + >>> mock = AsyncMock() + >>> async def main(): + ... await mock() + ... + >>> asyncio.run(main()) + >>> mock.await_count + 1 + >>> asyncio.run(main()) + >>> mock.await_count + 2 + + .. attribute:: await_args + + This is either ``None`` (if the mock hasn’t been awaited), or the arguments that + the mock was last awaited with. Functions the same as :attr:`Mock.call_args`. + + >>> mock = AsyncMock() + >>> async def main(*args): + ... await mock() + ... + >>> mock.await_args + >>> asyncio.run(main('foo')) + >>> mock.await_args + call('foo') + >>> asyncio.run(main('bar')) + >>> mock.await_args + call('bar') + + + .. attribute:: await_args_list + + This is a list of all the awaits made to the mock object in sequence (so the + length of the list is the number of times it has been awaited). Before any + awaits have been made it is an empty list. + + >>> mock = AsyncMock() + >>> async def main(*args): + ... await mock() + ... + >>> mock.await_args_list + [] + >>> asyncio.run(main('foo')) + >>> mock.await_args + [call('foo')] + >>> asyncio.run(main('bar')) + >>> mock.await_args_list + [call('foo'), call('bar')] + Calling ~~~~~~~ diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index d6388f8faaba4f..f3f9cedb5ce217 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -508,6 +508,10 @@ unicodedata unittest -------- +* XXX Added :class:`AsyncMock` to support an asynchronous version of :class:`Mock`. + Appropriate new assert functions for testing have been added as well. + (Contributed by Lisa Roach in :issue:`26467`). + * Added :func:`~unittest.addModuleCleanup()` and :meth:`~unittest.TestCase.addClassCleanup()` to unittest to support cleanups for :func:`~unittest.setUpModule()` and diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 181a195afd8c38..2f68b9ffa0d82e 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2429,7 +2429,6 @@ def call_list(self): call = _Call(from_kall=False) - def create_autospec(spec, spec_set=False, instance=False, _parent=None, _name=None, **kwargs): """Create a mock object using another object as a spec. Attributes on the @@ -2476,9 +2475,9 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # because we don't know what type they return _kwargs = {} elif is_async_func: - if instance: - raise RuntimeError("Instance can not be True when create_autospec " - "is mocking an async function") + # if instance: + # raise RuntimeError("Instance can not be True when create_autospec " + # "is mocking an async function") Klass = AsyncMock elif not _callable(spec): Klass = NonCallableMagicMock @@ -2501,7 +2500,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # recurse for functions mock = _set_signature(mock, spec) if is_async_func: - mock._is_coroutine = _is_coroutine + mock._is_coroutine = asyncio.coroutines._is_coroutine mock.await_count = 0 mock.await_args = None mock.await_args_list = _CallList() diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index e1665996590bc0..a9aa1434b963f1 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -2,7 +2,12 @@ import inspect import unittest -from unittest.mock import call, AsyncMock, patch, MagicMock +from unittest.mock import call, AsyncMock, patch, MagicMock, create_autospec + + +def tearDownModule(): + asyncio.set_event_loop_policy(None) + class AsyncClass: def __init__(self): @@ -28,14 +33,6 @@ def a(self): class AsyncPatchDecoratorTest(unittest.TestCase): - def setUp(self): - # Prevents altering the execution environment - self.old_policy = asyncio.events._event_loop_policy - - def tearDown(self): - # Restore the original event loop policy. - asyncio.events._event_loop_policy = self.old_policy - def test_is_coroutine_function_patch(self): @patch.object(AsyncClass, 'async_method') def test_async(mock_method): @@ -61,19 +58,12 @@ def test_no_parent_attribute(mock_method): def test_is_AsyncMock_patch(self): @patch.object(AsyncClass, 'async_method') def test_async(mock_method): - self.assertTrue(isinstance(mock_method, AsyncMock)) + self.assertIsInstance(mock_method, AsyncMock) test_async() class AsyncPatchCMTest(unittest.TestCase): - def setUp(self): - self.old_policy = asyncio.events._event_loop_policy - - def tearDown(self): - # Restore the original event loop policy. - asyncio.events._event_loop_policy = self.old_policy - def test_is_async_function_cm(self): def test_async(): with patch.object(AsyncClass, 'async_method') as mock_method: @@ -93,19 +83,12 @@ def test_async(): def test_is_AsyncMock_cm(self): def test_async(): with patch.object(AsyncClass, 'async_method') as mock_method: - self.assertTrue(isinstance(mock_method, AsyncMock)) + self.assertIsInstance(mock_method, AsyncMock) test_async() class AsyncMockTest(unittest.TestCase): - def setUp(self): - self.old_policy = asyncio.events._event_loop_policy - - def tearDown(self): - # Restore the original event loop policy. - asyncio.events._event_loop_policy = self.old_policy - def test_iscoroutinefunction_default(self): mock = AsyncMock() self.assertTrue(asyncio.iscoroutinefunction(mock)) @@ -130,7 +113,12 @@ def foo(): pass self.assertTrue(inspect.iscoroutinefunction(mock)) def test_future_isfuture(self): - mock = AsyncMock(asyncio.Future()) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + fut = asyncio.Future() + loop.stop() + loop.close() + mock = AsyncMock(fut) self.assertIsInstance(mock, asyncio.Future) @@ -138,29 +126,26 @@ class AsyncAutospecTest(unittest.TestCase): def test_is_AsyncMock_patch(self): @patch(async_foo_name, autospec=True) def test_async(mock_method): - self.assertTrue(isinstance( - mock_method.async_method, - AsyncMock)) - self.assertTrue(isinstance(mock_method, MagicMock)) + self.assertIsInstance(mock_method.async_method, AsyncMock) + self.assertIsInstance(mock_method, MagicMock) @patch(async_foo_name, autospec=True) def test_normal_method(mock_method): - self.assertTrue(isinstance( - mock_method.normal_method, - MagicMock)) + self.assertIsInstance(mock_method.normal_method, MagicMock) test_async() test_normal_method() + def test_create_autospec_instance(self): + with self.assertRaises(RuntimeError): + create_autospec(async_func, instance=True) -class AsyncSpecTest(unittest.TestCase): - def setUp(self): - self.old_policy = asyncio.events._event_loop_policy + def test_create_autospec(self): + spec = create_autospec(async_func) + self.assertTrue(asyncio.iscoroutinefunction(spec)) - def tearDown(self): - # Restore the original event loop policy. - asyncio.events._event_loop_policy = self.old_policy +class AsyncSpecTest(unittest.TestCase): def test_spec_as_async_positional_magicmock(self): mock = MagicMock(async_func) self.assertIsInstance(mock, MagicMock) @@ -206,23 +191,22 @@ def test_spec_as_normal_positional_AsyncMock(self): def test_spec_async_mock(self): @patch.object(AsyncClass, 'async_method', spec=True) def test_async(mock_method): - self.assertTrue(isinstance(mock_method, AsyncMock)) + self.assertIsInstance(mock_method, AsyncMock) test_async() def test_spec_parent_not_async_attribute_is(self): @patch(async_foo_name, spec=True) def test_async(mock_method): - self.assertTrue(isinstance(mock_method, MagicMock)) - self.assertTrue(isinstance(mock_method.async_method, - AsyncMock)) + self.assertIsInstance(mock_method, MagicMock) + self.assertIsInstance(mock_method.async_method, AsyncMock) test_async() def test_target_async_spec_not(self): @patch.object(AsyncClass, 'async_method', spec=NormalClass.a) def test_async_attribute(mock_method): - self.assertTrue(isinstance(mock_method, MagicMock)) + self.assertIsInstance(mock_method, MagicMock) self.assertFalse(inspect.iscoroutine(mock_method)) self.assertFalse(inspect.isawaitable(mock_method)) @@ -231,15 +215,14 @@ def test_async_attribute(mock_method): def test_target_not_async_spec_is(self): @patch.object(NormalClass, 'a', spec=async_func) def test_attribute_not_async_spec_is(mock_async_func): - self.assertTrue(isinstance(mock_async_func, AsyncMock)) + self.assertIsInstance(mock_async_func, AsyncMock) test_attribute_not_async_spec_is() def test_spec_async_attributes(self): @patch(normal_foo_name, spec=AsyncClass) def test_async_attributes_coroutines(MockNormalClass): - self.assertTrue(isinstance(MockNormalClass.async_method, - AsyncMock)) - self.assertTrue(isinstance(MockNormalClass, MagicMock)) + self.assertIsInstance(MockNormalClass.async_method, AsyncMock) + self.assertIsInstance(MockNormalClass, MagicMock) test_async_attributes_coroutines() @@ -248,30 +231,23 @@ class AsyncSpecSetTest(unittest.TestCase): def test_is_AsyncMock_patch(self): @patch.object(AsyncClass, 'async_method', spec_set=True) def test_async(async_method): - self.assertTrue(isinstance(async_method, AsyncMock)) + self.assertIsInstance(async_method, AsyncMock) def test_is_async_AsyncMock(self): mock = AsyncMock(spec_set=AsyncClass.async_method) self.assertTrue(asyncio.iscoroutinefunction(mock)) - self.assertTrue(isinstance(mock, AsyncMock)) + self.assertIsInstance(mock, AsyncMock) def test_is_child_AsyncMock(self): mock = MagicMock(spec_set=AsyncClass) self.assertTrue(asyncio.iscoroutinefunction(mock.async_method)) self.assertFalse(asyncio.iscoroutinefunction(mock.normal_method)) - self.assertTrue(isinstance(mock.async_method, AsyncMock)) - self.assertTrue(isinstance(mock.normal_method, MagicMock)) - self.assertTrue(isinstance(mock, MagicMock)) + self.assertIsInstance(mock.async_method, AsyncMock) + self.assertIsInstance(mock.normal_method, MagicMock) + self.assertIsInstance(mock, MagicMock) class AsyncArguments(unittest.TestCase): - def setUp(self): - self.old_policy = asyncio.events._event_loop_policy - - def tearDown(self): - # Restore the original event loop policy. - asyncio.events._event_loop_policy = self.old_policy - def test_add_return_value(self): async def addition(self, var): return var + 1 @@ -310,13 +286,6 @@ def test_add_side_effect_iterable(self): class AsyncContextManagerTest(unittest.TestCase): - def setUp(self): - self.old_policy = asyncio.events._event_loop_policy - - def tearDown(self): - # Restore the original event loop policy. - asyncio.events._event_loop_policy = self.old_policy - class WithAsyncContextManager: def __init__(self): self.entered = False @@ -407,13 +376,6 @@ async def raise_in(context_manager): class AsyncIteratorTest(unittest.TestCase): - def setUp(self): - self.old_policy = asyncio.events._event_loop_policy - - def tearDown(self): - # Restore the original event loop policy. - asyncio.events._event_loop_policy = self.old_policy - class WithAsyncIterator(object): def __init__(self): self.items = ["foo", "NormalFoo", "baz"] @@ -476,14 +438,8 @@ async def iterate(iterator): class AsyncMockAssert(unittest.TestCase): - def setUp(self): self.mock = AsyncMock() - self.old_policy = asyncio.events._event_loop_policy - - def tearDown(self): - # Restore the original event loop policy. - asyncio.events._event_loop_policy = self.old_policy async def _runnable_test(self, *args): if not args: From c7cd95e1881556453e9cd9a99946261ab38ac8ca Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Fri, 17 May 2019 15:41:52 -0700 Subject: [PATCH 22/24] Fixes broken docs. --- Doc/library/unittest.mock.rst | 4 ++-- Lib/unittest/mock.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index 9510d2e11d77fd..3a3276f5912cfa 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -1033,7 +1033,7 @@ object:: >>> mock = AsyncMock() >>> async def main(*args): - ... await mock() + ... await mock(*args) ... >>> mock.await_args >>> asyncio.run(main('foo')) @@ -1052,7 +1052,7 @@ object:: >>> mock = AsyncMock() >>> async def main(*args): - ... await mock() + ... await mock(*args) ... >>> mock.await_args_list [] diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 2f68b9ffa0d82e..43d95c3f6f62db 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2475,9 +2475,9 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # because we don't know what type they return _kwargs = {} elif is_async_func: - # if instance: - # raise RuntimeError("Instance can not be True when create_autospec " - # "is mocking an async function") + if instance: + raise RuntimeError("Instance can not be True when create_autospec " + "is mocking an async function") Klass = AsyncMock elif not _callable(spec): Klass = NonCallableMagicMock From 033f7d3c85df4f9074bfcddb4f888abd8175aa23 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Sat, 18 May 2019 15:45:03 -0700 Subject: [PATCH 23/24] Fixes broken doc await_arg. --- Doc/library/unittest.mock.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst index 3a3276f5912cfa..21e4709f81609e 100644 --- a/Doc/library/unittest.mock.rst +++ b/Doc/library/unittest.mock.rst @@ -1057,7 +1057,7 @@ object:: >>> mock.await_args_list [] >>> asyncio.run(main('foo')) - >>> mock.await_args + >>> mock.await_args_list [call('foo')] >>> asyncio.run(main('bar')) >>> mock.await_args_list From 2fef02c20f294b23d836ed217217ed90312a2d58 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Sat, 18 May 2019 15:59:08 -0700 Subject: [PATCH 24/24] Adds shoutout to Martin Richard for asynctest. --- Lib/unittest/mock.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 43d95c3f6f62db..2ebbcbe369631b 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -2202,6 +2202,8 @@ class AsyncMock(AsyncMockMixin, AsyncMagicMixin, Mock): case, the :class:`Mock` object behavior is the same as with an :class:`.Mock` object: the wrapped object may have methods defined as async function functions. + + Based on Martin Richard's asyntest project. """