From 8860004e2faa16434ede4e36c9314d12dd5a3eca Mon Sep 17 00:00:00 2001 From: Kurt Mosiejczuk Date: Mon, 16 Sep 2019 17:15:28 -0400 Subject: Include regression tests via MANIFEST.in so tests will be in PyPI tarball --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 7f47ab6..27027db 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include LICENSE.txt include *.rst +recursive-include mock/tests *.py -- cgit v1.2.3 From 8dd03de6ace6fed7f06b307d657872b8fb14188e Mon Sep 17 00:00:00 2001 From: Bulat Bochkariov Date: Mon, 23 Sep 2019 22:02:27 -0700 Subject: Fix a typo --- docs/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index 4e8bc17..27008a0 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -141,7 +141,7 @@ Backporting process git clone https://github.com/python/cpython.git git clone https://github.com/testing-cabal/mock.git - Make sure they both on master and up to date! + Make sure they are both on master and up to date! 2. Create a branch in your ``mock`` clone and switch to it. -- cgit v1.2.3 From d6b42149bb87cf38729eef8a100c473f602ef7fa Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 16 Oct 2019 08:22:07 +0100 Subject: this started failing when pypy 7.2 was released --- mock/tests/testmock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 5f6045a..15bac2e 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -826,6 +826,7 @@ class MockTest(unittest.TestCase): self.assertRaises(AttributeError, set_attr) + @unittest.skipIf('PyPy' in sys.version, "https://bitbucket.org/pypy/pypy/issues/3094") def test_copy(self): current = sys.getrecursionlimit() self.addCleanup(sys.setrecursionlimit, current) -- cgit v1.2.3 From 064c55c5b3026bcd7e29b1cf942912654777d6c4 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 16 Oct 2019 08:22:18 +0100 Subject: link to the pypy issue --- mock/tests/testhelpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mock/tests/testhelpers.py b/mock/tests/testhelpers.py index d56a47f..a5654ad 100644 --- a/mock/tests/testhelpers.py +++ b/mock/tests/testhelpers.py @@ -974,7 +974,7 @@ class SpecSignatureTest(unittest.TestCase): @unittest.skipIf('PyPy' in sys.version and sys.version_info > (3, 0), - "See https://github.com/testing-cabal/mock/issues/452") + "https://bitbucket.org/pypy/pypy/issues/3010") def test_autospec_on_bound_builtin_function(self): meth = six.create_bound_method(time.ctime, time.time()) self.assertIsInstance(meth(), str) -- cgit v1.2.3 From 57228528e69372cf622fc8a84f15302467d1a0bb Mon Sep 17 00:00:00 2001 From: Drew H Date: Thu, 7 Nov 2019 14:50:40 -0800 Subject: Remove redundant license option and fix typo --- setup.cfg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7283b79..42ba277 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,8 +5,7 @@ home-page = http://mock.readthedocs.org/en/latest/ description-file = README.rst author = Testing Cabal author-email = testing-in-python@lists.idyll.org -license = OSI Approved :: BSD License -classifier = +classifiers = Development Status :: 5 - Production/Stable Environment :: Console Intended Audience :: Developers -- cgit v1.2.3 From d1118fbc2a6044121625a9bc4a9a46a308b08f01 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 08:00:48 +0000 Subject: update list of supported versions. --- .circleci/config.yml | 32 +++++++++----------------------- README.rst | 2 +- docs/index.txt | 16 ++-------------- setup.cfg | 11 ++--------- tox.ini | 2 +- 5 files changed, 15 insertions(+), 48 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8416914..d3c6284 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,15 +24,6 @@ jobs: common: &common jobs: - - python/pip-run-tests: - name: python27 - image: circleci/python:2.7 - - python/pip-run-tests: - name: python34 - image: circleci/python:3.4 - - python/pip-run-tests: - name: python35 - image: circleci/python:3.5 - python/pip-run-tests: name: python36 image: circleci/python:3.6 @@ -40,8 +31,8 @@ common: &common name: python37 image: circleci/python:3.7 - python/pip-run-tests: - name: pypy27 - image: pypy:2.7 + name: python38 + image: circleci/python:3.8 - python/pip-run-tests: name: pypy36 image: pypy:3.6 @@ -49,12 +40,9 @@ common: &common - python/coverage: name: coverage requires: - - python27 - - python34 - - python35 - python36 - python37 - - pypy27 + - python38 - pypy36 - python/pip-docs: @@ -72,7 +60,7 @@ common: &common - check-package: name: check-package-python27 - image: circleci/python:2.7 + image: circleci/python:3.7 requires: - package @@ -83,16 +71,15 @@ common: &common - package - check-package: - name: check-package-pypy27 - image: pypy:2.7 - python: pypy + name: check-package-python38 + image: circleci/python:3.8 requires: - package - check-package: name: check-package-pypy36 - image: pypy:3.6 - python: pypy3 + image: pypy:2.7 + python: pypy requires: - package @@ -100,9 +87,8 @@ common: &common name: release config: .carthorse.yml requires: - - check-package-python27 - check-package-python37 - - check-package-pypy27 + - check-package-python38 - check-package-pypy36 workflows: diff --git a/README.rst b/README.rst index b4f3163..279f2dc 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ mock is now part of the Python standard library, available as `unittest.mock onwards. This package contains a rolling backport of the standard library mock code -compatible with Python 2.7 and 3.4 and up. +compatible with Python 3.6 and up. Please see the standard library documentation for more details. diff --git a/docs/index.txt b/docs/index.txt index 27008a0..f03f838 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -25,6 +25,8 @@ Python Version Compatibility * Version 2.0.0 is the last version offering official Jython support. +* version 3.0.5 is the last version supporting Python 3.5 and lower. + .. index:: installing .. _installing: @@ -77,9 +79,6 @@ Checkout from git (see :ref:`installing`) and submit pull requests. Committers can just push as desired: since all semantic development takes place in cPython, the backport process is as lightweight as we can make it. -mock is CI tested using Travis-CI on Python versions 2.7, 3.4, -3.5, 3.6, pypy, pypy3. - If you end up fixing anything backport-specific, please add an entry to the top of ``CHANGELOG.rst`` so it shows up in the next release notes. @@ -104,17 +103,6 @@ non-bugfix changes, patch on bugfix only changes. Backporting rules ----------------- -- ``isinstance`` checks in cPython to ``type`` need to check ``ClassTypes``. - Code calling ``obj.isidentifier`` needs to change to ``_isidentifier(obj)``. - -- f-strings need to be rewritten using some other string substitution. - -- ``assertRaisesRegex`` needs to be ``assertRaisesRegexp`` for Python 2. - -- If test code won't compile on a particular version of Python, move it to - a matching ``_py{version}.py`` file. If ``{version}`` isn't 3, adjust - ``conftest.py``. - - If code such as this causes coverage checking to drop below 100%: .. code-block:: python diff --git a/setup.cfg b/setup.cfg index 42ba277..647943c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,13 +12,9 @@ classifiers = License :: OSI Approved :: BSD License Operating System :: OS Independent Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Libraries @@ -28,10 +24,7 @@ keyword = testing, test, mock, mocking, unittest, patching, stubs, fakes, doubles [options] -install_requires = - six - funcsigs>=1;python_version<"3.3" -python_requires=>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +python_requires=>=3.6 packages = mock [options.extras_require] diff --git a/tox.ini b/tox.ini index 90ca455..14eb4f4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,pypy,py34,py35,py36,py37,docs +envlist = py36,py37,py38,docs [testenv] commands = -- cgit v1.2.3 From 4680a9654e8485dd5b8ed809af4ff0c62df3b493 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 08:02:15 +0000 Subject: move __version__ to __init__.py so we don't have to modify the mock.py from cpython. --- mock/__init__.py | 7 ++++++- mock/mock.py | 4 ---- release.py | 2 +- setup.py | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/mock/__init__.py b/mock/__init__.py index 8f383f0..0dfb165 100644 --- a/mock/__init__.py +++ b/mock/__init__.py @@ -1,4 +1,9 @@ from __future__ import absolute_import import mock.mock as _mock from mock.mock import * -__all__ = _mock.__all__ + +__version__ = '4.0.0b1' +version_info = tuple(int(p) for p in __version__.split('.')) + + +__all__ = ('__version__', 'version_info') + _mock.__all__ diff --git a/mock/mock.py b/mock/mock.py index 2d39253..79da3e5 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -34,8 +34,6 @@ from __future__ import absolute_import __all__ = ( - '__version__', - 'version_info', 'Mock', 'MagicMock', 'patch', @@ -69,8 +67,6 @@ from unittest.util import safe_repr import six from six import wraps -__version__ = '3.0.5' -version_info = tuple(int(p) for p in __version__.split('.')) import mock diff --git a/release.py b/release.py index 2556d50..13ab6a5 100644 --- a/release.py +++ b/release.py @@ -50,7 +50,7 @@ def news_to_changelog(version): def update_version(new_version): - path = join('mock', 'mock.py') + path = join('mock', '__init__.py') with open(path) as source: text = source.read() diff --git a/setup.py b/setup.py index d47345f..6f5ff41 100755 --- a/setup.py +++ b/setup.py @@ -5,6 +5,6 @@ import setuptools setuptools.setup( version=re.search("__version__ = '([^']+)'", - open(join('mock', 'mock.py')).read()).group(1), + open(join('mock', '__init__.py')).read()).group(1), long_description=open('README.rst').read(), ) -- cgit v1.2.3 From f8d22bc2a2cf4c41e51c7f99bb063195b8111501 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 08:02:35 +0000 Subject: fix release script to blank out lower version segments. --- release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.py b/release.py index 13ab6a5..7ef4f16 100644 --- a/release.py +++ b/release.py @@ -12,7 +12,7 @@ VERSION_TYPES = ['major', 'minor', 'bugfix'] def incremented_version(version_info, type_): type_index = VERSION_TYPES.index(type_) - version_info = tuple(e+(1 if i==type_index else 0) + version_info = tuple(0 if i>type_index else (e+(1 if i==type_index else 0)) for i, e in enumerate(version_info)) return '.'.join(str(p) for p in version_info) -- cgit v1.2.3 From 4195207c56eaa0f1707d1e64acc48967554f115f Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 08:11:28 +0000 Subject: support pre-releases in version_info --- mock/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mock/__init__.py b/mock/__init__.py index 0dfb165..1f29771 100644 --- a/mock/__init__.py +++ b/mock/__init__.py @@ -1,9 +1,13 @@ from __future__ import absolute_import + +import re + import mock.mock as _mock from mock.mock import * __version__ = '4.0.0b1' -version_info = tuple(int(p) for p in __version__.split('.')) +version_info = tuple(int(p) for p in + re.match(r'(\d+).(\d+).(\d+)', __version__).groups()) __all__ = ('__version__', 'version_info') + _mock.__all__ -- cgit v1.2.3 From ce5b6961d0b38b9f26e6bffabf46fadfc55696ba Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 08:19:08 +0000 Subject: new cut from cpython as at 4a686504eb2bbf69adf78077458508a7ba131667 --- lastsync.txt | 2 +- mock/mock.py | 847 ++++++++++++++++++++++++++--------------- mock/tests/__init__.py | 20 +- mock/tests/conftest.py | 6 - mock/tests/support.py | 30 -- mock/tests/testasync.py | 626 ++++++++++++++++++++++++++++++ mock/tests/testcallable.py | 12 +- mock/tests/testhelpers.py | 171 ++------- mock/tests/testhelpers_py3.py | 23 -- mock/tests/testmagicmethods.py | 168 +++----- mock/tests/testmock.py | 182 ++++----- mock/tests/testpatch.py | 133 +++---- mock/tests/testsealable.py | 2 +- mock/tests/testsentinel.py | 7 +- mock/tests/testsupport.py | 14 - mock/tests/testwith.py | 34 +- 16 files changed, 1424 insertions(+), 853 deletions(-) delete mode 100644 mock/tests/conftest.py create mode 100644 mock/tests/testasync.py delete mode 100644 mock/tests/testhelpers_py3.py delete mode 100644 mock/tests/testsupport.py diff --git a/lastsync.txt b/lastsync.txt index 1f18392..ff52fa8 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -11a8832c98b3db78727312154dd1d3ba76d639ec +4a686504eb2bbf69adf78077458508a7ba131667 diff --git a/mock/mock.py b/mock/mock.py index 79da3e5..be96194 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -1,37 +1,8 @@ # mock.py # Test tools for mocking and patching. -# E-mail: fuzzyman AT voidspace DOT org DOT uk -# -# http://www.voidspace.org.uk/python/mock/ -# -# Copyright (c) 2007-2013, Michael Foord & the mock team -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from __future__ import absolute_import +# Maintained by Michael Foord +# Backport for other versions of Python available from +# https://pypi.org/project/mock __all__ = ( 'Mock', @@ -42,8 +13,8 @@ __all__ = ( 'ANY', 'call', 'create_autospec', + 'AsyncMock', 'FILTER_DIR', - 'CallableMixin', 'NonCallableMock', 'NonCallableMagicMock', 'mock_open', @@ -52,77 +23,42 @@ __all__ = ( ) -from functools import partial +__version__ = '1.0' + +import asyncio +import contextlib import io import inspect import pprint import sys -try: - import builtins -except ImportError: - import __builtin__ as builtins -from types import ModuleType, MethodType +import builtins +from types import CodeType, ModuleType, MethodType from unittest.util import safe_repr - -import six -from six import wraps - - -import mock - -try: - inspectsignature = inspect.signature -except AttributeError: - import funcsigs - inspectsignature = funcsigs.signature - - -# TODO: use six. -try: - unicode -except NameError: - # Python 3 - basestring = unicode = str - -try: - long -except NameError: - # Python 3 - long = int - -if six.PY2: - # Python 2's next() can't handle a non-iterator with a __next__ method. - _next = next - def next(obj, _next=_next): - if getattr(obj, '__next__', None): - return obj.__next__() - return _next(obj) - - del _next +from functools import wraps, partial _builtins = {name for name in dir(builtins) if not name.startswith('_')} -try: - _isidentifier = str.isidentifier -except AttributeError: - # Python 2.X - import keyword - import re - regex = re.compile(r'^[a-z_][a-z0-9_]*$', re.I) - def _isidentifier(string): - if string in keyword.kwlist: - return False - return regex.match(string) - - -# NOTE: This FILTER_DIR is not used. The binding in mock.FILTER_DIR is. FILTER_DIR = True -# Workaround for Python issue #12370 +# Workaround for issue #12370 # Without this, the __class__ properties wouldn't be set correctly _safe_super = super +def _is_async_obj(obj): + if getattr(obj, '__code__', None): + return asyncio.iscoroutinefunction(obj) or inspect.isawaitable(obj) + else: + return False + + +def _is_async_func(func): + if getattr(func, '__code__', None): + return asyncio.iscoroutinefunction(func) + else: + return False + + def _is_instance_mock(obj): # can't use isinstance on Mock objects because they override __class__ # The base class for all mocks is NonCallableMock @@ -132,34 +68,19 @@ def _is_instance_mock(obj): def _is_exception(obj): return ( isinstance(obj, BaseException) or - isinstance(obj, ClassTypes) and issubclass(obj, BaseException) + isinstance(obj, type) and issubclass(obj, BaseException) ) -class _slotted(object): - __slots__ = ['a'] - - -# Do not use this tuple. It was never documented as a public API. -# It will be removed. It has no obvious signs of users on github. -DescriptorTypes = ( - type(_slotted.a), - property, -) - - def _get_signature_object(func, as_instance, eat_self): """ Given an arbitrary, possibly callable object, try to create a suitable signature object. Return a (reduced func, signature) tuple, or None. """ - if isinstance(func, ClassTypes) and not as_instance: + if isinstance(func, type) and not as_instance: # If it's a type and should be modelled as a type, use __init__. - try: - func = func.__init__ - except AttributeError: - return None + func = func.__init__ # Skip the `self` argument in __init__ eat_self = True elif not isinstance(func, FunctionTypes): @@ -173,9 +94,8 @@ def _get_signature_object(func, as_instance, eat_self): sig_func = partial(func, None) else: sig_func = func - try: - return func, inspectsignature(sig_func) + return func, inspect.signature(sig_func) except ValueError: # Certain callable types are not supported by inspect.signature() return None @@ -204,15 +124,10 @@ def _copy_func_details(func, funcopy): setattr(funcopy, attribute, getattr(func, attribute)) except AttributeError: pass - if six.PY2: - try: - funcopy.func_defaults = func.func_defaults - except AttributeError: - pass def _callable(obj): - if isinstance(obj, ClassTypes): + if isinstance(obj, type): return True if isinstance(obj, (staticmethod, classmethod, MethodType)): return _callable(obj.__func__) @@ -230,25 +145,15 @@ def _is_list(obj): def _instance_callable(obj): """Given an object, return True if the object is callable. For classes, return True if instances would be callable.""" - if not isinstance(obj, ClassTypes): + if not isinstance(obj, type): # already an instance return getattr(obj, '__call__', None) is not None - if six.PY3: - # *could* be broken by a class overriding __mro__ or __dict__ via - # a metaclass - for base in (obj,) + obj.__mro__: - if base.__dict__.get('__call__') is not None: - return True - else: - klass = obj - # uses __bases__ instead of __mro__ so that we work with old style classes - if klass.__dict__.get('__call__') is not None: + # *could* be broken by a class overriding __mro__ or __dict__ via + # a metaclass + for base in (obj,) + obj.__mro__: + if base.__dict__.get('__call__') is not None: return True - - for base in klass.__bases__: - if _instance_callable(base): - return True return False @@ -257,7 +162,7 @@ def _set_signature(mock, original, instance=False): # mock. It still does signature checking by calling a lambda with the same # signature as the original. - skipfirst = isinstance(original, ClassTypes) + skipfirst = isinstance(original, type) result = _get_signature_object(original, instance, skipfirst) if result is None: return mock @@ -267,13 +172,13 @@ def _set_signature(mock, original, instance=False): _copy_func_details(func, checksig) name = original.__name__ - if not _isidentifier(name): + if not name.isidentifier(): name = 'funcopy' context = {'_checksig_': checksig, 'mock': mock} src = """def %s(*args, **kwargs): _checksig_(*args, **kwargs) return mock(*args, **kwargs)""" % name - six.exec_(src, context) + exec (src, context) funcopy = context[name] _setup_func(funcopy, mock, sig) return funcopy @@ -282,14 +187,14 @@ def _set_signature(mock, original, instance=False): def _setup_func(funcopy, mock, sig): funcopy.mock = mock + def assert_called_with(*args, **kwargs): + return mock.assert_called_with(*args, **kwargs) def assert_called(*args, **kwargs): return mock.assert_called(*args, **kwargs) def assert_not_called(*args, **kwargs): return mock.assert_not_called(*args, **kwargs) def assert_called_once(*args, **kwargs): return mock.assert_called_once(*args, **kwargs) - def assert_called_with(*args, **kwargs): - return mock.assert_called_with(*args, **kwargs) def assert_called_once_with(*args, **kwargs): return mock.assert_called_once_with(*args, **kwargs) def assert_has_calls(*args, **kwargs): @@ -328,6 +233,34 @@ def _setup_func(funcopy, mock, sig): mock._mock_delegate = funcopy +def _setup_async_mock(mock): + mock._is_coroutine = asyncio.coroutines._is_coroutine + mock.await_count = 0 + mock.await_args = None + mock.await_args_list = _CallList() + mock.awaited = _AwaitEvent(mock) + + # Mock is not configured yet so the attributes are set + # to a function and then the corresponding mock helper function + # is called when the helper is accessed similar to _setup_func. + def wrapper(attr, *args, **kwargs): + return getattr(mock.mock, attr)(*args, **kwargs) + + for attribute in ('assert_awaited', + 'assert_awaited_once', + 'assert_awaited_with', + 'assert_awaited_once_with', + 'assert_any_await', + 'assert_has_awaits', + 'assert_not_awaited'): + + # setattr(mock, attribute, wrapper) causes late binding + # hence attribute will always be the last value in the loop + # Use partial(wrapper, attribute) to ensure the attribute is bound + # correctly. + setattr(mock, attribute, partial(wrapper, attribute)) + + def _is_magic(name): return '__%s__' % name[2:-2] == name @@ -341,11 +274,7 @@ class _SentinelObject(object): return 'sentinel.%s' % self.name def __reduce__(self): - return _unpickle_sentinel, (self.name, ) - - -def _unpickle_sentinel(name): - return getattr(sentinel, name) + return 'sentinel.%s' % self.name class _Sentinel(object): @@ -359,6 +288,9 @@ class _Sentinel(object): raise AttributeError return self._sentinels.setdefault(name, _SentinelObject(name)) + def __reduce__(self): + return 'sentinel' + sentinel = _Sentinel() @@ -367,15 +299,6 @@ _missing = sentinel.MISSING _deleted = sentinel.DELETED -class OldStyleClass: - pass -ClassType = type(OldStyleClass) - - -ClassTypes = (type,) -if six.PY2: - ClassTypes = (type, ClassType) - _allowed_names = { 'return_value', '_mock_return_value', 'side_effect', '_mock_side_effect', '_mock_parent', '_mock_new_parent', @@ -476,7 +399,20 @@ class NonCallableMock(Base): # 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 not issubclass(cls, AsyncMock): + # Check if spec is an async object or function + sig = inspect.signature(NonCallableMock.__init__) + bound_args = sig.bind_partial(cls, *args, **kw).arguments + spec_arg = [ + arg for arg in bound_args.keys() + if arg.startswith('spec') + ] + if spec_arg: + # what if spec_set is different than spec? + 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 @@ -552,9 +488,14 @@ class NonCallableMock(Base): _eat_self=False): _spec_class = None _spec_signature = None + _spec_asyncs = [] + + for attr in dir(spec): + if asyncio.iscoroutinefunction(getattr(spec, attr, None)): + _spec_asyncs.append(attr) if spec is not None and not _is_list(spec): - if isinstance(spec, ClassTypes): + if isinstance(spec, type): _spec_class = spec else: _spec_class = type(spec) @@ -569,7 +510,7 @@ class NonCallableMock(Base): __dict__['_spec_set'] = spec_set __dict__['_spec_signature'] = _spec_signature __dict__['_mock_methods'] = spec - + __dict__['_spec_asyncs'] = _spec_asyncs def __get_return_value(self): ret = self._mock_return_value @@ -631,7 +572,7 @@ class NonCallableMock(Base): side_effect = property(__get_side_effect, __set_side_effect) - def reset_mock(self, visited=None, return_value=False, side_effect=False): + def reset_mock(self, visited=None,*, return_value=False, side_effect=False): "Restore the mock object to its initial state." if visited is None: visited = [] @@ -684,7 +625,7 @@ class NonCallableMock(Base): def __getattr__(self, name): - if name in ('_mock_methods', '_mock_unsafe'): + if name in {'_mock_methods', '_mock_unsafe'}: raise AttributeError(name) elif self._mock_methods is not None: if name not in self._mock_methods or name in _all_magics: @@ -693,7 +634,8 @@ class NonCallableMock(Base): raise AttributeError(name) if not self._mock_unsafe: if name.startswith(('assert', 'assret')): - raise AttributeError(name) + raise AttributeError("Attributes cannot start with 'assert' " + "or 'assret'") result = self._mock_children.get(name) if result is _deleted: @@ -761,7 +703,7 @@ class NonCallableMock(Base): if self._spec_set: spec_string = ' spec_set=%r' spec_string = spec_string % self._spec_class.__name__ - return "<{}{}{} id='{}'>".format( + return "<%s%s%s id='%s'>" % ( type(self).__name__, name_string, spec_string, @@ -771,8 +713,7 @@ class NonCallableMock(Base): def __dir__(self): """Filter the output of `dir(mock)` to only useful members.""" - if not mock.FILTER_DIR and getattr(object, '__dir__', None): - # object.__dir__ is not in 2.7 + if not FILTER_DIR: return object.__dir__(self) extras = self._mock_methods or [] @@ -782,12 +723,9 @@ class NonCallableMock(Base): m_name for m_name, m_value in self._mock_children.items() if m_value is not _deleted] - if mock.FILTER_DIR: - # object.__dir__ is not in 2.7 - from_type = [e for e in from_type if not e.startswith('_')] - from_dict = [e for e in from_dict if not e.startswith('_') or - _is_magic(e)] - + from_type = [e for e in from_type if not e.startswith('_')] + from_dict = [e for e in from_dict if not e.startswith('_') or + _is_magic(e)] return sorted(set(extras + from_type + from_dict + from_child_mocks)) @@ -824,8 +762,8 @@ class NonCallableMock(Base): self._mock_children[name] = value if self._mock_sealed and not hasattr(self, name): - mock_name = self._extract_mock_name()+'.'+name - raise AttributeError('Cannot set '+mock_name) + mock_name = f'{self._extract_mock_name()}.{name}' + raise AttributeError(f'Cannot set {mock_name}') return object.__setattr__(self, name, value) @@ -853,12 +791,12 @@ class NonCallableMock(Base): return _format_call_signature(name, args, kwargs) - def _format_mock_failure_message(self, args, kwargs): - message = 'expected call not found.\nExpected: %s\nActual: %s' + def _format_mock_failure_message(self, args, kwargs, action='call'): + message = 'expected %s not found.\nExpected: %s\nActual: %s' expected_string = self._format_mock_call_signature(args, kwargs) call_args = self.call_args actual_string = self._format_mock_call_signature(*call_args) - return message % (expected_string, actual_string) + return message % (action, expected_string, actual_string) def _call_matcher(self, _call): @@ -878,8 +816,7 @@ class NonCallableMock(Base): try: return name, sig.bind(*args, **kwargs) except TypeError as e: - e.__traceback__ = None - return e + return e.with_traceback(None) else: return _call @@ -924,20 +861,17 @@ class NonCallableMock(Base): expected = self._format_mock_call_signature(args, kwargs) actual = 'not called.' error_message = ('expected call not found.\nExpected: %s\nActual: %s' - % (expected, actual)) + % (expected, actual)) raise AssertionError(error_message) - def _error_message(cause): + def _error_message(): msg = self._format_mock_failure_message(args, kwargs) - if six.PY2 and cause is not None: - # Tack on some diagnostics for Python without __cause__ - msg = '{}\n{}'.format(msg, str(cause)) return msg expected = self._call_matcher((args, kwargs)) actual = self._call_matcher(self.call_args) if expected != actual: cause = expected if isinstance(expected, Exception) else None - six.raise_from(AssertionError(_error_message(cause)), cause) + raise AssertionError(_error_message()) from cause def assert_called_once_with(_mock_self, *args, **kwargs): @@ -968,10 +902,10 @@ class NonCallableMock(Base): all_calls = _CallList(self._call_matcher(c) for c in self.mock_calls) if not any_order: if expected not in all_calls: - six.raise_from(AssertionError( + raise AssertionError( 'Calls not found.\nExpected: %r%s' % (_CallList(calls), self._calls_repr(prefix="Actual")) - ), cause) + ) from cause return all_calls = list(all_calls) @@ -983,11 +917,11 @@ class NonCallableMock(Base): except ValueError: not_found.append(kall) if not_found: - six.raise_from(AssertionError( + raise AssertionError( '%r does not contain all of %r in its call list, ' 'found %r instead' % (self._mock_name or 'mock', tuple(not_found), all_calls) - ), cause) + ) from cause def assert_any_call(self, *args, **kwargs): @@ -1001,9 +935,9 @@ class NonCallableMock(Base): if expected not in actual: cause = expected if isinstance(expected, Exception) else None expected_string = self._format_mock_call_signature(args, kwargs) - six.raise_from(AssertionError( + raise AssertionError( '%s call not found' % expected_string - ), cause) + ) from cause def _get_child_mock(self, **kw): @@ -1014,7 +948,15 @@ class NonCallableMock(Base): For non-callable mocks the callable variant will be used (rather than any custom subclass).""" + _new_name = kw.get("_new_name") + if _new_name in self.__dict__['_spec_asyncs']: + return AsyncMock(**kw) + _type = type(self) + 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): klass = MagicMock @@ -1041,7 +983,7 @@ class NonCallableMock(Base): """ if not self.mock_calls: return "" - return "\n"+prefix+": "+safe_repr(self.mock_calls)+"." + return f"\n{prefix}: {safe_repr(self.mock_calls)}." @@ -1060,14 +1002,12 @@ def _try_iter(obj): return obj - class CallableMixin(Base): 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 - _safe_super(CallableMixin, self).__init__( spec, wraps, name, spec_set, parent, _spec_state, _new_name, _new_parent, **kwargs @@ -1182,9 +1122,6 @@ class Mock(CallableMixin, NonCallableMock): arguments as the mock, and unless it returns `DEFAULT`, the return value of this function is used as the return value. - Alternatively `side_effect` can be an exception class or instance. In - this case the exception will be raised when the mock is called. - If `side_effect` is an iterable then each call to the mock will return the next value from the iterable. If any of the members of the iterable are exceptions they will be raised instead of returned. @@ -1212,7 +1149,6 @@ class Mock(CallableMixin, NonCallableMock): """ - def _dot_lookup(thing, comp, import_path): try: return getattr(thing, comp) @@ -1283,8 +1219,10 @@ class _patch(object): def __call__(self, func): - if isinstance(func, ClassTypes): + if isinstance(func, type): return self.decorate_class(func) + if inspect.iscoroutinefunction(func): + return self.decorate_async_callable(func) return self.decorate_callable(func) @@ -1302,41 +1240,68 @@ class _patch(object): return klass + @contextlib.contextmanager + def decoration_helper(self, patched, args, keywargs): + extra_args = [] + entered_patchers = [] + patching = None + + exc_info = tuple() + try: + for patching in patched.patchings: + arg = patching.__enter__() + entered_patchers.append(patching) + if patching.attribute_name is not None: + keywargs.update(arg) + elif patching.new is DEFAULT: + extra_args.append(arg) + + args += tuple(extra_args) + yield (args, keywargs) + except: + if (patching not in entered_patchers and + _is_started(patching)): + # the patcher may have been started, but an exception + # raised whilst entering one of its additional_patchers + entered_patchers.append(patching) + # Pass the exception to __exit__ + exc_info = sys.exc_info() + # re-raise the exception + raise + finally: + for patching in reversed(entered_patchers): + patching.__exit__(*exc_info) + + def decorate_callable(self, func): + # NB. Keep the method in sync with decorate_async_callable() if hasattr(func, 'patchings'): func.patchings.append(self) return func @wraps(func) def patched(*args, **keywargs): - extra_args = [] - entered_patchers = [] + with self.decoration_helper(patched, + args, + keywargs) as (newargs, newkeywargs): + return func(*newargs, **newkeywargs) + + patched.patchings = [self] + return patched - exc_info = tuple() - try: - for patching in patched.patchings: - arg = patching.__enter__() - entered_patchers.append(patching) - if patching.attribute_name is not None: - keywargs.update(arg) - elif patching.new is DEFAULT: - extra_args.append(arg) - - args += tuple(extra_args) - return func(*args, **keywargs) - except: - if (patching not in entered_patchers and - _is_started(patching)): - # the patcher may have been started, but an exception - # raised whilst entering one of its additional_patchers - entered_patchers.append(patching) - # Pass the exception to __exit__ - exc_info = sys.exc_info() - # re-raise the exception - raise - finally: - for patching in reversed(entered_patchers): - patching.__exit__(*exc_info) + + def decorate_async_callable(self, func): + # NB. Keep the method in sync with decorate_callable() + if hasattr(func, 'patchings'): + func.patchings.append(self) + return func + + @wraps(func) + async def patched(*args, **keywargs): + with self.decoration_helper(patched, + args, + keywargs) as (newargs, newkeywargs): + return await func(*newargs, **newkeywargs) patched.patchings = [self] return patched @@ -1361,7 +1326,7 @@ class _patch(object): if not self.create and original is DEFAULT: raise AttributeError( - "{} does not have the attribute {!r}".format(target, name) + "%s does not have the attribute %r" % (target, name) ) return original, local @@ -1407,11 +1372,13 @@ class _patch(object): if spec is not None or spec_set is not None: if original is DEFAULT: raise TypeError("Can't use 'spec' with create=True") - if isinstance(original, ClassTypes): + 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_async_obj(original): + Klass = AsyncMock + else: + Klass = MagicMock _kwargs = {} if new_callable is not None: Klass = new_callable @@ -1422,8 +1389,10 @@ class _patch(object): if _is_list(this_spec): not_callable = '__call__' not in this_spec else: - not_callable = not _callable(this_spec) - if not_callable: + not_callable = not callable(this_spec) + if _is_async_obj(this_spec): + Klass = AsyncMock + elif not_callable: Klass = NonCallableMagicMock if spec is not None: @@ -1592,7 +1561,7 @@ def _patch_multiple(target, spec=None, create=False, spec_set=None, When used as a class decorator `patch.multiple` honours `patch.TEST_PREFIX` for choosing which methods to wrap. """ - if type(target) in (unicode, str): + if type(target) is str: getter = lambda: _importer(target) else: getter = lambda: target @@ -1734,7 +1703,7 @@ class _patch_dict(object): def __call__(self, f): - if isinstance(f, ClassTypes): + if isinstance(f, type): return self.decorate_class(f) @wraps(f) def _inner(*args, **kw): @@ -1761,11 +1730,12 @@ class _patch_dict(object): def __enter__(self): """Patch the dict.""" self._patch_dict() + return self.in_dict def _patch_dict(self): values = self.values - if isinstance(self.in_dict, basestring): + if isinstance(self.in_dict, str): self.in_dict = _importer(self.in_dict) in_dict = self.in_dict clear = self.clear @@ -1845,34 +1815,26 @@ magic_methods = ( "divmod rdivmod neg pos abs invert " "complex int float index " "round trunc floor ceil " + "bool next " + "fspath " ) numerics = ( - "add sub mul matmul div floordiv mod lshift rshift and xor or pow" + "add sub mul matmul div floordiv mod lshift rshift and xor or pow truediv" ) -if six.PY3: - numerics += ' truediv' inplace = ' '.join('i%s' % n for n in numerics.split()) right = ' '.join('r%s' % n for n in numerics.split()) -extra = '' -if six.PY3: - extra = 'bool next ' - if sys.version_info >= (3, 6): - extra += 'fspath ' -else: - extra = 'unicode long nonzero oct hex truediv rtruediv ' # not including __prepare__, __instancecheck__, __subclasscheck__ # (as they are metaclass methods) # __del__ is not supported at all as it causes problems if it exists _non_defaults = { - '__cmp__', '__getslice__', '__setslice__', '__coerce__', # <3.x '__get__', '__set__', '__delete__', '__reversed__', '__missing__', '__reduce__', '__reduce_ex__', '__getinitargs__', '__getnewargs__', '__getstate__', '__setstate__', '__getformat__', '__setformat__', '__repr__', '__dir__', '__subclasses__', '__format__', - '__getnewargs_ex__', + '__getnewargs_ex__', '__aenter__', '__aexit__', '__anext__', '__aiter__', } @@ -1886,9 +1848,14 @@ def _get_method(name, func): _magics = { '__%s__' % method for method in - ' '.join([magic_methods, numerics, inplace, right, extra]).split() + ' '.join([magic_methods, numerics, inplace, right]).split() } +# 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 _unsupported_magics = { @@ -1902,8 +1869,7 @@ _calculate_return_value = { '__hash__': lambda self: object.__hash__(self), '__str__': lambda self: object.__str__(self), '__sizeof__': lambda self: object.__sizeof__(self), - '__unicode__': lambda self: unicode(object.__str__(self)), - '__fspath__': lambda self: type(self).__name__+'/'+self._extract_mock_name()+'/'+str(id(self)), + '__fspath__': lambda self: f"{type(self).__name__}/{self._extract_mock_name()}/{id(self)}", } _return_values = { @@ -1918,11 +1884,8 @@ _return_values = { '__complex__': 1j, '__float__': 1.0, '__bool__': True, - '__nonzero__': True, - '__oct__': '1', - '__hex__': '0x1', - '__long__': long(1), '__index__': 1, + '__aexit__': False, } @@ -1955,10 +1918,19 @@ def _get_iter(self): return iter(ret_val) return __iter__ +def _get_async_iter(self): + def __aiter__(): + ret_val = self.__aiter__._mock_return_value + if ret_val is DEFAULT: + return _AsyncIterator(iter([])) + return _AsyncIterator(iter(ret_val)) + return __aiter__ + _side_effect_methods = { '__eq__': _get_eq, '__ne__': _get_ne, '__iter__': _get_iter, + '__aiter__': _get_async_iter } @@ -1971,13 +1943,7 @@ def _set_return_value(mock, method, name): return_calulator = _calculate_return_value.get(name) if return_calulator is not None: - try: - return_value = return_calulator(mock) - except AttributeError: - # XXXX why do we return AttributeError here? - # set it as a side_effect instead? - # Answer: it makes magic mocks work on pypy?! - return_value = AttributeError(name) + return_value = return_calulator(mock) method.return_value = return_value return @@ -2029,8 +1995,33 @@ class NonCallableMagicMock(MagicMixin, NonCallableMock): 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__) + + _type = type(self) + for entry in these_magics: + setattr(_type, entry, MagicProxy(entry, self)) + -class MagicMock(MagicMixin, Mock): +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 @@ -2070,6 +2061,218 @@ class MagicProxy(object): return self.create_mock() +class AsyncMockMixin(Base): + awaited = _delegating_property('awaited') + 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 _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'] = 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 = inspect.CO_COROUTINE + self.__dict__['__code__'] = code_mock + + async def _mock_call(_mock_self, *args, **kwargs): + self = _mock_self + try: + result = super()._mock_call(*args, **kwargs) + except (BaseException, StopIteration) as e: + side_effect = self.side_effect + if side_effect is not None and not callable(side_effect): + raise + return await _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 await proxy() + + def assert_awaited(_mock_self): + """ + Assert that the mock was awaited at least once. + """ + 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): + """ + 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." + 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. + """ + 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, action='await') + 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. + """ + 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. + """ + 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`. + """ + 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. + """ + self = _mock_self + if self.await_count != 0: + msg = (f"Expected {self._mock_name or 'mock'} to not have been awaited." + 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.await_count = 0 + self.await_args = None + self.await_args_list = _CallList() + + +class AsyncMock(AsyncMockMixin, AsyncMagicMixin, Mock): + """ + Enhance :class:`Mock` with features allowing to mock + an async function. + + 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. + + 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 async function functions. + + Based on Martin Richard's asynctest project. + """ + class _ANY(object): "A helper object that compares equal to everything." @@ -2083,8 +2286,6 @@ class _ANY(object): def __repr__(self): return '' - __hash__ = None - ANY = _ANY() @@ -2093,15 +2294,8 @@ def _format_call_signature(name, args, kwargs): message = '%s(%%s)' % name formatted_args = '' args_string = ', '.join([repr(arg) for arg in args]) - - def encode_item(item): - if six.PY2 and isinstance(item, unicode): - return item.encode("utf-8") - else: - return item - kwargs_string = ', '.join([ - '{}={!r}'.format(encode_item(key), value) for key, value in sorted(kwargs.items()) + '%s=%r' % (key, value) for key, value in sorted(kwargs.items()) ]) if args_string: formatted_args = args_string @@ -2142,7 +2336,7 @@ class _Call(tuple): name, args, kwargs = value elif _len == 2: first, second = value - if isinstance(first, basestring): + if isinstance(first, str): name = first if isinstance(second, tuple): args = second @@ -2152,7 +2346,7 @@ class _Call(tuple): args, kwargs = first, second elif _len == 1: value, = value - if isinstance(value, basestring): + if isinstance(value, str): name = value elif isinstance(value, tuple): args = value @@ -2200,7 +2394,7 @@ class _Call(tuple): if isinstance(value, tuple): other_args = value other_kwargs = {} - elif isinstance(value, basestring): + elif isinstance(value, str): other_name = value other_args, other_kwargs = (), {} else: @@ -2209,7 +2403,7 @@ class _Call(tuple): elif len_other == 2: # could be (name, args) or (name, kwargs) or (args, kwargs) first, second = other - if isinstance(first, basestring): + if isinstance(first, str): other_name = first if isinstance(second, tuple): other_args, other_kwargs = second, {} @@ -2227,10 +2421,8 @@ class _Call(tuple): return (other_args, other_kwargs) == (self_args, self_kwargs) - def __ne__(self, other): - return not self.__eq__(other) + __ne__ = object.__ne__ - __hash__ = None def __call__(self, *args, **kwargs): if self._mock_name is None: @@ -2243,7 +2435,7 @@ class _Call(tuple): def __getattr__(self, attr): if self._mock_name is None: return _Call(name=attr, from_kall=False) - name = '{}.{}'.format(self._mock_name, attr) + name = '%s.%s' % (self._mock_name, attr) return _Call(name=name, parent=self, from_kall=False) @@ -2306,7 +2498,6 @@ class _Call(tuple): 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 @@ -2331,8 +2522,8 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # interpreted as a list of strings spec = type(spec) - is_type = isinstance(spec, ClassTypes) - + is_type = isinstance(spec, type) + is_async_func = _is_async_func(spec) _kwargs = {'spec': spec} if spec_set: _kwargs = {'spec_set': spec} @@ -2349,6 +2540,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_async_func: + 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 elif is_type and instance and not _instance_callable(spec): @@ -2368,6 +2564,8 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, # should only happen at the top level because we don't # recurse for functions mock = _set_signature(mock, spec) + if is_async_func: + _setup_async_mock(mock) else: _check_signature(spec, mock, is_type, instance) @@ -2379,12 +2577,6 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, _name='()', _parent=mock) for entry in dir(spec): - - # This are __ and so treated as magic on Py3, on Py2 we need to - # explicitly ignore them: - if six.PY2 and (entry.startswith('im_') or entry.startswith('func_')): - continue - if _is_magic(entry): # MagicMock already does the useful magic methods for us continue @@ -2417,9 +2609,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 = AsyncMock + 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) @@ -2438,14 +2634,11 @@ def _must_skip(spec, entry, is_type): Return whether we should skip the first argument on spec's `entry` attribute. """ - if not isinstance(spec, ClassTypes): + if not isinstance(spec, type): if entry in getattr(spec, '__dict__', {}): # instance attribute - shouldn't skip return False spec = spec.__class__ - if not hasattr(spec, '__mro__'): - # old style class: can't have descriptors anyway - return is_type for klass in spec.__mro__: result = klass.__dict__.get(entry, DEFAULT) @@ -2523,9 +2716,8 @@ def mock_open(mock=None, read_data=''): return handle.read.return_value return _state[0].read(*args, **kwargs) - def _readline_side_effect(*args, **kwargs): - for item in _iter_side_effect(): - yield item + def _readline_side_effect(*args, **kwargs): + yield from _iter_side_effect() while True: yield _state[0].readline(*args, **kwargs) @@ -2536,14 +2728,15 @@ def mock_open(mock=None, read_data=''): for line in _state[0]: yield line + def _next_side_effect(): + if handle.readline.return_value is not None: + return handle.readline.return_value + return next(_state[0]) + global file_spec if file_spec is None: - # set on first use - if six.PY3: - import _io - file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) - else: - file_spec = file + import _io + file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) if mock is None: mock = MagicMock(name='open', spec=open) @@ -2561,6 +2754,7 @@ def mock_open(mock=None, read_data=''): handle.readline.side_effect = _state[1] handle.readlines.side_effect = _readlines_side_effect handle.__iter__.side_effect = _iter_side_effect + handle.__next__.side_effect = _next_side_effect def reset_data(*args, **kwargs): _state[0] = _to_stream(read_data) @@ -2613,3 +2807,60 @@ 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_ITERABLE_COROUTINE + self.__dict__['__code__'] = code_mock + + def __aiter__(self): + return self + + 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/mock/tests/__init__.py b/mock/tests/__init__.py index 54ddf2e..87d7ae9 100644 --- a/mock/tests/__init__.py +++ b/mock/tests/__init__.py @@ -1,3 +1,17 @@ -# Copyright (C) 2007-2012 Michael Foord & the mock team -# E-mail: fuzzyman AT voidspace DOT org DOT uk -# http://www.voidspace.org.uk/python/mock/ +import os +import sys +import unittest + + +here = os.path.dirname(__file__) +loader = unittest.defaultTestLoader + +def load_tests(*args): + suite = unittest.TestSuite() + for fn in os.listdir(here): + if fn.startswith("test") and fn.endswith(".py"): + modname = "unittest.test.testmock." + fn[:-3] + __import__(modname) + module = sys.modules[modname] + suite.addTest(loader.loadTestsFromModule(module)) + return suite diff --git a/mock/tests/conftest.py b/mock/tests/conftest.py deleted file mode 100644 index 78831f6..0000000 --- a/mock/tests/conftest.py +++ /dev/null @@ -1,6 +0,0 @@ -import six - - -def pytest_ignore_collect(path): - if 'py3' in path.basename and six.PY2: - return True diff --git a/mock/tests/support.py b/mock/tests/support.py index d57a372..49986d6 100644 --- a/mock/tests/support.py +++ b/mock/tests/support.py @@ -1,7 +1,3 @@ -import contextlib -import sys - - target = {'foo': 'FOO'} @@ -18,29 +14,3 @@ class SomeClass(object): class X(object): pass - - -@contextlib.contextmanager -def uncache(*names): - """Uncache a module from sys.modules. - - A basic sanity check is performed to prevent uncaching modules that either - cannot/shouldn't be uncached. - - """ - for name in names: - if name in ('sys', 'marshal', 'imp'): - raise ValueError( - "cannot uncache {0}".format(name)) - try: - del sys.modules[name] - except KeyError: - pass - try: - yield - finally: - for name in names: - try: - del sys.modules[name] - except KeyError: - pass diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py new file mode 100644 index 0000000..fa906e4 --- /dev/null +++ b/mock/tests/testasync.py @@ -0,0 +1,626 @@ +import asyncio +import inspect +import unittest + +from unittest.mock import (call, AsyncMock, patch, MagicMock, create_autospec, + _AwaitEvent) + + +def tearDownModule(): + asyncio.set_event_loop_policy(None) + + +class AsyncClass: + def __init__(self): + pass + async def async_method(self): + pass + def normal_method(self): + pass + +async def async_func(): + pass + +async def async_func_args(a, b, *, c): + pass + +def normal_func(): + pass + +class NormalClass(object): + def a(self): + pass + + +async_foo_name = f'{__name__}.AsyncClass' +normal_foo_name = f'{__name__}.NormalClass' + + +class AsyncPatchDecoratorTest(unittest.TestCase): + def test_is_coroutine_function_patch(self): + @patch.object(AsyncClass, 'async_method') + def test_async(mock_method): + self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + test_async() + + def test_is_async_patch(self): + @patch.object(AsyncClass, 'async_method') + def test_async(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): + m = mock_method() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) + + test_async() + test_no_parent_attribute() + + def test_is_AsyncMock_patch(self): + @patch.object(AsyncClass, 'async_method') + def test_async(mock_method): + self.assertIsInstance(mock_method, AsyncMock) + + test_async() + + def test_async_def_patch(self): + @patch(f"{__name__}.async_func", AsyncMock()) + async def test_async(): + self.assertIsInstance(async_func, AsyncMock) + + asyncio.run(test_async()) + self.assertTrue(inspect.iscoroutinefunction(async_func)) + + +class AsyncPatchCMTest(unittest.TestCase): + def test_is_async_function_cm(self): + def test_async(): + with patch.object(AsyncClass, 'async_method') as mock_method: + self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + + test_async() + + def test_is_async_cm(self): + def test_async(): + with patch.object(AsyncClass, 'async_method') as mock_method: + m = mock_method() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) + + test_async() + + def test_is_AsyncMock_cm(self): + def test_async(): + with patch.object(AsyncClass, 'async_method') as mock_method: + self.assertIsInstance(mock_method, AsyncMock) + + test_async() + + def test_async_def_cm(self): + async def test_async(): + with patch(f"{__name__}.async_func", AsyncMock()): + self.assertIsInstance(async_func, AsyncMock) + self.assertTrue(inspect.iscoroutinefunction(async_func)) + + asyncio.run(test_async()) + + +class AsyncMockTest(unittest.TestCase): + def test_iscoroutinefunction_default(self): + mock = AsyncMock() + self.assertTrue(asyncio.iscoroutinefunction(mock)) + + def test_iscoroutinefunction_function(self): + async def foo(): pass + mock = AsyncMock(foo) + self.assertTrue(asyncio.iscoroutinefunction(mock)) + self.assertTrue(inspect.iscoroutinefunction(mock)) + + def test_isawaitable(self): + mock = AsyncMock() + m = mock() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) + self.assertIn('assert_awaited', dir(mock)) + + def test_iscoroutinefunction_normal_function(self): + def foo(): pass + mock = AsyncMock(foo) + self.assertTrue(asyncio.iscoroutinefunction(mock)) + self.assertTrue(inspect.iscoroutinefunction(mock)) + + def test_future_isfuture(self): + 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) + + +class AsyncAutospecTest(unittest.TestCase): + def test_is_AsyncMock_patch(self): + @patch(async_foo_name, autospec=True) + def test_async(mock_method): + 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.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) + + def test_create_autospec(self): + spec = create_autospec(async_func_args) + awaitable = spec(1, 2, c=3) + async def main(): + await awaitable + + self.assertEqual(spec.await_count, 0) + self.assertIsNone(spec.await_args) + self.assertEqual(spec.await_args_list, []) + self.assertIsInstance(spec.awaited, _AwaitEvent) + spec.assert_not_awaited() + + asyncio.run(main()) + + self.assertTrue(asyncio.iscoroutinefunction(spec)) + self.assertTrue(asyncio.iscoroutine(awaitable)) + self.assertEqual(spec.await_count, 1) + self.assertEqual(spec.await_args, call(1, 2, c=3)) + self.assertEqual(spec.await_args_list, [call(1, 2, c=3)]) + spec.assert_awaited_once() + spec.assert_awaited_once_with(1, 2, c=3) + spec.assert_awaited_with(1, 2, c=3) + spec.assert_awaited() + + def test_patch_with_autospec(self): + + async def test_async(): + with patch(f"{__name__}.async_func_args", autospec=True) as mock_method: + awaitable = mock_method(1, 2, c=3) + self.assertIsInstance(mock_method.mock, AsyncMock) + + self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + self.assertTrue(asyncio.iscoroutine(awaitable)) + self.assertTrue(inspect.isawaitable(awaitable)) + + # Verify the default values during mock setup + self.assertEqual(mock_method.await_count, 0) + self.assertEqual(mock_method.await_args_list, []) + self.assertIsNone(mock_method.await_args) + self.assertIsInstance(mock_method.awaited, _AwaitEvent) + mock_method.assert_not_awaited() + + await awaitable + + self.assertEqual(mock_method.await_count, 1) + self.assertEqual(mock_method.await_args, call(1, 2, c=3)) + self.assertEqual(mock_method.await_args_list, [call(1, 2, c=3)]) + mock_method.assert_awaited_once() + mock_method.assert_awaited_once_with(1, 2, c=3) + mock_method.assert_awaited_with(1, 2, c=3) + mock_method.assert_awaited() + + mock_method.reset_mock() + self.assertEqual(mock_method.await_count, 0) + self.assertIsNone(mock_method.await_args) + self.assertEqual(mock_method.await_args_list, []) + + asyncio.run(test_async()) + + +class AsyncSpecTest(unittest.TestCase): + def test_spec_as_async_positional_magicmock(self): + mock = MagicMock(async_func) + self.assertIsInstance(mock, MagicMock) + 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) + 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) + 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) + 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) + 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) + m = mock() + self.assertTrue(inspect.isawaitable(m)) + asyncio.run(m) + + def test_spec_async_mock(self): + @patch.object(AsyncClass, 'async_method', spec=True) + def test_async(mock_method): + 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.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.assertIsInstance(mock_method, MagicMock) + self.assertFalse(inspect.iscoroutine(mock_method)) + self.assertFalse(inspect.isawaitable(mock_method)) + + test_async_attribute() + + 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.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.assertIsInstance(MockNormalClass.async_method, AsyncMock) + self.assertIsInstance(MockNormalClass, MagicMock) + + test_async_attributes_coroutines() + + +class AsyncSpecSetTest(unittest.TestCase): + def test_is_AsyncMock_patch(self): + @patch.object(AsyncClass, 'async_method', spec_set=True) + def test_async(async_method): + self.assertIsInstance(async_method, AsyncMock) + + def test_is_async_AsyncMock(self): + mock = AsyncMock(spec_set=AsyncClass.async_method) + self.assertTrue(asyncio.iscoroutinefunction(mock)) + 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.assertIsInstance(mock.async_method, AsyncMock) + self.assertIsInstance(mock.normal_method, MagicMock) + self.assertIsInstance(mock, MagicMock) + + +class AsyncArguments(unittest.TestCase): + def test_add_return_value(self): + async def addition(self, var): + return var + 1 + + mock = AsyncMock(addition, return_value=10) + output = asyncio.run(mock(5)) + + self.assertEqual(output, 10) + + def test_add_side_effect_exception(self): + 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): + class WithAsyncContextManager: + 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 + + 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): + class WithAsyncIterator(object): + def __init__(self): + 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_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): + def setUp(self): + self.mock = AsyncMock() + + 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): + self.mock.assert_awaited() + + 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()) + self.mock.assert_awaited_once() + + 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()) + msg = 'expected await not found' + with self.assertRaisesRegex(AssertionError, msg): + self.mock.assert_awaited_with('foo') + + asyncio.run(self._runnable_test('foo')) + self.mock.assert_awaited_with('foo') + + asyncio.run(self._runnable_test('SomethingElse')) + with self.assertRaises(AssertionError): + self.mock.assert_awaited_with('foo') + + def test_assert_awaited_once_with(self): + with self.assertRaises(AssertionError): + self.mock.assert_awaited_once_with('foo') + + asyncio.run(self._runnable_test('foo')) + self.mock.assert_awaited_once_with('foo') + + asyncio.run(self._runnable_test('foo')) + with self.assertRaises(AssertionError): + self.mock.assert_awaited_once_with('foo') + + def test_assert_any_wait(self): + with self.assertRaises(AssertionError): + self.mock.assert_any_await('NormalFoo') + + asyncio.run(self._runnable_test('foo')) + with self.assertRaises(AssertionError): + self.mock.assert_any_await('NormalFoo') + + asyncio.run(self._runnable_test('NormalFoo')) + self.mock.assert_any_await('NormalFoo') + + asyncio.run(self._runnable_test('SomethingElse')) + self.mock.assert_any_await('NormalFoo') + + def test_assert_has_awaits_no_order(self): + calls = [call('NormalFoo'), call('baz')] + + with self.assertRaises(AssertionError) as cm: + self.mock.assert_has_awaits(calls) + self.assertEqual(len(cm.exception.args), 1) + + asyncio.run(self._runnable_test('foo')) + with self.assertRaises(AssertionError): + self.mock.assert_has_awaits(calls) + + asyncio.run(self._runnable_test('NormalFoo')) + with self.assertRaises(AssertionError): + self.mock.assert_has_awaits(calls) + + asyncio.run(self._runnable_test('baz')) + self.mock.assert_has_awaits(calls) + + asyncio.run(self._runnable_test('SomethingElse')) + self.mock.assert_has_awaits(calls) + + 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) + + 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')) + with self.assertRaises(AssertionError): + self.mock.assert_has_awaits(calls, any_order=True) + + asyncio.run(self._runnable_test('NormalFoo')) + self.mock.assert_has_awaits(calls, any_order=True) + + 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()) + with self.assertRaises(AssertionError): + self.mock.assert_not_awaited() diff --git a/mock/tests/testcallable.py b/mock/tests/testcallable.py index 729947e..5eadc00 100644 --- a/mock/tests/testcallable.py +++ b/mock/tests/testcallable.py @@ -3,9 +3,9 @@ # http://www.voidspace.org.uk/python/mock/ import unittest -from mock.tests.support import is_instance, X, SomeClass +from unittest.test.testmock.support import is_instance, X, SomeClass -from mock import ( +from unittest.mock import ( Mock, MagicMock, NonCallableMagicMock, NonCallableMock, patch, create_autospec, CallableMixin @@ -106,14 +106,8 @@ class TestCallable(unittest.TestCase): class Multi(SomeClass, Sub): pass - class OldStyle: - def __call__(self): pass - - class OldStyleSub(OldStyle): - pass - for arg in 'spec', 'spec_set': - for Klass in CallableX, Sub, Multi, OldStyle, OldStyleSub: + for Klass in CallableX, Sub, Multi: with patch('%s.X' % __name__, **{arg: Klass}) as mock: instance = mock() mock.assert_called_once_with() diff --git a/mock/tests/testhelpers.py b/mock/tests/testhelpers.py index a5654ad..301bca4 100644 --- a/mock/tests/testhelpers.py +++ b/mock/tests/testhelpers.py @@ -1,28 +1,16 @@ -# Copyright (C) 2007-2012 Michael Foord & the mock team -# E-mail: fuzzyman AT voidspace DOT org DOT uk -# http://www.voidspace.org.uk/python/mock/ -import socket - import inspect -import six -import sys import time +import types import unittest -from mock import ( - call, create_autospec, MagicMock, - Mock, ANY, patch, PropertyMock +from unittest.mock import ( + call, _Call, create_autospec, MagicMock, + Mock, ANY, _CallList, patch, PropertyMock, _callable ) -from mock.mock import _Call, _CallList, _callable from datetime import datetime from functools import partial - -if six.PY2: - import funcsigs - - class SomeClass(object): def one(self, a, b): pass def two(self): pass @@ -418,12 +406,9 @@ class SpecSignatureTest(unittest.TestCase): m = create_autospec(Foo, a='3') self.assertEqual(m.a, '3') - @unittest.skipUnless(six.PY3, "Keyword only arguments Python 3 specific") + def test_create_autospec_keyword_only_arguments(self): - func_def = "def foo(a, *, b=None): pass\n" - namespace = {} - exec (func_def, namespace) - foo = namespace['foo'] + def foo(a, *, b=None): pass m = create_autospec(foo) m(1) @@ -433,6 +418,7 @@ class SpecSignatureTest(unittest.TestCase): m(2, b=3) m.assert_called_with(2, b=3) + def test_function_as_instance_attribute(self): obj = SomeClass() def f(a): pass @@ -471,16 +457,16 @@ class SpecSignatureTest(unittest.TestCase): self._check_someclass_mock(mock) - @unittest.skipIf('PyPy' in sys.version, - "This fails on pypy, " - "see https://github.com/testing-cabal/mock/issues/452") def test_spec_has_descriptor_returning_function(self): + class CrazyDescriptor(object): + def __get__(self, obj, type_): if obj is None: return lambda x: None class MyClass(object): + some_attr = CrazyDescriptor() mock = create_autospec(MyClass) @@ -490,11 +476,13 @@ class SpecSignatureTest(unittest.TestCase): with self.assertRaises(TypeError): mock.some_attr(1, 2) - @unittest.skipIf(six.PY2, "object.__dir__ doesn't exist in Python 2") + def test_spec_has_function_not_in_bases(self): + class CrazyClass(object): + def __dir__(self): - return super(CrazyClass, self).__dir__() + ['crazy'] + return super(CrazyClass, self).__dir__()+['crazy'] def __getattr__(self, item): if item == 'crazy': @@ -505,6 +493,7 @@ class SpecSignatureTest(unittest.TestCase): with self.assertRaises(AttributeError): inst.other self.assertEqual(inst.crazy(42), 42) + mock = create_autospec(inst) mock.crazy(42) with self.assertRaises(TypeError): @@ -513,8 +502,6 @@ class SpecSignatureTest(unittest.TestCase): mock.crazy(1, 2) - @unittest.skipIf('PyPy' in sys.version and sys.version_info < (3, 0), - "Fails on pypy2 due to incorrect signature for dict.pop from funcsigs") def test_builtin_functions_types(self): # we could replace builtin functions / methods with a function # with *args / **kwargs signature. Using the builtin method type @@ -611,27 +598,6 @@ class SpecSignatureTest(unittest.TestCase): mock.g.assert_called_once_with(3, 4) - @unittest.skipIf(six.PY3, "No old style classes in Python 3") - def test_old_style_classes(self): - class Foo: - def f(self, a, b): pass - - class Bar(Foo): - g = Foo() - - for spec in (Foo, Foo(), Bar, Bar()): - mock = create_autospec(spec) - mock.f(1, 2) - mock.f.assert_called_once_with(1, 2) - - self.assertRaises(AttributeError, getattr, mock, 'foo') - self.assertRaises(AttributeError, getattr, mock.f, 'foo') - - mock.g.f(1, 2) - mock.g.f.assert_called_once_with(1, 2) - self.assertRaises(AttributeError, getattr, mock.g, 'foo') - - def test_recursive(self): class A(object): def a(self): pass @@ -785,21 +751,6 @@ class SpecSignatureTest(unittest.TestCase): mock = create_autospec(Foo) - self.assertRaises(TypeError, mock) - mock(1) - mock.assert_called_once_with(1) - - mock(4, 5) - mock.assert_called_with(4, 5) - - - @unittest.skipIf(six.PY3, 'no old style classes in Python 3') - def test_signature_old_style_class(self): - class Foo: - def __init__(self, a, b=3): pass - - mock = create_autospec(Foo) - self.assertRaises(TypeError, mock) mock(1) mock.assert_called_once_with(1) @@ -820,15 +771,6 @@ class SpecSignatureTest(unittest.TestCase): create_autospec(Foo) - @unittest.skipIf(six.PY3, 'no old style classes in Python 3') - def test_old_style_class_with_no_init(self): - # this used to raise an exception - # due to Foo.__init__ raising an AttributeError - class Foo: - pass - create_autospec(Foo) - - def test_signature_callable(self): class Callable(object): def __init__(self, x, y): pass @@ -899,36 +841,6 @@ class SpecSignatureTest(unittest.TestCase): a.f.assert_called_with(self=10) - def test_autospec_property(self): - class Foo(object): - @property - def foo(self): pass - - foo = create_autospec(Foo) - mock_property = foo.foo - - # no spec on properties - self.assertIsInstance(mock_property, MagicMock) - mock_property(1, 2, 3) - mock_property.abc(4, 5, 6) - mock_property.assert_called_once_with(1, 2, 3) - mock_property.abc.assert_called_once_with(4, 5, 6) - - - def test_autospec_slots(self): - class Foo(object): - __slots__ = ['a'] - - foo = create_autospec(Foo) - mock_slot = foo.a - - # no spec on slots - mock_slot(1, 2, 3) - mock_slot.abc(4, 5, 6) - mock_slot.assert_called_once_with(1, 2, 3) - mock_slot.abc.assert_called_once_with(4, 5, 6) - - def test_autospec_data_descriptor(self): class Descriptor(object): def __init__(self, value): @@ -973,10 +885,8 @@ class SpecSignatureTest(unittest.TestCase): check_data_descriptor(foo.desc) - @unittest.skipIf('PyPy' in sys.version and sys.version_info > (3, 0), - "https://bitbucket.org/pypy/pypy/issues/3010") def test_autospec_on_bound_builtin_function(self): - meth = six.create_bound_method(time.ctime, time.time()) + meth = types.MethodType(time.ctime, time.time()) self.assertIsInstance(meth(), str) mocked = create_autospec(meth) @@ -987,17 +897,15 @@ class SpecSignatureTest(unittest.TestCase): mocked(4, 5, 6) mocked.assert_called_once_with(4, 5, 6) - def test_autospec_socket(self): - sock_class = create_autospec(socket.socket) - self.assertRaises(TypeError, sock_class, foo=1) - def test_autospec_getattr_partial_function(self): # bpo-32153 : getattr returning partial functions without # __name__ should not create AttributeError in create_autospec - class Foo(object): + class Foo: + def __getattr__(self, attribute): return partial(lambda name: name, attribute) + proxy = Foo() autospec = create_autospec(proxy) self.assertFalse(hasattr(autospec, '__name__')) @@ -1011,24 +919,29 @@ class SpecSignatureTest(unittest.TestCase): mock(1, 2) mock(x=1, y=2) - if six.PY2: - self.assertEqual(funcsigs.signature(mock), funcsigs.signature(myfunc)) - else: - self.assertEqual(inspect.getfullargspec(mock), inspect.getfullargspec(myfunc)) + self.assertEqual(inspect.signature(mock), inspect.signature(myfunc)) self.assertEqual(mock.mock_calls, [call(1, 2), call(x=1, y=2)]) self.assertRaises(TypeError, mock, 1) - def test_spec_function_no_name(self): - func = lambda: 'nope' - mock = create_autospec(func) - self.assertEqual(mock.__name__, 'funcopy') + def test_spec_inspect_signature_annotations(self): + + def foo(a: int, b: int=10, *, c:int) -> int: + return a + b + c + + self.assertEqual(foo(1, 2 , c=3), 6) + mock = create_autospec(foo) + mock(1, 2, c=3) + mock(1, c=3) + + self.assertEqual(inspect.signature(mock), inspect.signature(foo)) + self.assertEqual(mock.mock_calls, [call(1, 2, c=3), call(1, c=3)]) + self.assertRaises(TypeError, mock, 1) + self.assertRaises(TypeError, mock, 1, 2, 3, c=4) - @unittest.skipIf(six.PY3, "Here to test our Py2 _isidentifier") - def test_spec_function_has_identifier_name(self): + def test_spec_function_no_name(self): func = lambda: 'nope' - func.__name__ = 'global' mock = create_autospec(func) self.assertEqual(mock.__name__, 'funcopy') @@ -1105,20 +1018,6 @@ class TestCallList(unittest.TestCase): self.assertEqual(str(mock.mock_calls), expected) - @unittest.skipIf(six.PY3, "Unicode is properly handled with Python 3") - def test_call_list_unicode(self): - # See github issue #328 - mock = Mock() - - class NonAsciiRepr(object): - def __repr__(self): - return "\xe9" - - mock(**{unicode("a"): NonAsciiRepr()}) - - self.assertEqual(str(mock.mock_calls), "[call(a=\xe9)]") - - def test_propertymock(self): p = patch('%s.SomeClass.one' % __name__, new_callable=PropertyMock) mock = p.start() diff --git a/mock/tests/testhelpers_py3.py b/mock/tests/testhelpers_py3.py deleted file mode 100644 index 64d62f8..0000000 --- a/mock/tests/testhelpers_py3.py +++ /dev/null @@ -1,23 +0,0 @@ -import inspect -import unittest - -from mock import call, create_autospec - - -class CallTest(unittest.TestCase): - - - def test_spec_inspect_signature_annotations(self): - - def foo(a: int, b: int=10, *, c:int) -> int: - return a + b + c - - self.assertEqual(foo(1, 2, c=3), 6) - mock = create_autospec(foo) - mock(1, 2, c=3) - mock(1, c=3) - - self.assertEqual(inspect.getfullargspec(mock), inspect.getfullargspec(foo)) - self.assertEqual(mock.mock_calls, [call(1, 2, c=3), call(1, c=3)]) - self.assertRaises(TypeError, mock, 1) - self.assertRaises(TypeError, mock, 1, 2, 3, c=4) diff --git a/mock/tests/testmagicmethods.py b/mock/tests/testmagicmethods.py index f6c25fb..130a339 100644 --- a/mock/tests/testmagicmethods.py +++ b/mock/tests/testmagicmethods.py @@ -1,26 +1,8 @@ -# Copyright (C) 2007-2012 Michael Foord & the mock team -# E-mail: fuzzyman AT voidspace DOT org DOT uk -# http://www.voidspace.org.uk/python/mock/ - -from __future__ import division - -try: - unicode -except NameError: - # Python 3 - unicode = str - long = int - import math +import unittest import os import sys -import textwrap -import unittest - -import six - -from mock import Mock, MagicMock -from mock.mock import _magics +from unittest.mock import Mock, MagicMock, _magics @@ -87,15 +69,6 @@ class TestMockingMagicMethods(unittest.TestCase): self.assertEqual(str(mock), 'foo') - @unittest.skipIf(six.PY3, "no unicode in Python 3") - def test_unicode(self): - mock = Mock() - self.assertEqual(unicode(mock), unicode(str(mock))) - - mock.__unicode__ = lambda s: unicode('foo') - self.assertEqual(unicode(mock), unicode('foo')) - - def test_dict_methods(self): mock = Mock() @@ -167,16 +140,13 @@ class TestMockingMagicMethods(unittest.TestCase): self.assertEqual(mock.value, 16) del mock.__truediv__ - if six.PY3: - def itruediv(mock): - mock /= 4 - self.assertRaises(TypeError, itruediv, mock) - mock.__itruediv__ = truediv - mock /= 8 - self.assertEqual(mock, original) - self.assertEqual(mock.value, 2) - else: - mock.value = 2 + def itruediv(mock): + mock /= 4 + self.assertRaises(TypeError, itruediv, mock) + mock.__itruediv__ = truediv + mock /= 8 + self.assertEqual(mock, original) + self.assertEqual(mock.value, 2) self.assertRaises(TypeError, lambda: 8 / mock) mock.__rtruediv__ = truediv @@ -198,12 +168,7 @@ class TestMockingMagicMethods(unittest.TestCase): m = Mock() self.assertTrue(bool(m)) - nonzero = lambda s: False - if six.PY2: - m.__nonzero__ = nonzero - else: - m.__bool__ = nonzero - + m.__bool__ = lambda s: False self.assertFalse(bool(m)) @@ -217,25 +182,18 @@ class TestMockingMagicMethods(unittest.TestCase): self. assertTrue(mock <= 3) self. assertTrue(mock >= 3) - if six.PY2: - # incomparable in Python 3 - self.assertEqual(Mock() < 3, object() < 3) - self.assertEqual(Mock() > 3, object() > 3) - self.assertEqual(Mock() <= 3, object() <= 3) - self.assertEqual(Mock() >= 3, object() >= 3) - else: - self.assertRaises(TypeError, lambda: MagicMock() < object()) - self.assertRaises(TypeError, lambda: object() < MagicMock()) - self.assertRaises(TypeError, lambda: MagicMock() < MagicMock()) - self.assertRaises(TypeError, lambda: MagicMock() > object()) - self.assertRaises(TypeError, lambda: object() > MagicMock()) - self.assertRaises(TypeError, lambda: MagicMock() > MagicMock()) - self.assertRaises(TypeError, lambda: MagicMock() <= object()) - self.assertRaises(TypeError, lambda: object() <= MagicMock()) - self.assertRaises(TypeError, lambda: MagicMock() <= MagicMock()) - self.assertRaises(TypeError, lambda: MagicMock() >= object()) - self.assertRaises(TypeError, lambda: object() >= MagicMock()) - self.assertRaises(TypeError, lambda: MagicMock() >= MagicMock()) + self.assertRaises(TypeError, lambda: MagicMock() < object()) + self.assertRaises(TypeError, lambda: object() < MagicMock()) + self.assertRaises(TypeError, lambda: MagicMock() < MagicMock()) + self.assertRaises(TypeError, lambda: MagicMock() > object()) + self.assertRaises(TypeError, lambda: object() > MagicMock()) + self.assertRaises(TypeError, lambda: MagicMock() > MagicMock()) + self.assertRaises(TypeError, lambda: MagicMock() <= object()) + self.assertRaises(TypeError, lambda: object() <= MagicMock()) + self.assertRaises(TypeError, lambda: MagicMock() <= MagicMock()) + self.assertRaises(TypeError, lambda: MagicMock() >= object()) + self.assertRaises(TypeError, lambda: object() >= MagicMock()) + self.assertRaises(TypeError, lambda: MagicMock() >= MagicMock()) def test_equality(self): @@ -293,12 +251,8 @@ class TestMockingMagicMethods(unittest.TestCase): mock.__iter__.return_value = iter([1, 2, 3]) self.assertEqual(list(mock), [1, 2, 3]) - name = '__nonzero__' - other = '__bool__' - if six.PY3: - name, other = other, name - getattr(mock, name).return_value = False - self.assertFalse(hasattr(mock, other)) + getattr(mock, '__bool__').return_value = False + self.assertFalse(hasattr(mock, '__nonzero__')) self.assertFalse(bool(mock)) for entry in _magics: @@ -322,55 +276,31 @@ class TestMockingMagicMethods(unittest.TestCase): self.assertEqual(int(mock), 1) self.assertEqual(complex(mock), 1j) self.assertEqual(float(mock), 1.0) - self.assertEqual(long(mock), long(1)) self.assertNotIn(object(), mock) self.assertEqual(len(mock), 0) self.assertEqual(list(mock), []) self.assertEqual(hash(mock), object.__hash__(mock)) self.assertEqual(str(mock), object.__str__(mock)) - self.assertEqual(unicode(mock), object.__str__(mock)) - self.assertIsInstance(unicode(mock), unicode) self.assertTrue(bool(mock)) + self.assertEqual(round(mock), mock.__round__()) self.assertEqual(math.trunc(mock), mock.__trunc__()) - if six.PY2: - # These fall back to __float__ in Python 2: - self.assertEqual(round(mock), 1.0) - self.assertEqual(math.floor(mock), 1.0) - self.assertEqual(math.ceil(mock), 1.0) - else: - self.assertEqual(round(mock), mock.__round__()) - self.assertEqual(math.floor(mock), mock.__floor__()) - self.assertEqual(math.ceil(mock), mock.__ceil__()) - if six.PY2: - self.assertEqual(oct(mock), '1') - else: - # in Python 3 oct and hex use __index__ - # so these tests are for __index__ in py3k - self.assertEqual(oct(mock), '0o1') + self.assertEqual(math.floor(mock), mock.__floor__()) + self.assertEqual(math.ceil(mock), mock.__ceil__()) + + # in Python 3 oct and hex use __index__ + # so these tests are for __index__ in py3k + self.assertEqual(oct(mock), '0o1') self.assertEqual(hex(mock), '0x1') # how to test __sizeof__ ? - @unittest.skipIf(six.PY3, "no __cmp__ in Python 3") - def test_non_default_magic_methods(self): - mock = MagicMock() - self.assertRaises(AttributeError, lambda: mock.__cmp__) - - mock = Mock() - mock.__cmp__ = lambda s, o: 0 - - self.assertEqual(mock, object()) - - def test_magic_methods_fspath(self): mock = MagicMock() - if sys.version_info < (3, 6): - self.assertRaises(AttributeError, lambda: mock.__fspath__) - else: - expected_path = mock.__fspath__() - mock.reset_mock() - self.assertEqual(os.fspath(mock), expected_path) - mock.__fspath__.assert_called_once() + expected_path = mock.__fspath__() + mock.reset_mock() + + self.assertEqual(os.fspath(mock), expected_path) + mock.__fspath__.assert_called_once() def test_magic_methods_and_spec(self): @@ -425,7 +355,7 @@ class TestMockingMagicMethods(unittest.TestCase): mock = MagicMock() def set_setattr(): mock.__setattr__ = lambda self, name: None - self.assertRaisesRegexp(AttributeError, + self.assertRaisesRegex(AttributeError, "Attempting to set unsupported magic method '__setattr__'.", set_setattr ) @@ -459,6 +389,7 @@ class TestMockingMagicMethods(unittest.TestCase): mock.reset_mock() self.assertFalse(mock.__str__.called) + def test_dir(self): # overriding the default implementation for mock in Mock(), MagicMock(): @@ -504,20 +435,17 @@ class TestMockingMagicMethods(unittest.TestCase): self.assertEqual(list(m), [4, 5, 6]) self.assertEqual(list(m), []) - @unittest.skipIf(sys.version_info < (3, 5), "@ added in Python 3.5") + def test_matmul(self): - src = textwrap.dedent("""\ - m = MagicMock() - self.assertIsInstance(m @ 1, MagicMock) - m.__matmul__.return_value = 42 - m.__rmatmul__.return_value = 666 - m.__imatmul__.return_value = 24 - self.assertEqual(m @ 1, 42) - self.assertEqual(1 @ m, 666) - m @= 24 - self.assertEqual(m, 24) - """) - exec(src) + m = MagicMock() + self.assertIsInstance(m @ 1, MagicMock) + m.__matmul__.return_value = 42 + m.__rmatmul__.return_value = 666 + m.__imatmul__.return_value = 24 + self.assertEqual(m @ 1, 42) + self.assertEqual(1 @ m, 666) + m @= 24 + self.assertEqual(m, 24) def test_divmod_and_rdivmod(self): m = MagicMock() diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 15bac2e..0f30bcc 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -1,30 +1,17 @@ -# Copyright (C) 2007-2012 Michael Foord & the mock team -# E-mail: fuzzyman AT voidspace DOT org DOT uk -# http://www.voidspace.org.uk/python/mock/ - import copy -import pickle import re import sys import tempfile -import six import unittest - -import mock -from mock.mock import ( +from unittest.test.testmock.support import is_instance +from unittest import mock +from unittest.mock import ( call, DEFAULT, patch, sentinel, MagicMock, Mock, NonCallableMock, - NonCallableMagicMock, _Call, _CallList, + NonCallableMagicMock, AsyncMock, _Call, _CallList, create_autospec ) -from mock.tests.support import is_instance - - -try: - unicode -except NameError: - unicode = str class Iter(object): @@ -50,23 +37,13 @@ class Something(object): def smeth(a, b, c, d=None): pass -class Subclass(MagicMock): - pass - - -class Thing(object): - attribute = 6 - foo = 'bar' - - - class MockTest(unittest.TestCase): def test_all(self): # if __all__ is badly defined then import * will raise an error # We have to exec it because you can't import * inside a method # in Python 3 - exec("from mock import *") + exec("from unittest.mock import *") def test_constructor(self): @@ -215,7 +192,8 @@ class MockTest(unittest.TestCase): mock = create_autospec(f) mock.side_effect = ValueError('Bazinga!') - self.assertRaisesRegexp(ValueError, 'Bazinga!', mock) + self.assertRaisesRegex(ValueError, 'Bazinga!', mock) + def test_reset_mock(self): parent = Mock() @@ -384,8 +362,7 @@ class MockTest(unittest.TestCase): # Expected call doesn't match the spec's signature with self.assertRaises(AssertionError) as cm: mock.assert_called_with(e=8) - if hasattr(cm.exception, '__cause__'): - self.assertIsInstance(cm.exception.__cause__, TypeError) + self.assertIsInstance(cm.exception.__cause__, TypeError) def test_assert_called_with_method_spec(self): @@ -435,7 +412,7 @@ class MockTest(unittest.TestCase): m = Mock() m(1) m(2) - self.assertRaisesRegexp(AssertionError, + self.assertRaisesRegex(AssertionError, re.escape("Calls: [call(1), call(2)]"), lambda: m.assert_called_once_with(2)) @@ -453,8 +430,7 @@ class MockTest(unittest.TestCase): # Expected call doesn't match the spec's signature with self.assertRaises(AssertionError) as cm: mock.assert_called_once_with(e=8) - if hasattr(cm.exception, '__cause__'): - self.assertIsInstance(cm.exception.__cause__, TypeError) + self.assertIsInstance(cm.exception.__cause__, TypeError) # Mock called more than once => always fails mock(4, 5, 6) self.assertRaises(AssertionError, mock.assert_called_once_with, @@ -535,7 +511,7 @@ class MockTest(unittest.TestCase): # this should be allowed mock.something - self.assertRaisesRegexp( + self.assertRaisesRegex( AttributeError, "Mock object has no attribute 'something_else'", getattr, mock, 'something_else' @@ -553,12 +529,12 @@ class MockTest(unittest.TestCase): mock.x mock.y mock.__something__ - self.assertRaisesRegexp( + self.assertRaisesRegex( AttributeError, "Mock object has no attribute 'z'", getattr, mock, 'z' ) - self.assertRaisesRegexp( + self.assertRaisesRegex( AttributeError, "Mock object has no attribute '__foobar__'", getattr, mock, '__foobar__' @@ -738,13 +714,13 @@ class MockTest(unittest.TestCase): def test_assert_called_with_message(self): mock = Mock() - self.assertRaisesRegexp(AssertionError, 'not called', + self.assertRaisesRegex(AssertionError, 'not called', mock.assert_called_with) def test_assert_called_once_with_message(self): mock = Mock(name='geoffrey') - self.assertRaisesRegexp(AssertionError, + self.assertRaisesRegex(AssertionError, r"Expected 'geoffrey' to be called once\.", mock.assert_called_once_with) @@ -794,10 +770,8 @@ class MockTest(unittest.TestCase): mock = Mock(spec=X) self.assertIsInstance(mock, X) - if not six.PY2: - # This isn't true on Py2, we should fix if anyone complains: - mock = Mock(spec=X()) - self.assertIsInstance(mock, X) + mock = Mock(spec=X()) + self.assertIsInstance(mock, X) self.assertIs(mock.__class__, X) self.assertEqual(Mock().__class__.__name__, 'Mock') @@ -805,10 +779,8 @@ class MockTest(unittest.TestCase): mock = Mock(spec_set=X) self.assertIsInstance(mock, X) - if not six.PY2: - # This isn't true on Py2, we should fix if anyone complains: - mock = Mock(spec_set=X()) - self.assertIsInstance(mock, X) + mock = Mock(spec_set=X()) + self.assertIsInstance(mock, X) def test_setting_attribute_with_spec_set(self): @@ -826,7 +798,6 @@ class MockTest(unittest.TestCase): self.assertRaises(AttributeError, set_attr) - @unittest.skipIf('PyPy' in sys.version, "https://bitbucket.org/pypy/pypy/issues/3094") def test_copy(self): current = sys.getrecursionlimit() self.addCleanup(sys.setrecursionlimit, current) @@ -837,42 +808,6 @@ class MockTest(unittest.TestCase): copy.copy(Mock()) - @unittest.skipIf(six.PY3, "no old style classes in Python 3") - def test_spec_old_style_classes(self): - class Foo: - bar = 7 - - mock = Mock(spec=Foo) - mock.bar = 6 - self.assertRaises(AttributeError, lambda: mock.foo) - - mock = Mock(spec=Foo()) - mock.bar = 6 - self.assertRaises(AttributeError, lambda: mock.foo) - - - @unittest.skipIf(six.PY3, "no old style classes in Python 3") - def test_spec_set_old_style_classes(self): - class Foo: - bar = 7 - - mock = Mock(spec_set=Foo) - mock.bar = 6 - self.assertRaises(AttributeError, lambda: mock.foo) - - def _set(): - mock.foo = 3 - self.assertRaises(AttributeError, _set) - - mock = Mock(spec_set=Foo()) - mock.bar = 6 - self.assertRaises(AttributeError, lambda: mock.foo) - - def _set(): - mock.foo = 3 - self.assertRaises(AttributeError, _set) - - def test_subclass_with_properties(self): class SubClass(Mock): def _get(self): @@ -908,7 +843,7 @@ class MockTest(unittest.TestCase): def test_dir(self): mock = Mock() attrs = set(dir(mock)) - type_attrs = {m for m in dir(Mock) if not m.startswith('_')} + type_attrs = set([m for m in dir(Mock) if not m.startswith('_')]) # all public attributes from the type are included self.assertEqual(set(), type_attrs - attrs) @@ -1181,7 +1116,7 @@ class MockTest(unittest.TestCase): m = Mock() m.foo = m repr(m.foo()) - self.assertRegexpMatches(repr(m.foo()), r"") + self.assertRegex(repr(m.foo()), r"") def test_mock_calls_contains(self): @@ -1281,6 +1216,16 @@ class MockTest(unittest.TestCase): self.assertRaises(StopIteration, mock) + def test_side_effect_iterator_exceptions(self): + for Klass in Mock, MagicMock: + iterable = (ValueError, 3, KeyError, 6) + m = Klass(side_effect=iterable) + self.assertRaises(ValueError, m) + self.assertEqual(m(), 3) + self.assertRaises(KeyError, m) + self.assertEqual(m(), 6) + + def test_side_effect_setting_iterator(self): mock = Mock() mock.side_effect = iter([1, 2, 3]) @@ -1302,17 +1247,6 @@ class MockTest(unittest.TestCase): self.assertRaises(StopIteration, mock) self.assertIs(mock.side_effect, this_iter) - - def test_side_effect_iterator_exceptions(self): - for Klass in Mock, MagicMock: - iterable = (ValueError, 3, KeyError, 6) - m = Klass(side_effect=iterable) - self.assertRaises(ValueError, m) - self.assertEqual(m(), 3) - self.assertRaises(KeyError, m) - self.assertEqual(m(), 6) - - def test_side_effect_iterator_default(self): mock = Mock(return_value=2) mock.side_effect = iter([1, DEFAULT]) @@ -1476,8 +1410,7 @@ class MockTest(unittest.TestCase): # Expected call doesn't match the spec's signature with self.assertRaises(AssertionError) as cm: mock.assert_any_call(e=8) - if hasattr(cm.exception, '__cause__'): - self.assertIsInstance(cm.exception.__cause__, TypeError) + self.assertIsInstance(cm.exception.__cause__, TypeError) def test_mock_calls_create_autospec(self): @@ -1511,6 +1444,7 @@ class MockTest(unittest.TestCase): @staticmethod def static_method(): pass for method in ('class_method', 'static_method'): + with self.subTest(method=method): mock_method = mock.create_autospec(getattr(TestClass, method)) mock_method() mock_method.assert_called_once_with() @@ -1519,9 +1453,10 @@ class MockTest(unittest.TestCase): #Issue21238 def test_mock_unsafe(self): m = Mock() - with self.assertRaises(AttributeError): + msg = "Attributes cannot start with 'assert' or 'assret'" + with self.assertRaisesRegex(AttributeError, msg): m.assert_foo_call() - with self.assertRaises(AttributeError): + with self.assertRaisesRegex(AttributeError, msg): m.assret_foo_call() m = Mock(unsafe=True) m.assert_foo_call() @@ -1538,7 +1473,7 @@ class MockTest(unittest.TestCase): def test_assert_not_called_message(self): m = Mock() m(1, 2) - self.assertRaisesRegexp(AssertionError, + self.assertRaisesRegex(AssertionError, re.escape("Calls: [call(1, 2)]"), m.assert_not_called) @@ -1567,7 +1502,7 @@ class MockTest(unittest.TestCase): m = Mock() m(1, 2) m(3) - self.assertRaisesRegexp(AssertionError, + self.assertRaisesRegex(AssertionError, re.escape("Calls: [call(1, 2), call(3)]"), m.assert_called_once) @@ -1683,7 +1618,8 @@ class MockTest(unittest.TestCase): def test_adding_child_mock(self): - for Klass in NonCallableMock, Mock, MagicMock, NonCallableMagicMock: + for Klass in (NonCallableMock, Mock, MagicMock, NonCallableMagicMock, + AsyncMock): mock = Klass() mock.foo = Mock() @@ -1766,6 +1702,19 @@ class MockTest(unittest.TestCase): self.assertEqual(lines[1], 'Norwegian Blue') self.assertEqual(list(f1), []) + def test_mock_open_using_next(self): + mocked_open = mock.mock_open(read_data='1st line\n2nd line\n3rd line') + f1 = mocked_open('a-name') + line1 = next(f1) + line2 = f1.__next__() + lines = [line for line in f1] + self.assertEqual(line1, '1st line\n') + self.assertEqual(line2, '2nd line\n') + self.assertEqual(lines[0], '3rd line') + self.assertEqual(list(f1), []) + with self.assertRaises(StopIteration): + next(f1) + def test_mock_open_write(self): # Test exception in file writing write() mock_namedtemp = mock.mock_open(mock.MagicMock(name='JLV')) @@ -1785,7 +1734,6 @@ class MockTest(unittest.TestCase): self.assertEqual('abc', first) self.assertEqual('abc', second) - def test_mock_open_after_eof(self): # read, readline and readlines should work after end of file. _open = mock.mock_open(read_data='foo') @@ -1798,7 +1746,6 @@ class MockTest(unittest.TestCase): self.assertEqual([], h.readlines()) self.assertEqual([], h.readlines()) - def test_mock_parents(self): for Klass in Mock, MagicMock: m = Klass() @@ -1933,12 +1880,15 @@ class MockTest(unittest.TestCase): self.assertEqual(type(call.parent), _Call) self.assertEqual(type(call.parent().parent), _Call) + def test_parent_propagation_with_create_autospec(self): + def foo(a, b): pass mock = Mock() mock.child = create_autospec(foo) mock.child(1, 2) + self.assertRaises(TypeError, mock.child, 1) self.assertEqual(mock.mock_calls, [call.child(1, 2)]) @@ -1950,27 +1900,35 @@ class MockTest(unittest.TestCase): # dependent on unittest.mock.patch. In testpatch.PatchTest # test_patch_dict_test_prefix and test_patch_test_prefix not restoring # causes the objects patched to go out of sync - old_patch = mock.patch + + old_patch = unittest.mock.patch + # Directly using __setattr__ on unittest.mock causes current imported # reference to be updated. Use a lambda so that during cleanup the # re-imported new reference is updated. - self.addCleanup(lambda patch: setattr(mock, 'patch', patch), + self.addCleanup(lambda patch: setattr(unittest.mock, 'patch', patch), old_patch) + with patch.dict('sys.modules'): - del sys.modules['mock.mock'] + del sys.modules['unittest.mock'] + # This trace will stop coverage being measured ;-) def trace(frame, event, arg): # pragma: no cover return trace + self.addCleanup(sys.settrace, sys.gettrace()) sys.settrace(trace) - from mock.mock import ( + + from unittest.mock import ( Mock, MagicMock, NonCallableMock, NonCallableMagicMock ) + mocks = [ Mock, MagicMock, NonCallableMock, NonCallableMagicMock ] - for mock_ in mocks: - obj = mock_(spec=Something) + + for mock in mocks: + obj = mock(spec=Something) self.assertIsInstance(obj, Something) diff --git a/mock/tests/testpatch.py b/mock/tests/testpatch.py index bbd6d26..27914a9 100644 --- a/mock/tests/testpatch.py +++ b/mock/tests/testpatch.py @@ -5,23 +5,19 @@ import os import sys -import six import unittest - -from mock.tests import support -from mock.tests.support import SomeClass, is_instance, uncache - -from mock import ( - NonCallableMock, CallableMixin, patch, sentinel, - MagicMock, Mock, NonCallableMagicMock, - DEFAULT, call +from unittest.test.testmock import support +from unittest.test.testmock.support import SomeClass, is_instance + +from test.test_importlib.util import uncache +from unittest.mock import ( + NonCallableMock, CallableMixin, sentinel, + MagicMock, Mock, NonCallableMagicMock, patch, _patch, + DEFAULT, call, _get_target ) -from mock.mock import _patch, _get_target -builtin_string = '__builtin__' -if six.PY3: - builtin_string = 'builtins' - unicode = str + +builtin_string = 'builtins' PTModule = sys.modules[__name__] MODNAME = '%s.PTModule' % __name__ @@ -623,6 +619,13 @@ class PatchTest(unittest.TestCase): self.assertEqual(foo.values, original) + def test_patch_dict_as_context_manager(self): + foo = {'a': 'b'} + with patch.dict(foo, a='c') as patched: + self.assertEqual(patched, {'a': 'c'}) + self.assertEqual(foo, {'a': 'b'}) + + def test_name_preserved(self): foo = {} @@ -656,21 +659,15 @@ class PatchTest(unittest.TestCase): test() - def test_patch_dict_with_unicode(self): - @patch.dict(u'os.environ', {'konrad_delong': 'some value'}) - def test(): - self.assertIn('konrad_delong', os.environ) - - test() - - def test_patch_dict_decorator_resolution(self): # bpo-35512: Ensure that patch with a string target resolves to # the new dictionary during function call original = support.target.copy() - @patch.dict('mock.tests.support.target', {'bar': 'BAR'}) + + @patch.dict('unittest.test.testmock.support.target', {'bar': 'BAR'}) def test(): self.assertEqual(support.target, {'foo': 'BAZ', 'bar': 'BAR'}) + try: support.target = {'foo': 'BAZ'} test() @@ -1329,7 +1326,7 @@ class PatchTest(unittest.TestCase): try: f = result['f'] foo = result['foo'] - self.assertEqual(set(result), {'f', 'foo'}) + self.assertEqual(set(result), set(['f', 'foo'])) self.assertIs(Foo, original_foo) self.assertIs(Foo.f, f) @@ -1533,18 +1530,17 @@ class PatchTest(unittest.TestCase): def test_patch_multiple_string_subclasses(self): - for base in (str, unicode): - Foo = type('Foo', (base,), {'fish': 'tasty'}) - foo = Foo() - @patch.multiple(foo, fish='nearly gone') - def test(): - self.assertEqual(foo.fish, 'nearly gone') + Foo = type('Foo', (str,), {'fish': 'tasty'}) + foo = Foo() + @patch.multiple(foo, fish='nearly gone') + def test(): + self.assertEqual(foo.fish, 'nearly gone') - test() - self.assertEqual(foo.fish, 'tasty') + test() + self.assertEqual(foo.fish, 'tasty') - @patch('mock.patch.TEST_PREFIX', 'foo') + @patch('unittest.mock.patch.TEST_PREFIX', 'foo') def test_patch_test_prefix(self): class Foo(object): thing = 'original' @@ -1567,7 +1563,7 @@ class PatchTest(unittest.TestCase): self.assertEqual(foo.test_two(), 'original') - @patch('mock.patch.TEST_PREFIX', 'bar') + @patch('unittest.mock.patch.TEST_PREFIX', 'bar') def test_patch_dict_test_prefix(self): class Foo(object): def bar_one(self): @@ -1605,15 +1601,12 @@ class PatchTest(unittest.TestCase): def test_patch_nested_autospec_repr(self): - p = patch('mock.tests.support', autospec=True) - m = p.start() - try: + with patch('unittest.test.testmock.support', autospec=True) as m: self.assertIn(" name='support.SomeClass.wibble()'", repr(m.SomeClass.wibble())) self.assertIn(" name='support.SomeClass().wibble()'", repr(m.SomeClass().wibble())) - finally: - p.stop() + def test_mock_calls_with_patch(self): @@ -1793,32 +1786,6 @@ class PatchTest(unittest.TestCase): patched() self.assertIs(os.path, path) - - def test_wrapped_patch(self): - decorated = patch('sys.modules')(function) - self.assertIs(decorated.__wrapped__, function) - - - def test_wrapped_several_times_patch(self): - decorated = patch('sys.modules')(function) - decorated = patch('sys.modules')(decorated) - self.assertIs(decorated.__wrapped__, function) - - - def test_wrapped_patch_object(self): - decorated = patch.object(sys, 'modules')(function) - self.assertIs(decorated.__wrapped__, function) - - - def test_wrapped_patch_dict(self): - decorated = patch.dict('sys.modules')(function) - self.assertIs(decorated.__wrapped__, function) - - - def test_wrapped_patch_multiple(self): - decorated = patch.multiple('sys', modules={})(function) - self.assertIs(decorated.__wrapped__, function) - def test_stopall_lifo(self): stopped = [] class thing(object): @@ -1851,32 +1818,32 @@ class PatchTest(unittest.TestCase): with patch.object(foo, '__module__', "testpatch2"): self.assertEqual(foo.__module__, "testpatch2") - self.assertEqual(foo.__module__, __name__) + self.assertEqual(foo.__module__, 'unittest.test.testmock.testpatch') - if hasattr(self.test_special_attrs, '__annotations__'): - with patch.object(foo, '__annotations__', dict([('s', 1, )])): - self.assertEqual(foo.__annotations__, dict([('s', 1, )])) - self.assertEqual(foo.__annotations__, dict()) - - if hasattr(self.test_special_attrs, '__kwdefaults__'): - foo = eval("lambda *a, x=0: x") - with patch.object(foo, '__kwdefaults__', dict([('x', 1, )])): - self.assertEqual(foo(), 1) - self.assertEqual(foo(), 0) + with patch.object(foo, '__annotations__', dict([('s', 1, )])): + self.assertEqual(foo.__annotations__, dict([('s', 1, )])) + self.assertEqual(foo.__annotations__, dict()) + def foo(*a, x=0): + return x + with patch.object(foo, '__kwdefaults__', dict([('x', 1, )])): + self.assertEqual(foo(), 1) + self.assertEqual(foo(), 0) def test_dotted_but_module_not_loaded(self): # This exercises the AttributeError branch of _dot_lookup. + # make sure it's there - import mock.tests.support + import unittest.test.testmock.support # now make sure it's not: with patch.dict('sys.modules'): - del sys.modules['mock.tests.support'] - del sys.modules['mock.tests'] - del sys.modules['mock.mock'] - del sys.modules['mock'] + del sys.modules['unittest.test.testmock.support'] + del sys.modules['unittest.test.testmock'] + del sys.modules['unittest.test'] + del sys.modules['unittest'] + # now make sure we can patch based on a dotted path: - @patch('mock.tests.support.X') + @patch('unittest.test.testmock.support.X') def test(mock): pass test() @@ -1888,7 +1855,7 @@ class PatchTest(unittest.TestCase): def test_cant_set_kwargs_when_passing_a_mock(self): - @patch('mock.tests.support.X', new=object(), x=1) + @patch('unittest.test.testmock.support.X', new=object(), x=1) def test(): pass with self.assertRaises(TypeError): test() diff --git a/mock/tests/testsealable.py b/mock/tests/testsealable.py index 63a8541..59f5233 100644 --- a/mock/tests/testsealable.py +++ b/mock/tests/testsealable.py @@ -1,5 +1,5 @@ import unittest -import mock +from unittest import mock class SampleObject: diff --git a/mock/tests/testsentinel.py b/mock/tests/testsentinel.py index 1411445..de53509 100644 --- a/mock/tests/testsentinel.py +++ b/mock/tests/testsentinel.py @@ -1,11 +1,7 @@ -# Copyright (C) 2007-2012 Michael Foord & the mock team -# E-mail: fuzzyman AT voidspace DOT org DOT uk -# http://www.voidspace.org.uk/python/mock/ - import unittest import copy import pickle -from mock import sentinel, DEFAULT +from unittest.mock import sentinel, DEFAULT class SentinelTest(unittest.TestCase): @@ -31,6 +27,7 @@ class SentinelTest(unittest.TestCase): def testPickle(self): for proto in range(pickle.HIGHEST_PROTOCOL+1): + with self.subTest(protocol=proto): pickled = pickle.dumps(sentinel.whatever, proto) unpickled = pickle.loads(pickled) self.assertIs(unpickled, sentinel.whatever) diff --git a/mock/tests/testsupport.py b/mock/tests/testsupport.py deleted file mode 100644 index 4882572..0000000 --- a/mock/tests/testsupport.py +++ /dev/null @@ -1,14 +0,0 @@ -# Tests to make sure helpers we backport are actually working! -from unittest import TestCase - -from .support import uncache - - -class TestUncache(TestCase): - - def test_cant_uncache_sys(self): - with self.assertRaises(ValueError): - with uncache('sys'): pass - - def test_uncache_non_existent(self): - with uncache('mock.tests.support.bad'): pass diff --git a/mock/tests/testwith.py b/mock/tests/testwith.py index 587fde9..42ebf38 100644 --- a/mock/tests/testwith.py +++ b/mock/tests/testwith.py @@ -1,13 +1,9 @@ -# Copyright (C) 2007-2012 Michael Foord & the mock team -# E-mail: fuzzyman AT voidspace DOT org DOT uk -# http://www.voidspace.org.uk/python/mock/ - +import unittest from warnings import catch_warnings -import unittest +from unittest.test.testmock.support import is_instance +from unittest.mock import MagicMock, Mock, patch, sentinel, mock_open, call -from mock.tests.support import is_instance -from mock import MagicMock, Mock, patch, sentinel, mock_open, call something = sentinel.Something @@ -52,11 +48,10 @@ class WithTest(unittest.TestCase): def test_with_statement_nested(self): with catch_warnings(record=True): - with patch('%s.something' % __name__) as mock_something: - with patch('%s.something_else' % __name__) as mock_something_else: - self.assertEqual(something, mock_something, "unpatched") - self.assertEqual(something_else, mock_something_else, - "unpatched") + with patch('%s.something' % __name__) as mock_something, patch('%s.something_else' % __name__) as mock_something_else: + self.assertEqual(something, mock_something, "unpatched") + self.assertEqual(something_else, mock_something_else, + "unpatched") self.assertEqual(something, sentinel.Something) self.assertEqual(something_else, sentinel.SomethingElse) @@ -235,7 +230,22 @@ class TestMockOpen(unittest.TestCase): self.assertEqual(lines[1], 'bar\n') self.assertEqual(lines[2], 'baz\n') self.assertEqual(h.readline(), '') + with self.assertRaises(StopIteration): + next(h) + def test_next_data(self): + # Check that next will correctly return the next available + # line and plays well with the dunder_iter part. + mock = mock_open(read_data='foo\nbar\nbaz\n') + with patch('%s.open' % __name__, mock, create=True): + h = open('bar') + line1 = next(h) + line2 = next(h) + lines = [l for l in h] + self.assertEqual(line1, 'foo\n') + self.assertEqual(line2, 'bar\n') + self.assertEqual(lines[0], 'baz\n') + self.assertEqual(h.readline(), '') def test_readlines_data(self): # Test that emulating a file that ends in a newline character works -- cgit v1.2.3 From b81f7b60f4478b83f8614db4263ab7e4b2593783 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 08:27:06 +0000 Subject: not needed in backport --- .coveragerc | 1 - mock/tests/__init__.py | 17 ----------------- mock/tests/__main__.py | 18 ------------------ 3 files changed, 36 deletions(-) delete mode 100644 mock/tests/__main__.py diff --git a/.coveragerc b/.coveragerc index 5a29219..f3f4763 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,5 @@ [run] source = mock -omit = mock/tests/__main__.py [report] exclude_lines = diff --git a/mock/tests/__init__.py b/mock/tests/__init__.py index 87d7ae9..e69de29 100644 --- a/mock/tests/__init__.py +++ b/mock/tests/__init__.py @@ -1,17 +0,0 @@ -import os -import sys -import unittest - - -here = os.path.dirname(__file__) -loader = unittest.defaultTestLoader - -def load_tests(*args): - suite = unittest.TestSuite() - for fn in os.listdir(here): - if fn.startswith("test") and fn.endswith(".py"): - modname = "unittest.test.testmock." + fn[:-3] - __import__(modname) - module = sys.modules[modname] - suite.addTest(loader.loadTestsFromModule(module)) - return suite diff --git a/mock/tests/__main__.py b/mock/tests/__main__.py deleted file mode 100644 index 45c633a..0000000 --- a/mock/tests/__main__.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -import unittest - - -def load_tests(loader, standard_tests, pattern): - # top level directory cached on loader instance - this_dir = os.path.dirname(__file__) - pattern = pattern or "test*.py" - # We are inside unittest.test.testmock, so the top-level is three notches up - top_level_dir = os.path.dirname(os.path.dirname(os.path.dirname(this_dir))) - package_tests = loader.discover(start_dir=this_dir, pattern=pattern, - top_level_dir=top_level_dir) - standard_tests.addTests(package_tests) - return standard_tests - - -if __name__ == '__main__': - unittest.main() -- cgit v1.2.3 From 389e48e48edca1a04f4b5a9fa29d434669b4d2e8 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 12:42:08 +0000 Subject: fix imports --- mock/tests/testasync.py | 4 ++-- mock/tests/testcallable.py | 7 +++---- mock/tests/testhelpers.py | 8 ++++---- mock/tests/testmagicmethods.py | 4 ++-- mock/tests/testmock.py | 10 +++++----- mock/tests/testpatch.py | 37 ++++++++++++++++++------------------- mock/tests/testsealable.py | 2 +- mock/tests/testsentinel.py | 2 +- mock/tests/testwith.py | 4 ++-- 9 files changed, 38 insertions(+), 40 deletions(-) diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index fa906e4..1910fba 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -2,8 +2,8 @@ import asyncio import inspect import unittest -from unittest.mock import (call, AsyncMock, patch, MagicMock, create_autospec, - _AwaitEvent) +from mock import call, AsyncMock, patch, MagicMock, create_autospec +from mock.mock import _AwaitEvent def tearDownModule(): diff --git a/mock/tests/testcallable.py b/mock/tests/testcallable.py index 5eadc00..41715ed 100644 --- a/mock/tests/testcallable.py +++ b/mock/tests/testcallable.py @@ -3,14 +3,13 @@ # http://www.voidspace.org.uk/python/mock/ import unittest -from unittest.test.testmock.support import is_instance, X, SomeClass +from mock.tests.support import is_instance, X, SomeClass -from unittest.mock import ( +from mock import ( Mock, MagicMock, NonCallableMagicMock, NonCallableMock, patch, create_autospec, - CallableMixin ) - +from mock.mock import CallableMixin class TestCallable(unittest.TestCase): diff --git a/mock/tests/testhelpers.py b/mock/tests/testhelpers.py index 301bca4..c58e55a 100644 --- a/mock/tests/testhelpers.py +++ b/mock/tests/testhelpers.py @@ -3,10 +3,11 @@ import time import types import unittest -from unittest.mock import ( - call, _Call, create_autospec, MagicMock, - Mock, ANY, _CallList, patch, PropertyMock, _callable +from mock import ( + call, create_autospec, MagicMock, + Mock, ANY, patch, PropertyMock ) +from mock.mock import _Call, _CallList, _callable from datetime import datetime from functools import partial @@ -17,7 +18,6 @@ class SomeClass(object): def three(self, a=None): pass - class AnyTest(unittest.TestCase): def test_any(self): diff --git a/mock/tests/testmagicmethods.py b/mock/tests/testmagicmethods.py index 130a339..fdb6f19 100644 --- a/mock/tests/testmagicmethods.py +++ b/mock/tests/testmagicmethods.py @@ -2,8 +2,8 @@ import math import unittest import os import sys -from unittest.mock import Mock, MagicMock, _magics - +from mock import Mock, MagicMock +from mock.mock import _magics class TestMockingMagicMethods(unittest.TestCase): diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 0f30bcc..8a2f0e7 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -4,14 +4,14 @@ import sys import tempfile import unittest -from unittest.test.testmock.support import is_instance -from unittest import mock -from unittest.mock import ( +from mock.tests.support import is_instance +from mock import ( call, DEFAULT, patch, sentinel, MagicMock, Mock, NonCallableMock, - NonCallableMagicMock, AsyncMock, _Call, _CallList, - create_autospec + NonCallableMagicMock, AsyncMock, + create_autospec, mock ) +from mock.mock import _Call, _CallList class Iter(object): diff --git a/mock/tests/testpatch.py b/mock/tests/testpatch.py index 27914a9..15b3068 100644 --- a/mock/tests/testpatch.py +++ b/mock/tests/testpatch.py @@ -6,16 +6,16 @@ import os import sys import unittest -from unittest.test.testmock import support -from unittest.test.testmock.support import SomeClass, is_instance +from mock.tests import support +from mock.tests.support import SomeClass, is_instance from test.test_importlib.util import uncache -from unittest.mock import ( - NonCallableMock, CallableMixin, sentinel, - MagicMock, Mock, NonCallableMagicMock, patch, _patch, - DEFAULT, call, _get_target +from mock import ( + NonCallableMock, sentinel, + MagicMock, Mock, NonCallableMagicMock, patch, + DEFAULT, call ) - +from mock.mock import CallableMixin, _patch, _get_target builtin_string = 'builtins' @@ -664,7 +664,7 @@ class PatchTest(unittest.TestCase): # the new dictionary during function call original = support.target.copy() - @patch.dict('unittest.test.testmock.support.target', {'bar': 'BAR'}) + @patch.dict('mock.tests.support.target', {'bar': 'BAR'}) def test(): self.assertEqual(support.target, {'foo': 'BAZ', 'bar': 'BAR'}) @@ -1540,7 +1540,7 @@ class PatchTest(unittest.TestCase): self.assertEqual(foo.fish, 'tasty') - @patch('unittest.mock.patch.TEST_PREFIX', 'foo') + @patch('mock.patch.TEST_PREFIX', 'foo') def test_patch_test_prefix(self): class Foo(object): thing = 'original' @@ -1563,7 +1563,7 @@ class PatchTest(unittest.TestCase): self.assertEqual(foo.test_two(), 'original') - @patch('unittest.mock.patch.TEST_PREFIX', 'bar') + @patch('mock.patch.TEST_PREFIX', 'bar') def test_patch_dict_test_prefix(self): class Foo(object): def bar_one(self): @@ -1601,7 +1601,7 @@ class PatchTest(unittest.TestCase): def test_patch_nested_autospec_repr(self): - with patch('unittest.test.testmock.support', autospec=True) as m: + with patch('mock.tests.support', autospec=True) as m: self.assertIn(" name='support.SomeClass.wibble()'", repr(m.SomeClass.wibble())) self.assertIn(" name='support.SomeClass().wibble()'", @@ -1818,7 +1818,7 @@ class PatchTest(unittest.TestCase): with patch.object(foo, '__module__', "testpatch2"): self.assertEqual(foo.__module__, "testpatch2") - self.assertEqual(foo.__module__, 'unittest.test.testmock.testpatch') + self.assertEqual(foo.__module__, 'mock.tests.testpatch') with patch.object(foo, '__annotations__', dict([('s', 1, )])): self.assertEqual(foo.__annotations__, dict([('s', 1, )])) @@ -1834,16 +1834,15 @@ class PatchTest(unittest.TestCase): # This exercises the AttributeError branch of _dot_lookup. # make sure it's there - import unittest.test.testmock.support + import mock.tests.support # now make sure it's not: with patch.dict('sys.modules'): - del sys.modules['unittest.test.testmock.support'] - del sys.modules['unittest.test.testmock'] - del sys.modules['unittest.test'] - del sys.modules['unittest'] + del sys.modules['mock.tests.support'] + del sys.modules['mock.tests'] + del sys.modules['mock'] # now make sure we can patch based on a dotted path: - @patch('unittest.test.testmock.support.X') + @patch('mock.tests.support.X') def test(mock): pass test() @@ -1855,7 +1854,7 @@ class PatchTest(unittest.TestCase): def test_cant_set_kwargs_when_passing_a_mock(self): - @patch('unittest.test.testmock.support.X', new=object(), x=1) + @patch('mock.tests.support.X', new=object(), x=1) def test(): pass with self.assertRaises(TypeError): test() diff --git a/mock/tests/testsealable.py b/mock/tests/testsealable.py index 59f5233..63a8541 100644 --- a/mock/tests/testsealable.py +++ b/mock/tests/testsealable.py @@ -1,5 +1,5 @@ import unittest -from unittest import mock +import mock class SampleObject: diff --git a/mock/tests/testsentinel.py b/mock/tests/testsentinel.py index de53509..5666434 100644 --- a/mock/tests/testsentinel.py +++ b/mock/tests/testsentinel.py @@ -1,7 +1,7 @@ import unittest import copy import pickle -from unittest.mock import sentinel, DEFAULT +from mock import sentinel, DEFAULT class SentinelTest(unittest.TestCase): diff --git a/mock/tests/testwith.py b/mock/tests/testwith.py index 42ebf38..825387b 100644 --- a/mock/tests/testwith.py +++ b/mock/tests/testwith.py @@ -1,8 +1,8 @@ import unittest from warnings import catch_warnings -from unittest.test.testmock.support import is_instance -from unittest.mock import MagicMock, Mock, patch, sentinel, mock_open, call +from mock.tests.support import is_instance +from mock import MagicMock, Mock, patch, sentinel, mock_open, call -- cgit v1.2.3 From b3d20d40b2477816e43cd4cef4f417632b09614b Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 13:04:06 +0000 Subject: vendor uncache helper in to our package. --- mock/tests/support.py | 29 +++++++++++++++++++++++++++++ mock/tests/testpatch.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/mock/tests/support.py b/mock/tests/support.py index 49986d6..40fd3a0 100644 --- a/mock/tests/support.py +++ b/mock/tests/support.py @@ -1,3 +1,6 @@ +import contextlib +import sys + target = {'foo': 'FOO'} @@ -14,3 +17,29 @@ class SomeClass(object): class X(object): pass + + +@contextlib.contextmanager +def uncache(*names): + """Uncache a module from sys.modules. + + A basic sanity check is performed to prevent uncaching modules that either + cannot/shouldn't be uncached. + + """ + for name in names: + if name in ('sys', 'marshal', 'imp'): + raise ValueError( + "cannot uncache {0}".format(name)) + try: + del sys.modules[name] + except KeyError: + pass + try: + yield + finally: + for name in names: + try: + del sys.modules[name] + except KeyError: + pass diff --git a/mock/tests/testpatch.py b/mock/tests/testpatch.py index 15b3068..e9f76a4 100644 --- a/mock/tests/testpatch.py +++ b/mock/tests/testpatch.py @@ -9,7 +9,7 @@ import unittest from mock.tests import support from mock.tests.support import SomeClass, is_instance -from test.test_importlib.util import uncache +from .support import uncache from mock import ( NonCallableMock, sentinel, MagicMock, Mock, NonCallableMagicMock, patch, -- cgit v1.2.3 From cdf2d7979a653dda4a2ad6b06bb068d20b3427d6 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 19:02:52 +0000 Subject: py3.6 needs the inner mock module to be deleted too --- mock/tests/testpatch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mock/tests/testpatch.py b/mock/tests/testpatch.py index e9f76a4..4518f87 100644 --- a/mock/tests/testpatch.py +++ b/mock/tests/testpatch.py @@ -1839,6 +1839,7 @@ class PatchTest(unittest.TestCase): with patch.dict('sys.modules'): del sys.modules['mock.tests.support'] del sys.modules['mock.tests'] + del sys.modules['mock.mock'] del sys.modules['mock'] # now make sure we can patch based on a dotted path: -- cgit v1.2.3 From 45c1e65142cf605bb2686a130a9f92331c697378 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 19:04:01 +0000 Subject: run() implementation for py3.6, where it's missing --- mock/tests/testasync.py | 109 +++++++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 48 deletions(-) diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 1910fba..5843bba 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -1,3 +1,4 @@ + import asyncio import inspect import unittest @@ -6,6 +7,18 @@ from mock import call, AsyncMock, patch, MagicMock, create_autospec from mock.mock import _AwaitEvent +try: + from asyncio import run +except ImportError: + def run(main): + loop = asyncio.new_event_loop() + try: + return_value = loop.run_until_complete(main) + finally: + loop.close() + return return_value + + def tearDownModule(): asyncio.set_event_loop_policy(None) @@ -48,13 +61,13 @@ class AsyncPatchDecoratorTest(unittest.TestCase): def test_async(mock_method): m = mock_method() self.assertTrue(inspect.isawaitable(m)) - asyncio.run(m) + run(m) @patch(f'{async_foo_name}.async_method') def test_no_parent_attribute(mock_method): m = mock_method() self.assertTrue(inspect.isawaitable(m)) - asyncio.run(m) + run(m) test_async() test_no_parent_attribute() @@ -71,7 +84,7 @@ class AsyncPatchDecoratorTest(unittest.TestCase): async def test_async(): self.assertIsInstance(async_func, AsyncMock) - asyncio.run(test_async()) + run(test_async()) self.assertTrue(inspect.iscoroutinefunction(async_func)) @@ -88,7 +101,7 @@ class AsyncPatchCMTest(unittest.TestCase): with patch.object(AsyncClass, 'async_method') as mock_method: m = mock_method() self.assertTrue(inspect.isawaitable(m)) - asyncio.run(m) + run(m) test_async() @@ -105,7 +118,7 @@ class AsyncPatchCMTest(unittest.TestCase): self.assertIsInstance(async_func, AsyncMock) self.assertTrue(inspect.iscoroutinefunction(async_func)) - asyncio.run(test_async()) + run(test_async()) class AsyncMockTest(unittest.TestCase): @@ -123,7 +136,7 @@ class AsyncMockTest(unittest.TestCase): mock = AsyncMock() m = mock() self.assertTrue(inspect.isawaitable(m)) - asyncio.run(m) + run(m) self.assertIn('assert_awaited', dir(mock)) def test_iscoroutinefunction_normal_function(self): @@ -172,7 +185,7 @@ class AsyncAutospecTest(unittest.TestCase): self.assertIsInstance(spec.awaited, _AwaitEvent) spec.assert_not_awaited() - asyncio.run(main()) + run(main()) self.assertTrue(asyncio.iscoroutinefunction(spec)) self.assertTrue(asyncio.iscoroutine(awaitable)) @@ -217,7 +230,7 @@ class AsyncAutospecTest(unittest.TestCase): self.assertIsNone(mock_method.await_args) self.assertEqual(mock_method.await_args_list, []) - asyncio.run(test_async()) + run(test_async()) class AsyncSpecTest(unittest.TestCase): @@ -226,42 +239,42 @@ class AsyncSpecTest(unittest.TestCase): self.assertIsInstance(mock, MagicMock) m = mock() self.assertTrue(inspect.isawaitable(m)) - asyncio.run(m) + run(m) def test_spec_as_async_kw_magicmock(self): mock = MagicMock(spec=async_func) self.assertIsInstance(mock, MagicMock) m = mock() self.assertTrue(inspect.isawaitable(m)) - asyncio.run(m) + run(m) def test_spec_as_async_kw_AsyncMock(self): mock = AsyncMock(spec=async_func) self.assertIsInstance(mock, AsyncMock) m = mock() self.assertTrue(inspect.isawaitable(m)) - asyncio.run(m) + run(m) def test_spec_as_async_positional_AsyncMock(self): mock = AsyncMock(async_func) self.assertIsInstance(mock, AsyncMock) m = mock() self.assertTrue(inspect.isawaitable(m)) - asyncio.run(m) + run(m) def test_spec_as_normal_kw_AsyncMock(self): mock = AsyncMock(spec=normal_func) self.assertIsInstance(mock, AsyncMock) m = mock() self.assertTrue(inspect.isawaitable(m)) - asyncio.run(m) + run(m) def test_spec_as_normal_positional_AsyncMock(self): mock = AsyncMock(normal_func) self.assertIsInstance(mock, AsyncMock) m = mock() self.assertTrue(inspect.isawaitable(m)) - asyncio.run(m) + run(m) def test_spec_async_mock(self): @patch.object(AsyncClass, 'async_method', spec=True) @@ -328,7 +341,7 @@ class AsyncArguments(unittest.TestCase): return var + 1 mock = AsyncMock(addition, return_value=10) - output = asyncio.run(mock(5)) + output = run(mock(5)) self.assertEqual(output, 10) @@ -337,23 +350,23 @@ class AsyncArguments(unittest.TestCase): return var + 1 mock = AsyncMock(addition, side_effect=Exception('err')) with self.assertRaises(Exception): - asyncio.run(mock(5)) + 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)) + result = 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())) + self.assertEqual(item, run(mock())) with self.assertRaises(RuntimeError) as e: - asyncio.run(mock()) + run(mock()) self.assertEqual( e.exception, RuntimeError('coroutine raised StopIteration') @@ -389,7 +402,7 @@ class AsyncContextManagerTest(unittest.TestCase): called = True return result - result = asyncio.run(use_context_manager()) + result = run(use_context_manager()) self.assertFalse(instance.entered) self.assertFalse(instance.exited) self.assertTrue(called) @@ -411,7 +424,7 @@ class AsyncContextManagerTest(unittest.TestCase): async with mock_instance as result: return result - self.assertIs(asyncio.run(use_context_manager()), expected_result) + self.assertIs(run(use_context_manager()), expected_result) def test_mock_customize_async_context_manager_with_coroutine(self): enter_called = False @@ -435,7 +448,7 @@ class AsyncContextManagerTest(unittest.TestCase): async with mock_instance: pass - asyncio.run(use_context_manager()) + run(use_context_manager()) self.assertTrue(enter_called) self.assertTrue(exit_called) @@ -447,7 +460,7 @@ class AsyncContextManagerTest(unittest.TestCase): instance = self.WithAsyncContextManager() mock_instance = MagicMock(instance) with self.assertRaises(TypeError): - asyncio.run(raise_in(mock_instance)) + run(raise_in(mock_instance)) class AsyncIteratorTest(unittest.TestCase): @@ -477,11 +490,11 @@ class AsyncIteratorTest(unittest.TestCase): iterator = instance.__aiter__() if asyncio.iscoroutine(iterator): - iterator = asyncio.run(iterator) + iterator = run(iterator) mock_iterator = mock_instance.__aiter__() if asyncio.iscoroutine(mock_iterator): - mock_iterator = asyncio.run(mock_iterator) + mock_iterator = run(mock_iterator) self.assertEqual(asyncio.iscoroutine(iterator.__aiter__), asyncio.iscoroutine(mock_iterator.__aiter__)) @@ -499,17 +512,17 @@ class AsyncIteratorTest(unittest.TestCase): expected = ["FOO", "BAR", "BAZ"] with self.subTest("iterate through default value"): mock_instance = MagicMock(self.WithAsyncIterator()) - self.assertEqual([], asyncio.run(iterate(mock_instance))) + self.assertEqual([], 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))) + self.assertEqual(expected, 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))) + self.assertEqual(expected, run(iterate(mock_instance))) class AsyncMockAssert(unittest.TestCase): @@ -526,30 +539,30 @@ class AsyncMockAssert(unittest.TestCase): with self.assertRaises(AssertionError): self.mock.assert_awaited() - asyncio.run(self._runnable_test()) + 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()) + run(self._runnable_test()) self.mock.assert_awaited_once() - asyncio.run(self._runnable_test()) + run(self._runnable_test()) with self.assertRaises(AssertionError): self.mock.assert_awaited_once() def test_assert_awaited_with(self): - asyncio.run(self._runnable_test()) + run(self._runnable_test()) msg = 'expected await not found' with self.assertRaisesRegex(AssertionError, msg): self.mock.assert_awaited_with('foo') - asyncio.run(self._runnable_test('foo')) + run(self._runnable_test('foo')) self.mock.assert_awaited_with('foo') - asyncio.run(self._runnable_test('SomethingElse')) + run(self._runnable_test('SomethingElse')) with self.assertRaises(AssertionError): self.mock.assert_awaited_with('foo') @@ -557,10 +570,10 @@ class AsyncMockAssert(unittest.TestCase): with self.assertRaises(AssertionError): self.mock.assert_awaited_once_with('foo') - asyncio.run(self._runnable_test('foo')) + run(self._runnable_test('foo')) self.mock.assert_awaited_once_with('foo') - asyncio.run(self._runnable_test('foo')) + run(self._runnable_test('foo')) with self.assertRaises(AssertionError): self.mock.assert_awaited_once_with('foo') @@ -568,14 +581,14 @@ class AsyncMockAssert(unittest.TestCase): with self.assertRaises(AssertionError): self.mock.assert_any_await('NormalFoo') - asyncio.run(self._runnable_test('foo')) + run(self._runnable_test('foo')) with self.assertRaises(AssertionError): self.mock.assert_any_await('NormalFoo') - asyncio.run(self._runnable_test('NormalFoo')) + run(self._runnable_test('NormalFoo')) self.mock.assert_any_await('NormalFoo') - asyncio.run(self._runnable_test('SomethingElse')) + run(self._runnable_test('SomethingElse')) self.mock.assert_any_await('NormalFoo') def test_assert_has_awaits_no_order(self): @@ -585,18 +598,18 @@ class AsyncMockAssert(unittest.TestCase): self.mock.assert_has_awaits(calls) self.assertEqual(len(cm.exception.args), 1) - asyncio.run(self._runnable_test('foo')) + run(self._runnable_test('foo')) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls) - asyncio.run(self._runnable_test('NormalFoo')) + run(self._runnable_test('NormalFoo')) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls) - asyncio.run(self._runnable_test('baz')) + run(self._runnable_test('baz')) self.mock.assert_has_awaits(calls) - asyncio.run(self._runnable_test('SomethingElse')) + run(self._runnable_test('SomethingElse')) self.mock.assert_has_awaits(calls) def test_assert_has_awaits_ordered(self): @@ -604,23 +617,23 @@ class AsyncMockAssert(unittest.TestCase): with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls, any_order=True) - asyncio.run(self._runnable_test('baz')) + run(self._runnable_test('baz')) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls, any_order=True) - asyncio.run(self._runnable_test('foo')) + run(self._runnable_test('foo')) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls, any_order=True) - asyncio.run(self._runnable_test('NormalFoo')) + run(self._runnable_test('NormalFoo')) self.mock.assert_has_awaits(calls, any_order=True) - asyncio.run(self._runnable_test('qux')) + 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()) + run(self._runnable_test()) with self.assertRaises(AssertionError): self.mock.assert_not_awaited() -- cgit v1.2.3 From 12bd6d5cbc494d8e2e8dc7b0815a4c5ff92a802b Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 15 Jan 2020 08:20:45 +0000 Subject: PyPy's object class has no __sizeof__. --- mock/__init__.py | 4 +++- mock/mock.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mock/__init__.py b/mock/__init__.py index 1f29771..cdf5a16 100644 --- a/mock/__init__.py +++ b/mock/__init__.py @@ -1,6 +1,8 @@ from __future__ import absolute_import -import re +import re, sys + +IS_PYPY = 'PyPy' in sys.version import mock.mock as _mock from mock.mock import * diff --git a/mock/mock.py b/mock/mock.py index be96194..aaf0452 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -36,6 +36,7 @@ from types import CodeType, ModuleType, MethodType from unittest.util import safe_repr from functools import wraps, partial +from mock import IS_PYPY _builtins = {name for name in dir(builtins) if not name.startswith('_')} @@ -1819,6 +1820,10 @@ magic_methods = ( "fspath " ) +if IS_PYPY: + # PyPy has no __sizeof__: http://doc.pypy.org/en/latest/cpython_differences.html + magic_methods = magic_methods.replace('sizeof ', '') + numerics = ( "add sub mul matmul div floordiv mod lshift rshift and xor or pow truediv" ) -- cgit v1.2.3 From 3d44cb87bfbee040fdd219d1460ce186fb06882d Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 15 Jan 2020 08:21:09 +0000 Subject: re-introduce skips for https://bitbucket.org/pypy/pypy/issues/3010 --- mock/tests/testhelpers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mock/tests/testhelpers.py b/mock/tests/testhelpers.py index c58e55a..8b4d709 100644 --- a/mock/tests/testhelpers.py +++ b/mock/tests/testhelpers.py @@ -8,10 +8,14 @@ from mock import ( Mock, ANY, patch, PropertyMock ) from mock.mock import _Call, _CallList, _callable +from mock import IS_PYPY from datetime import datetime from functools import partial +import pytest + + class SomeClass(object): def one(self, a, b): pass def two(self): pass @@ -457,6 +461,8 @@ class SpecSignatureTest(unittest.TestCase): self._check_someclass_mock(mock) + @pytest.mark.skipif(IS_PYPY, + reason="https://bitbucket.org/pypy/pypy/issues/3010") def test_spec_has_descriptor_returning_function(self): class CrazyDescriptor(object): @@ -885,6 +891,8 @@ class SpecSignatureTest(unittest.TestCase): check_data_descriptor(foo.desc) + @pytest.mark.skipif(IS_PYPY, + reason="https://bitbucket.org/pypy/pypy/issues/3010") def test_autospec_on_bound_builtin_function(self): meth = types.MethodType(time.ctime, time.time()) self.assertIsInstance(meth(), str) -- cgit v1.2.3 From 9b35e34a8cd0cfea7465c7b251d982f31726d7c5 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 15 Jan 2020 08:34:43 +0000 Subject: Skip cpython's 2085bd0877e17ad4d98a4586d5eabb6faecbb190 as PEP 570 syntax for positional-only parameters would limit this codebase to being Py3.8+ only. --- lastsync.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lastsync.txt b/lastsync.txt index ff52fa8..fbd0d7c 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -4a686504eb2bbf69adf78077458508a7ba131667 +2085bd0877e17ad4d98a4586d5eabb6faecbb190 -- cgit v1.2.3 From 20abe16d0dad5be502be77e61cbfa882d83fe58c Mon Sep 17 00:00:00 2001 From: Xtreak Date: Mon, 22 Jul 2019 13:08:22 +0530 Subject: bpo-21478: Record calls to parent when autospecced objects are used as child with attach_mock (GH 14688) * Clear name and parent of mock in autospecced objects used with attach_mock * Add NEWS entry * Fix reversed order of comparison * Test child and standalone function calls * Use a helper function extracting mock to avoid code duplication and refactor tests. Backports: 7397cda99795a4a8d96193d710105e77a07b7411 Signed-off-by: Chris Withers --- NEWS.d/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst | 2 ++ mock/mock.py | 27 ++++++++++-------- mock/tests/testmock.py | 37 +++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 NEWS.d/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst diff --git a/NEWS.d/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst b/NEWS.d/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst new file mode 100644 index 0000000..0ac9b8e --- /dev/null +++ b/NEWS.d/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst @@ -0,0 +1,2 @@ +Record calls to parent when autospecced object is attached to a mock using +:func:`unittest.mock.attach_mock`. Patch by Karthikeyan Singaravelan. diff --git a/mock/mock.py b/mock/mock.py index aaf0452..437f2d1 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -73,6 +73,15 @@ def _is_exception(obj): ) +def _extract_mock(obj): + # Autospecced functions will return a FunctionType with "mock" attribute + # which is the actual mock object that needs to be used. + if isinstance(obj, FunctionTypes) and hasattr(obj, 'mock'): + return obj.mock + else: + return obj + + def _get_signature_object(func, as_instance, eat_self): """ Given an arbitrary, possibly callable object, try to create a suitable @@ -347,13 +356,7 @@ class _CallList(list): def _check_and_set_parent(parent, value, name, new_name): - # function passed to create_autospec will have mock - # attribute attached to which parent must be set - if isinstance(value, FunctionTypes): - try: - value = value.mock - except AttributeError: - pass + value = _extract_mock(value) if not _is_instance_mock(value): return False @@ -468,10 +471,12 @@ class NonCallableMock(Base): Attach a mock as an attribute of this one, replacing its name and parent. Calls to the attached mock will be recorded in the `method_calls` and `mock_calls` attributes of this one.""" - mock._mock_parent = None - mock._mock_new_parent = None - mock._mock_name = '' - mock._mock_new_name = None + inner_mock = _extract_mock(mock) + + inner_mock._mock_parent = None + inner_mock._mock_new_parent = None + inner_mock._mock_name = '' + inner_mock._mock_new_name = None setattr(self, attribute, mock) diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 8a2f0e7..5ad1016 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -37,6 +37,9 @@ class Something(object): def smeth(a, b, c, d=None): pass +def something(a): pass + + class MockTest(unittest.TestCase): def test_all(self): @@ -1808,6 +1811,26 @@ class MockTest(unittest.TestCase): self.assertEqual(m.mock_calls, call().foo().call_list()) + def test_attach_mock_patch_autospec(self): + parent = Mock() + + with mock.patch(f'{__name__}.something', autospec=True) as mock_func: + self.assertEqual(mock_func.mock._extract_mock_name(), 'something') + parent.attach_mock(mock_func, 'child') + parent.child(1) + something(2) + mock_func(3) + + parent_calls = [call.child(1), call.child(2), call.child(3)] + child_calls = [call(1), call(2), call(3)] + self.assertEqual(parent.mock_calls, parent_calls) + self.assertEqual(parent.child.mock_calls, child_calls) + self.assertEqual(something.mock_calls, child_calls) + self.assertEqual(mock_func.mock_calls, child_calls) + self.assertIn('mock.child', repr(parent.child.mock)) + self.assertEqual(mock_func.mock._extract_mock_name(), 'mock.child') + + def test_attribute_deletion(self): for mock in (Mock(), MagicMock(), NonCallableMagicMock(), NonCallableMock()): @@ -1891,6 +1914,20 @@ class MockTest(unittest.TestCase): self.assertRaises(TypeError, mock.child, 1) self.assertEqual(mock.mock_calls, [call.child(1, 2)]) + self.assertIn('mock.child', repr(mock.child.mock)) + + def test_parent_propagation_with_autospec_attach_mock(self): + + def foo(a, b): pass + + parent = Mock() + parent.attach_mock(create_autospec(foo, name='bar'), 'child') + parent.child(1, 2) + + self.assertRaises(TypeError, parent.child, 1) + self.assertEqual(parent.child.mock_calls, [call.child(1, 2)]) + self.assertIn('mock.child', repr(parent.child.mock)) + def test_isinstance_under_settrace(self): # bpo-36593 : __class__ is not set for a class that has __class__ -- cgit v1.2.3 From ae02b3025132cc0c4adf3eb5085e5c91f04dbba2 Mon Sep 17 00:00:00 2001 From: Min ho Kim Date: Wed, 31 Jul 2019 08:16:13 +1000 Subject: Fix typos in comments, docs and test names (#15018) * Fix typos in comments, docs and test names * Update test_pyparse.py account for change in string length * Apply suggestion: splitable -> splittable Co-Authored-By: Terry Jan Reedy * Apply suggestion: splitable -> splittable Co-Authored-By: Terry Jan Reedy * Apply suggestion: Dealloccte -> Deallocate Co-Authored-By: Terry Jan Reedy * Update posixmodule checksum. * Reverse idlelib changes. Backports: c4cacc8c5eab50db8da3140353596f38a01115ca Signed-off-by: Chris Withers --- lastsync.txt | 2 +- mock/tests/testmock.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lastsync.txt b/lastsync.txt index fbd0d7c..d1e4418 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -2085bd0877e17ad4d98a4586d5eabb6faecbb190 +7397cda99795a4a8d96193d710105e77a07b7411 diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 5ad1016..3d196e2 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -262,7 +262,7 @@ class MockTest(unittest.TestCase): ret_val = mock(sentinel.Arg) self.assertTrue(mock.called, "called not set") - self.assertEqual(mock.call_count, 1, "call_count incoreect") + self.assertEqual(mock.call_count, 1, "call_count incorrect") self.assertEqual(mock.call_args, ((sentinel.Arg,), {}), "call_args not set") self.assertEqual(mock.call_args.args, (sentinel.Arg,), -- cgit v1.2.3 From cd18e01ba0f9ac50e06e6131cde73d16b191bfca Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 8 Aug 2019 08:42:54 +0300 Subject: bpo-37685: Fixed __eq__, __lt__ etc implementations in some classes. (GH-14952) They now return NotImplemented for unsupported type of the other operand. Backports: 662db125cddbca1db68116c547c290eb3943d98e Signed-off-by: Chris Withers --- NEWS.d/2019-07-26-00-12-29.bpo-37685.TqckMZ.rst | 4 ++++ lastsync.txt | 2 +- mock/mock.py | 4 +--- mock/tests/support.py | 12 ++++++++++++ mock/tests/testmock.py | 8 ++++++++ 5 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 NEWS.d/2019-07-26-00-12-29.bpo-37685.TqckMZ.rst diff --git a/NEWS.d/2019-07-26-00-12-29.bpo-37685.TqckMZ.rst b/NEWS.d/2019-07-26-00-12-29.bpo-37685.TqckMZ.rst new file mode 100644 index 0000000..d1179a6 --- /dev/null +++ b/NEWS.d/2019-07-26-00-12-29.bpo-37685.TqckMZ.rst @@ -0,0 +1,4 @@ +Fixed ``__eq__``, ``__lt__`` etc implementations in some classes. They now +return :data:`NotImplemented` for unsupported type of the other operand. +This allows the other operand to play role (for example the equality +comparison with :data:`~unittest.mock.ANY` will return ``True``). diff --git a/lastsync.txt b/lastsync.txt index d1e4418..69bd2fe 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -7397cda99795a4a8d96193d710105e77a07b7411 +c4cacc8c5eab50db8da3140353596f38a01115ca diff --git a/mock/mock.py b/mock/mock.py index 437f2d1..b989561 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -2377,12 +2377,10 @@ class _Call(tuple): def __eq__(self, other): - if other is ANY: - return True try: len_other = len(other) except TypeError: - return False + return NotImplemented self_name = '' if len(self) == 2: diff --git a/mock/tests/support.py b/mock/tests/support.py index 40fd3a0..79576dd 100644 --- a/mock/tests/support.py +++ b/mock/tests/support.py @@ -43,3 +43,15 @@ def uncache(*names): del sys.modules[name] except KeyError: pass + + +class _ALWAYS_EQ: + """ + Object that is equal to anything. + """ + def __eq__(self, other): + return True + def __ne__(self, other): + return False + +ALWAYS_EQ = _ALWAYS_EQ() diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 3d196e2..5819ece 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -4,6 +4,7 @@ import sys import tempfile import unittest +from mock.tests.support import ALWAYS_EQ from mock.tests.support import is_instance from mock import ( call, DEFAULT, patch, sentinel, @@ -322,6 +323,8 @@ class MockTest(unittest.TestCase): self.assertFalse(mm != mock.ANY) self.assertTrue(mock.ANY == mm) self.assertFalse(mock.ANY != mm) + self.assertTrue(mm == ALWAYS_EQ) + self.assertFalse(mm != ALWAYS_EQ) call1 = mock.call(mock.MagicMock()) call2 = mock.call(mock.ANY) @@ -330,6 +333,11 @@ class MockTest(unittest.TestCase): self.assertTrue(call2 == call1) self.assertFalse(call2 != call1) + self.assertTrue(call1 == ALWAYS_EQ) + self.assertFalse(call1 != ALWAYS_EQ) + self.assertFalse(call1 == 1) + self.assertTrue(call1 != 1) + def test_assert_called_with(self): mock = Mock() -- cgit v1.2.3 From 9f43738d3d2bfc4eb1a660f9f8c43f3f4e2881fd Mon Sep 17 00:00:00 2001 From: Xtreak Date: Thu, 29 Aug 2019 11:39:01 +0530 Subject: bpo-36871: Ensure method signature is used when asserting mock calls to a method (GH13261) * Fix call_matcher for mock when using methods * Add NEWS entry * Use None check and convert doctest to unittest * Use better name for mock in tests. Handle _SpecState when the attribute was not accessed and add tests. * Use reset_mock instead of reinitialization. Change inner class constructor signature for check * Reword comment regarding call object lookup logic Backports: c96127821ebda50760e788b1213975a0d5bea37f Signed-off-by: Chris Withers --- NEWS.d/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst | 3 ++ mock/mock.py | 36 ++++++++++++++++++- mock/tests/testmock.py | 48 +++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 NEWS.d/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst diff --git a/NEWS.d/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst b/NEWS.d/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst new file mode 100644 index 0000000..218795f --- /dev/null +++ b/NEWS.d/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst @@ -0,0 +1,3 @@ +Ensure method signature is used instead of constructor signature of a class +while asserting mock object against method calls. Patch by Karthikeyan +Singaravelan. diff --git a/mock/mock.py b/mock/mock.py index b989561..3431639 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -805,6 +805,35 @@ class NonCallableMock(Base): return message % (action, expected_string, actual_string) + def _get_call_signature_from_name(self, name): + """ + * If call objects are asserted against a method/function like obj.meth1 + then there could be no name for the call object to lookup. Hence just + return the spec_signature of the method/function being asserted against. + * If the name is not empty then remove () and split by '.' to get + list of names to iterate through the children until a potential + match is found. A child mock is created only during attribute access + so if we get a _SpecState then no attributes of the spec were accessed + and can be safely exited. + """ + if not name: + return self._spec_signature + + sig = None + names = name.replace('()', '').split('.') + children = self._mock_children + + for name in names: + child = children.get(name) + if child is None or isinstance(child, _SpecState): + break + else: + children = child._mock_children + sig = child._spec_signature + + return sig + + def _call_matcher(self, _call): """ Given a call (or simply an (args, kwargs) tuple), return a @@ -812,7 +841,12 @@ class NonCallableMock(Base): This is a best effort method which relies on the spec's signature, if available, or falls back on the arguments themselves. """ - sig = self._spec_signature + + if isinstance(_call, tuple) and len(_call) > 2: + sig = self._get_call_signature_from_name(_call[0]) + else: + sig = self._spec_signature + if sig is not None: if len(_call) == 2: name = '' diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 5819ece..b24890d 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -1347,6 +1347,54 @@ class MockTest(unittest.TestCase): ) + def test_assert_has_calls_nested_spec(self): + class Something: + + def __init__(self): pass + def meth(self, a, b, c, d=None): pass + + class Foo: + + def __init__(self, a): pass + def meth1(self, a, b): pass + + mock_class = create_autospec(Something) + + for m in [mock_class, mock_class()]: + m.meth(1, 2, 3, d=1) + m.assert_has_calls([call.meth(1, 2, 3, d=1)]) + m.assert_has_calls([call.meth(1, 2, 3, 1)]) + + mock_class.reset_mock() + + for m in [mock_class, mock_class()]: + self.assertRaises(AssertionError, m.assert_has_calls, [call.Foo()]) + m.Foo(1).meth1(1, 2) + m.assert_has_calls([call.Foo(1), call.Foo(1).meth1(1, 2)]) + m.Foo.assert_has_calls([call(1), call().meth1(1, 2)]) + + mock_class.reset_mock() + + invalid_calls = [call.meth(1), + call.non_existent(1), + call.Foo().non_existent(1), + call.Foo().meth(1, 2, 3, 4)] + + for kall in invalid_calls: + self.assertRaises(AssertionError, + mock_class.assert_has_calls, + [kall] + ) + + + def test_assert_has_calls_nested_without_spec(self): + m = MagicMock() + m().foo().bar().baz() + m.one().two().three() + calls = call.one().two().three().call_list() + m.assert_has_calls(calls) + + def test_assert_has_calls_with_function_spec(self): def f(a, b, c, d=None): pass -- cgit v1.2.3 From 202ff787f9c423ece9f20994f7798c2738add362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Lapeyre?= Date: Thu, 29 Aug 2019 08:15:53 +0200 Subject: bpo-35946: Improve assert_called_with documentation (GH-11796) Backports: f5896a05edf5df91fb1b55bd481ba5b2a3682f4e Signed-off-by: Chris Withers --- lastsync.txt | 2 +- mock/mock.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lastsync.txt b/lastsync.txt index 69bd2fe..b8f1999 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -c4cacc8c5eab50db8da3140353596f38a01115ca +c96127821ebda50760e788b1213975a0d5bea37f diff --git a/mock/mock.py b/mock/mock.py index 3431639..953ca84 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -892,7 +892,7 @@ class NonCallableMock(Base): raise AssertionError(msg) def assert_called_with(_mock_self, *args, **kwargs): - """assert that the mock was called with the specified arguments. + """assert that the last call was made with the specified arguments. Raises an AssertionError if the args and keyword args passed in are different to the last call to the mock.""" -- cgit v1.2.3 From 20fce5db496c7864341665233e0f73d2df1836c8 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Thu, 29 Aug 2019 01:27:42 -0700 Subject: bpo-36743: __get__ is sometimes called without the owner argument (#12992) Backports: 0dac68f1e593c11612ed54af9edb865d398f3b05 Signed-off-by: Chris Withers --- lastsync.txt | 2 +- mock/mock.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lastsync.txt b/lastsync.txt index b8f1999..33fae73 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -c96127821ebda50760e788b1213975a0d5bea37f +f5896a05edf5df91fb1b55bd481ba5b2a3682f4e diff --git a/mock/mock.py b/mock/mock.py index 953ca84..adbd87d 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -2823,7 +2823,7 @@ class PropertyMock(Mock): def _get_child_mock(self, **kwargs): return MagicMock(**kwargs) - def __get__(self, obj, obj_type): + def __get__(self, obj, obj_type=None): return self() def __set__(self, obj, val): self(val) -- cgit v1.2.3 From 97bf5a41187fe84e47fe8799df1b585af73870c4 Mon Sep 17 00:00:00 2001 From: Min ho Kim Date: Sat, 31 Aug 2019 06:21:19 +1000 Subject: Fix typos mostly in comments, docs and test names (GH-15209) Backports: 39d87b54715197ca9dcb6902bb43461c0ed701a2 Signed-off-by: Chris Withers --- lastsync.txt | 2 +- mock/tests/testpatch.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lastsync.txt b/lastsync.txt index 33fae73..34a2e3e 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -f5896a05edf5df91fb1b55bd481ba5b2a3682f4e +0dac68f1e593c11612ed54af9edb865d398f3b05 diff --git a/mock/tests/testpatch.py b/mock/tests/testpatch.py index 4518f87..ae5cdff 100644 --- a/mock/tests/testpatch.py +++ b/mock/tests/testpatch.py @@ -1651,7 +1651,7 @@ class PatchTest(unittest.TestCase): p1.stop() self.assertEqual(squizz.squozz, 3) - def test_patch_propogrates_exc_on_exit(self): + def test_patch_propagates_exc_on_exit(self): class holder: exc_info = None, None, None @@ -1680,9 +1680,9 @@ class PatchTest(unittest.TestCase): self.assertIs(holder.exc_info[0], RuntimeError) self.assertIsNotNone(holder.exc_info[1], - 'exception value not propgated') + 'exception value not propagated') self.assertIsNotNone(holder.exc_info[2], - 'exception traceback not propgated') + 'exception traceback not propagated') def test_create_and_specs(self): -- cgit v1.2.3 From 39b5272fb7099da0f3a3719a1ef0d2bd48c7efcc Mon Sep 17 00:00:00 2001 From: Xtreak Date: Mon, 9 Sep 2019 14:34:57 +0530 Subject: Fix assertions regarding magic methods function body that was not executed (GH-14154) Backports: aa515082749687c1e3bc9ec5e2296368191b9f84 Signed-off-by: Chris Withers --- lastsync.txt | 2 +- mock/tests/testasync.py | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/lastsync.txt b/lastsync.txt index 34a2e3e..4b3f4da 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -0dac68f1e593c11612ed54af9edb865d398f3b05 +39d87b54715197ca9dcb6902bb43461c0ed701a2 diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 5843bba..9e0d322 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -374,17 +374,14 @@ class AsyncArguments(unittest.TestCase): class AsyncContextManagerTest(unittest.TestCase): + class WithAsyncContextManager: - 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 + pass def test_magic_methods_are_async_mocks(self): mock = MagicMock(self.WithAsyncContextManager()) @@ -403,11 +400,7 @@ class AsyncContextManagerTest(unittest.TestCase): return result result = 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) -- cgit v1.2.3 From 93f85f9e5546a0de971f7cc62e156c252b044603 Mon Sep 17 00:00:00 2001 From: Xtreak Date: Mon, 9 Sep 2019 16:25:22 +0530 Subject: bpo-37212: Preserve keyword argument order in unittest.mock.call and error messages (GH-14310) Backports: 9d607061c9c888913ae2c18543775cf360d55f27 Signed-off-by: Chris Withers --- NEWS.d/2019-06-22-22-00-35.bpo-37212.Zhv-tq.rst | 2 ++ mock/mock.py | 2 +- mock/tests/testmock.py | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 NEWS.d/2019-06-22-22-00-35.bpo-37212.Zhv-tq.rst diff --git a/NEWS.d/2019-06-22-22-00-35.bpo-37212.Zhv-tq.rst b/NEWS.d/2019-06-22-22-00-35.bpo-37212.Zhv-tq.rst new file mode 100644 index 0000000..520a022 --- /dev/null +++ b/NEWS.d/2019-06-22-22-00-35.bpo-37212.Zhv-tq.rst @@ -0,0 +1,2 @@ +:func:`unittest.mock.call` now preserves the order of keyword arguments in +repr output. Patch by Karthikeyan Singaravelan. diff --git a/mock/mock.py b/mock/mock.py index adbd87d..bece0a5 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -2339,7 +2339,7 @@ def _format_call_signature(name, args, kwargs): formatted_args = '' args_string = ', '.join([repr(arg) for arg in args]) kwargs_string = ', '.join([ - '%s=%r' % (key, value) for key, value in sorted(kwargs.items()) + '%s=%r' % (key, value) for key, value in kwargs.items() ]) if args_string: formatted_args = args_string diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index b24890d..3947cd6 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -1571,11 +1571,11 @@ class MockTest(unittest.TestCase): m.assert_called_once() self.assertNotIn("Calls:", str(e.exception)) - #Issue21256 printout of keyword args should be in deterministic order - def test_sorted_call_signature(self): + #Issue37212 printout of keyword args now preserves the original order + def test_ordered_call_signature(self): m = Mock() m.hello(name='hello', daddy='hero') - text = "call(daddy='hero', name='hello')" + text = "call(name='hello', daddy='hero')" self.assertEqual(repr(m.hello.call_args), text) #Issue21270 overrides tuple methods for mock.call objects -- cgit v1.2.3 From 42ae9ebded7db37a290b6e7d3309b21738992a07 Mon Sep 17 00:00:00 2001 From: Mario Corchero Date: Mon, 9 Sep 2019 15:18:06 +0100 Subject: docs: Add references to AsyncMock in unittest.mock.patch (#13681) Update the docs as patch can now return an AsyncMock if the patched object is an async function. Backports: f5e7f39d2916ed150e80381faed125f405a11e11 Signed-off-by: Chris Withers --- mock/mock.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mock/mock.py b/mock/mock.py index bece0a5..2d4d89e 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -1638,8 +1638,9 @@ def patch( is patched with a `new` object. When the function/with statement exits the patch is undone. - If `new` is omitted, then the target is replaced with a - `MagicMock`. If `patch` is used as a decorator and `new` is + If `new` is omitted, then the target is replaced with an + `AsyncMock if the patched object is an async function or a + `MagicMock` otherwise. If `patch` is used as a decorator and `new` is omitted, the created mock is passed in as an extra argument to the decorated function. If `patch` is used as a context manager the created mock is returned by the context manager. @@ -1657,8 +1658,8 @@ def patch( patch to pass in the object being mocked as the spec/spec_set object. `new_callable` allows you to specify a different class, or callable object, - that will be called to create the `new` object. By default `MagicMock` is - used. + that will be called to create the `new` object. By default `AsyncMock` is + used for async functions and `MagicMock` for the rest. A more powerful form of `spec` is `autospec`. If you set `autospec=True` then the mock will be created with a spec from the object being replaced. -- cgit v1.2.3 From d8170ff058661e2a96ec871b4e48bc57b41f5a44 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Tue, 10 Sep 2019 12:18:40 +0100 Subject: bpo-37251: Removes __code__ check from _is_async_obj. (GH-15830) Backports: f1a297acb60b88917712450ebd3cfa707e6efd6b Signed-off-by: Chris Withers --- NEWS.d/2019-09-10-10-59-50.bpo-37251.8zn2o3.rst | 3 +++ mock/mock.py | 5 ++--- mock/tests/testasync.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 NEWS.d/2019-09-10-10-59-50.bpo-37251.8zn2o3.rst diff --git a/NEWS.d/2019-09-10-10-59-50.bpo-37251.8zn2o3.rst b/NEWS.d/2019-09-10-10-59-50.bpo-37251.8zn2o3.rst new file mode 100644 index 0000000..27fd1e4 --- /dev/null +++ b/NEWS.d/2019-09-10-10-59-50.bpo-37251.8zn2o3.rst @@ -0,0 +1,3 @@ +Remove `__code__` check in AsyncMock that incorrectly +evaluated function specs as async objects but failed to evaluate classes +with `__await__` but no `__code__` attribute defined as async objects. diff --git a/mock/mock.py b/mock/mock.py index 2d4d89e..8a9fc9d 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -47,10 +47,9 @@ FILTER_DIR = True _safe_super = super def _is_async_obj(obj): - if getattr(obj, '__code__', None): - return asyncio.iscoroutinefunction(obj) or inspect.isawaitable(obj) - else: + if _is_instance_mock(obj) and not isinstance(obj, AsyncMock): return False + return asyncio.iscoroutinefunction(obj) or inspect.isawaitable(obj) def _is_async_func(func): diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 9e0d322..e6f923e 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -31,6 +31,10 @@ class AsyncClass: def normal_method(self): pass +class AwaitableClass: + def __await__(self): + yield + async def async_func(): pass @@ -173,6 +177,10 @@ class AsyncAutospecTest(unittest.TestCase): with self.assertRaises(RuntimeError): create_autospec(async_func, instance=True) + def test_create_autospec_awaitable_class(self): + awaitable_mock = create_autospec(spec=AwaitableClass()) + self.assertIsInstance(create_autospec(awaitable_mock), AsyncMock) + def test_create_autospec(self): spec = create_autospec(async_func_args) awaitable = spec(1, 2, c=3) @@ -334,6 +342,13 @@ class AsyncSpecSetTest(unittest.TestCase): self.assertIsInstance(mock.normal_method, MagicMock) self.assertIsInstance(mock, MagicMock) + def test_magicmock_lambda_spec(self): + mock_obj = MagicMock() + mock_obj.mock_func = MagicMock(spec=lambda x: x) + + with patch.object(mock_obj, "mock_func") as cm: + self.assertIsInstance(cm, MagicMock) + class AsyncArguments(unittest.TestCase): def test_add_return_value(self): -- cgit v1.2.3 From 08a2de7a298ddcdf2b5f3a4a13f18f09b05df168 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 21 Jan 2020 08:19:38 +0000 Subject: backport fixed iscoroutinefunction from Python 3.8 --- mock/backports.py | 39 +++++++++++++++++++++++++++++++++++++++ mock/mock.py | 9 +++++---- 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 mock/backports.py diff --git a/mock/backports.py b/mock/backports.py new file mode 100644 index 0000000..fdca197 --- /dev/null +++ b/mock/backports.py @@ -0,0 +1,39 @@ +import sys + + +if sys.version_info[:2] < (3, 8): + + import functools + from asyncio.coroutines import _is_coroutine + from inspect import ismethod, isfunction, CO_COROUTINE + + def _unwrap_partial(func): + while isinstance(func, functools.partial): + func = func.func + return func + + def _has_code_flag(f, flag): + """Return true if ``f`` is a function (or a method or functools.partial + wrapper wrapping a function) whose code object has the given ``flag`` + set in its flags.""" + while ismethod(f): + f = f.__func__ + f = _unwrap_partial(f) + if not isfunction(f): + return False + return bool(f.__code__.co_flags & flag) + + def iscoroutinefunction(obj): + """Return true if the object is a coroutine function. + + Coroutine functions are defined with "async def" syntax. + """ + return ( + _has_code_flag(obj, CO_COROUTINE) or + getattr(obj, '_is_coroutine', None) is _is_coroutine + ) + +else: + + from asyncio import iscoroutinefunction + diff --git a/mock/mock.py b/mock/mock.py index 8a9fc9d..f8bf32a 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -37,6 +37,7 @@ from unittest.util import safe_repr from functools import wraps, partial from mock import IS_PYPY +from .backports import iscoroutinefunction _builtins = {name for name in dir(builtins) if not name.startswith('_')} @@ -49,12 +50,12 @@ _safe_super = super def _is_async_obj(obj): if _is_instance_mock(obj) and not isinstance(obj, AsyncMock): return False - return asyncio.iscoroutinefunction(obj) or inspect.isawaitable(obj) + return iscoroutinefunction(obj) or inspect.isawaitable(obj) def _is_async_func(func): if getattr(func, '__code__', None): - return asyncio.iscoroutinefunction(func) + return iscoroutinefunction(func) else: return False @@ -2113,7 +2114,7 @@ class AsyncMockMixin(Base): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # asyncio.iscoroutinefunction() checks _is_coroutine property to say if an + # 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). @@ -2284,7 +2285,7 @@ class AsyncMock(AsyncMockMixin, AsyncMagicMixin, Mock): recognized as an async function, and the result of a call is an awaitable: >>> mock = AsyncMock() - >>> asyncio.iscoroutinefunction(mock) + >>> iscoroutinefunction(mock) True >>> inspect.isawaitable(mock()) True -- cgit v1.2.3 From 702d35bd917f37a5105596d36dfa9a476d5f7155 Mon Sep 17 00:00:00 2001 From: Elizabeth Uselton Date: Fri, 13 Sep 2019 08:54:32 -0700 Subject: bpo-37555: Update _CallList.__contains__ to respect ANY (#14700) * Flip equality to use mock calls' __eq__ * bpo-37555: Regression test demonstrating assert_has_calls not working with ANY and spec_set Co-authored-by: Neal Finne * Revert "Flip equality to use mock calls' __eq__" This reverts commit 94ddf54c5a8aab7d00d9ab93e1cc5695c28d73e7. * bpo-37555: Add regression tests for mock ANY ordering issues Add regression tests for whether __eq__ is order agnostic on _Call and _CallList, which is useful for comparisons involving ANY, especially if the ANY comparison is to a class not defaulting __eq__ to NotImplemented. Co-authored-by: Neal Finne * bpo-37555: Fix _CallList and _Call order sensitivity _Call and _CallList depend on ordering to correctly process that an object being compared to ANY with __eq__ should return True. This fix updates the comparison to check both a == b and b == a and return True if either condition is met, fixing situations from the tests in the previous two commits where assertEqual would not be commutative if checking _Call or _CallList objects. This seems like a reasonable fix considering that the Python data model specifies that if an object doesn't know how to compare itself to another object it should return NotImplemented, and that on getting NotImplemented from a == b, it should try b == a, implying that good behavior for __eq__ is commutative. This also flips the order of comparison in _CallList's __contains__ method, guaranteeing ANY will be on the left and have it's __eq__ called for equality checking, fixing the interaction between assert_has_calls and ANY. Co-author: Neal Finne * bpo-37555: Ensure _call_matcher returns _Call object * Adding ACK and news entry * bpo-37555: Replacing __eq__ with == to sidestep NotImplemented bool(NotImplemented) returns True, so it's necessary to use == instead of __eq__ in this comparison. * bpo-37555: cleaning up changes unnecessary to the final product * bpo-37555: Fixed call on bound arguments to respect args and kwargs * Revert "bpo-37555: Add regression tests for mock ANY ordering issues" This reverts commit 49c5310ad493c4356dd3bc58c03653cd9466c4fa. * Revert "bpo-37555: cleaning up changes unnecessary to the final product" This reverts commit 18e964ba0126d8964d89842cb95534b63c2d326e. * Revert "bpo-37555: Replacing __eq__ with == to sidestep NotImplemented" This reverts commit f295eaca5bceac6636c0e2b10e6c7d9a8ee8296a. * Revert "bpo-37555: Fix _CallList and _Call order sensitivity" This reverts commit 874fb697b8376fcea130116e56189061f944fde6. * Updated NEWS.d * bpo-37555: Add tests checking every function using _call_matcher both with and without spec * bpo-37555: Ensure all assert methods using _call_matcher are actually passing calls * Remove AnyCompare and use call objects everywhere. * Revert "Remove AnyCompare and use call objects everywhere." This reverts commit 24973c0b32ce7d796a7f4eeaf259832222aae0f5. * Check for exception in assert_any_await Backports: d6a9d17d8b6c68073931dd8ffa213b4ac351a4ab Signed-off-by: Chris Withers --- NEWS.d/2019-07-19-20-13-48.bpo-37555.S5am28.rst | 2 ++ mock/mock.py | 39 ++++++++++++++++++------- mock/tests/testasync.py | 30 ++++++++++++++++++- mock/tests/testhelpers.py | 21 +++++++++++++ 4 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 NEWS.d/2019-07-19-20-13-48.bpo-37555.S5am28.rst diff --git a/NEWS.d/2019-07-19-20-13-48.bpo-37555.S5am28.rst b/NEWS.d/2019-07-19-20-13-48.bpo-37555.S5am28.rst new file mode 100644 index 0000000..16d1d62 --- /dev/null +++ b/NEWS.d/2019-07-19-20-13-48.bpo-37555.S5am28.rst @@ -0,0 +1,2 @@ +Fix `NonCallableMock._call_matcher` returning tuple instead of `_Call` object +when `self._spec_signature` exists. Patch by Elizabeth Uselton \ No newline at end of file diff --git a/mock/mock.py b/mock/mock.py index f8bf32a..03b2ce5 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -854,7 +854,8 @@ class NonCallableMock(Base): else: name, args, kwargs = _call try: - return name, sig.bind(*args, **kwargs) + bound_call = sig.bind(*args, **kwargs) + return call(name, bound_call.args, bound_call.kwargs) except TypeError as e: return e.with_traceback(None) else: @@ -907,9 +908,9 @@ class NonCallableMock(Base): def _error_message(): msg = self._format_mock_failure_message(args, kwargs) return msg - expected = self._call_matcher((args, kwargs)) + expected = self._call_matcher(_Call((args, kwargs), two=True)) actual = self._call_matcher(self.call_args) - if expected != actual: + if actual != expected: cause = expected if isinstance(expected, Exception) else None raise AssertionError(_error_message()) from cause @@ -970,10 +971,10 @@ class NonCallableMock(Base): The assert passes if the mock has *ever* been called, unlike `assert_called_with` and `assert_called_once_with` that only pass if the call is the most recent one.""" - expected = self._call_matcher((args, kwargs)) + expected = self._call_matcher(_Call((args, kwargs), two=True)) + cause = expected if isinstance(expected, Exception) else None actual = [self._call_matcher(c) for c in self.call_args_list] - if expected not in actual: - cause = expected if isinstance(expected, Exception) else None + if cause or expected not in _AnyComparer(actual): expected_string = self._format_mock_call_signature(args, kwargs) raise AssertionError( '%s call not found' % expected_string @@ -1026,6 +1027,22 @@ class NonCallableMock(Base): return f"\n{prefix}: {safe_repr(self.mock_calls)}." +class _AnyComparer(list): + """A list which checks if it contains a call which may have an + argument of ANY, flipping the components of item and self from + their traditional locations so that ANY is guaranteed to be on + the left.""" + def __contains__(self, item): + for _call in self: + if len(item) != len(_call): + continue + if all([ + expected == actual + for expected, actual in zip(item, _call) + ]): + return True + return False + def _try_iter(obj): if obj is None: @@ -2187,9 +2204,9 @@ class AsyncMockMixin(Base): msg = self._format_mock_failure_message(args, kwargs, action='await') return msg - expected = self._call_matcher((args, kwargs)) + expected = self._call_matcher(_Call((args, kwargs), two=True)) actual = self._call_matcher(self.await_args) - if expected != actual: + if actual != expected: cause = expected if isinstance(expected, Exception) else None raise AssertionError(_error_message()) from cause @@ -2210,10 +2227,10 @@ class AsyncMockMixin(Base): Assert the mock has ever been awaited with the specified arguments. """ self = _mock_self - expected = self._call_matcher((args, kwargs)) + expected = self._call_matcher(_Call((args, kwargs), two=True)) + cause = expected if isinstance(expected, Exception) else None 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 + if cause or expected not in _AnyComparer(actual): expected_string = self._format_mock_call_signature(args, kwargs) raise AssertionError( '%s await not found' % expected_string diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index e6f923e..71979e2 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -3,7 +3,7 @@ import asyncio import inspect import unittest -from mock import call, AsyncMock, patch, MagicMock, create_autospec +from mock import ANY, call, AsyncMock, patch, MagicMock, create_autospec from mock.mock import _AwaitEvent @@ -205,6 +205,10 @@ class AsyncAutospecTest(unittest.TestCase): spec.assert_awaited_with(1, 2, c=3) spec.assert_awaited() + with self.assertRaises(AssertionError): + spec.assert_any_await(e=1) + + def test_patch_with_autospec(self): async def test_async(): @@ -620,6 +624,30 @@ class AsyncMockAssert(unittest.TestCase): run(self._runnable_test('SomethingElse')) self.mock.assert_has_awaits(calls) + def test_awaits_asserts_with_any(self): + class Foo: + def __eq__(self, other): pass + + run(self._runnable_test(Foo(), 1)) + + self.mock.assert_has_awaits([call(ANY, 1)]) + self.mock.assert_awaited_with(ANY, 1) + self.mock.assert_any_await(ANY, 1) + + def test_awaits_asserts_with_spec_and_any(self): + class Foo: + def __eq__(self, other): pass + + mock_with_spec = AsyncMock(spec=Foo) + + async def _custom_mock_runnable_test(*args): + await mock_with_spec(*args) + + run(_custom_mock_runnable_test(Foo(), 1)) + mock_with_spec.assert_has_awaits([call(ANY, 1)]) + mock_with_spec.assert_awaited_with(ANY, 1) + mock_with_spec.assert_any_await(ANY, 1) + def test_assert_has_awaits_ordered(self): calls = [call('NormalFoo'), call('baz')] with self.assertRaises(AssertionError): diff --git a/mock/tests/testhelpers.py b/mock/tests/testhelpers.py index 8b4d709..ae4c847 100644 --- a/mock/tests/testhelpers.py +++ b/mock/tests/testhelpers.py @@ -68,7 +68,28 @@ class AnyTest(unittest.TestCase): self.assertEqual(expected, mock.mock_calls) self.assertEqual(mock.mock_calls, expected) + def test_any_no_spec(self): + # This is a regression test for bpo-37555 + class Foo: + def __eq__(self, other): pass + + mock = Mock() + mock(Foo(), 1) + mock.assert_has_calls([call(ANY, 1)]) + mock.assert_called_with(ANY, 1) + mock.assert_any_call(ANY, 1) + + def test_any_and_spec_set(self): + # This is a regression test for bpo-37555 + class Foo: + def __eq__(self, other): pass + + mock = Mock(spec=Foo) + mock(Foo(), 1) + mock.assert_has_calls([call(ANY, 1)]) + mock.assert_called_with(ANY, 1) + mock.assert_any_call(ANY, 1) class CallTest(unittest.TestCase): -- cgit v1.2.3 From 21ea393db3c288d6d1450a90c1b07d11d0c0f02f Mon Sep 17 00:00:00 2001 From: Michael Foord Date: Fri, 13 Sep 2019 18:40:56 +0200 Subject: bpo-38122: minor fixes to AsyncMock spec handling (GH-16099) Backports: 14fd925a18fe3db0922a7d798e373102fe7a8a9c Signed-off-by: Chris Withers --- mock/mock.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/mock/mock.py b/mock/mock.py index 03b2ce5..671d924 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -404,18 +404,12 @@ class NonCallableMock(Base): # so we can create magic methods on the # class without stomping on other mocks bases = (cls,) - if not issubclass(cls, AsyncMock): + if not issubclass(cls, AsyncMockMixin): # Check if spec is an async object or function - sig = inspect.signature(NonCallableMock.__init__) - bound_args = sig.bind_partial(cls, *args, **kw).arguments - spec_arg = [ - arg for arg in bound_args.keys() - if arg.startswith('spec') - ] - if spec_arg: - # what if spec_set is different than spec? - if _is_async_obj(bound_args[spec_arg[0]]): - bases = (AsyncMockMixin, cls,) + bound_args = _MOCK_SIG.bind_partial(cls, *args, **kw).arguments + spec_arg = bound_args.get('spec_set', bound_args.get('spec')) + if spec_arg and _is_async_obj(spec_arg): + bases = (AsyncMockMixin, cls) new = type(cls.__name__, bases, {'__doc__': cls.__doc__}) instance = object.__new__(new) return instance @@ -1027,6 +1021,9 @@ class NonCallableMock(Base): return f"\n{prefix}: {safe_repr(self.mock_calls)}." +_MOCK_SIG = inspect.signature(NonCallableMock.__init__) + + class _AnyComparer(list): """A list which checks if it contains a call which may have an argument of ANY, flipping the components of item and self from -- cgit v1.2.3 From 14aaccb5db4b977fe6968807f51a7b6286def5b3 Mon Sep 17 00:00:00 2001 From: marcoramirezmx <55331462+marcoramirezmx@users.noreply.github.com> Date: Mon, 16 Sep 2019 11:34:46 -0500 Subject: bpo-38100: Fix spelling error in unittest.mock code (GH-16168) Backports: a9187c31185fe7ea47271839898416400cc3d976 Signed-off-by: Chris Withers --- mock/mock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mock/mock.py b/mock/mock.py index 671d924..e012871 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -2000,9 +2000,9 @@ def _set_return_value(mock, method, name): method.return_value = fixed return - return_calulator = _calculate_return_value.get(name) - if return_calulator is not None: - return_value = return_calulator(mock) + return_calculator = _calculate_return_value.get(name) + if return_calculator is not None: + return_value = return_calculator(mock) method.return_value = return_value return -- cgit v1.2.3 From 3b149c9b1e8be34c12710ed88f5e73a6469b0215 Mon Sep 17 00:00:00 2001 From: Abraham Toriz Cruz Date: Tue, 17 Sep 2019 06:16:08 -0500 Subject: bpo-37828: Fix default mock_name in unittest.mock.assert_called error (GH-16166) In the format string for assert_called the evaluation order is incorrect and hence for mock's without name, 'None' is printed whereas it should be 'mock' like for other messages. The error message is ("Expected '%s' to have been called." % self._mock_name or 'mock'). Backports: 5f5f11faf9de0d8dcbe1a8a4eb35d2a4232d6eaa Signed-off-by: Chris Withers --- NEWS.d/2019-09-15-21-31-18.bpo-37828.gLLDX7.rst | 2 ++ lastsync.txt | 2 +- mock/mock.py | 2 +- mock/tests/testmock.py | 8 ++++++++ 4 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 NEWS.d/2019-09-15-21-31-18.bpo-37828.gLLDX7.rst diff --git a/NEWS.d/2019-09-15-21-31-18.bpo-37828.gLLDX7.rst b/NEWS.d/2019-09-15-21-31-18.bpo-37828.gLLDX7.rst new file mode 100644 index 0000000..c364009 --- /dev/null +++ b/NEWS.d/2019-09-15-21-31-18.bpo-37828.gLLDX7.rst @@ -0,0 +1,2 @@ +Fix default mock name in :meth:`unittest.mock.Mock.assert_called` exceptions. +Patch by Abraham Toriz Cruz. diff --git a/lastsync.txt b/lastsync.txt index 4b3f4da..b7a4c8d 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -39d87b54715197ca9dcb6902bb43461c0ed701a2 +a9187c31185fe7ea47271839898416400cc3d976 diff --git a/mock/mock.py b/mock/mock.py index e012871..7971897 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -872,7 +872,7 @@ class NonCallableMock(Base): self = _mock_self if self.call_count == 0: msg = ("Expected '%s' to have been called." % - self._mock_name or 'mock') + (self._mock_name or 'mock')) raise AssertionError(msg) def assert_called_once(_mock_self): diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 3947cd6..8ab87b0 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -396,6 +396,14 @@ class MockTest(unittest.TestCase): _check(mock) + def test_assert_called_exception_message(self): + msg = "Expected '{0}' to have been called" + with self.assertRaisesRegex(AssertionError, msg.format('mock')): + Mock().assert_called() + with self.assertRaisesRegex(AssertionError, msg.format('test_name')): + Mock(name="test_name").assert_called() + + def test_assert_called_once_with(self): mock = Mock() mock() -- cgit v1.2.3 From 136175ad1590c5a11629346931759868213e1416 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Thu, 19 Sep 2019 21:04:18 -0700 Subject: bpo-38093: Correctly returns AsyncMock for async subclasses. (GH-15947) Backports: 8b03f943c37e07fb2394acdcfacd066647f9b1fd Signed-off-by: Chris Withers --- NEWS.d/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst | 2 + mock/mock.py | 18 ++- mock/tests/testasync.py | 163 +++++++++++++++++------- mock/tests/testmagicmethods.py | 35 ++++- 4 files changed, 163 insertions(+), 55 deletions(-) create mode 100644 NEWS.d/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst diff --git a/NEWS.d/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst b/NEWS.d/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst new file mode 100644 index 0000000..24a5301 --- /dev/null +++ b/NEWS.d/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst @@ -0,0 +1,2 @@ +Fixes AsyncMock so it doesn't crash when used with AsyncContextManagers +or AsyncIterators. diff --git a/mock/mock.py b/mock/mock.py index 7971897..85746a7 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -990,9 +990,13 @@ class NonCallableMock(Base): _type = type(self) if issubclass(_type, MagicMock) and _new_name in _async_method_magics: klass = AsyncMock - if issubclass(_type, AsyncMockMixin): + elif _new_name in _sync_async_magics: + # Special case these ones b/c users will assume they are async, + # but they are actually sync (ie. __aiter__) klass = MagicMock - if not issubclass(_type, CallableMixin): + elif issubclass(_type, AsyncMockMixin): + klass = AsyncMock + elif not issubclass(_type, CallableMixin): if issubclass(_type, NonCallableMagicMock): klass = MagicMock elif issubclass(_type, NonCallableMock) : @@ -1893,7 +1897,7 @@ _non_defaults = { '__reduce__', '__reduce_ex__', '__getinitargs__', '__getnewargs__', '__getstate__', '__setstate__', '__getformat__', '__setformat__', '__repr__', '__dir__', '__subclasses__', '__format__', - '__getnewargs_ex__', '__aenter__', '__aexit__', '__anext__', '__aiter__', + '__getnewargs_ex__', } @@ -1912,10 +1916,12 @@ _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__"} +# Magic methods that are only used with async calls but are synchronous functions themselves +_sync_async_magics = {"__aiter__"} +_async_magics = _async_method_magics | _sync_async_magics -_all_magics = _magics | _non_defaults +_all_sync_magics = _magics | _non_defaults +_all_magics = _all_sync_magics | _async_magics _unsupported_magics = { '__getattr__', '__setattr__', diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 71979e2..87b4878 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -395,36 +395,89 @@ class AsyncArguments(unittest.TestCase): class AsyncContextManagerTest(unittest.TestCase): class WithAsyncContextManager: - async def __aenter__(self, *args, **kwargs): return self async def __aexit__(self, *args, **kwargs): pass - def test_magic_methods_are_async_mocks(self): - mock = MagicMock(self.WithAsyncContextManager()) - self.assertIsInstance(mock.__aenter__, AsyncMock) - self.assertIsInstance(mock.__aexit__, AsyncMock) + class WithSyncContextManager: + def __enter__(self, *args, **kwargs): + return self + + def __exit__(self, *args, **kwargs): + pass + + class ProductionCode: + # Example real-world(ish) code + def __init__(self): + self.session = None + + async def main(self): + async with self.session.post('https://python.org') as response: + val = await response.json() + return val + + def test_async_magic_methods_are_async_mocks_with_magicmock(self): + cm_mock = MagicMock(self.WithAsyncContextManager()) + self.assertIsInstance(cm_mock.__aenter__, AsyncMock) + self.assertIsInstance(cm_mock.__aexit__, AsyncMock) + + def test_magicmock_has_async_magic_methods(self): + cm = MagicMock(name='magic_cm') + self.assertTrue(hasattr(cm, "__aenter__")) + self.assertTrue(hasattr(cm, "__aexit__")) + + def test_magic_methods_are_async_functions(self): + cm = MagicMock(name='magic_cm') + self.assertIsInstance(cm.__aenter__, AsyncMock) + self.assertIsInstance(cm.__aexit__, AsyncMock) + # AsyncMocks are also coroutine functions + self.assertTrue(asyncio.iscoroutinefunction(cm.__aenter__)) + self.assertTrue(asyncio.iscoroutinefunction(cm.__aexit__)) + + def test_set_return_value_of_aenter(self): + def inner_test(mock_type): + pc = self.ProductionCode() + pc.session = MagicMock(name='sessionmock') + cm = mock_type(name='magic_cm') + response = AsyncMock(name='response') + response.json = AsyncMock(return_value={'json': 123}) + cm.__aenter__.return_value = response + pc.session.post.return_value = cm + result = run(pc.main()) + self.assertEqual(result, {'json': 123}) + + for mock_type in [AsyncMock, MagicMock]: + with self.subTest(f"test set return value of aenter with {mock_type}"): + inner_test(mock_type) 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 + def inner_test(mock_type): + called = False + cm = self.WithAsyncContextManager() + cm_mock = mock_type(cm) - result = run(use_context_manager()) - self.assertTrue(called) - self.assertTrue(mock_instance.__aenter__.called) - self.assertTrue(mock_instance.__aexit__.called) - self.assertIsNot(mock_instance, result) - self.assertIsInstance(result, AsyncMock) + async def use_context_manager(): + nonlocal called + async with cm_mock as result: + called = True + return result + cm_result = run(use_context_manager()) + self.assertTrue(called) + self.assertTrue(cm_mock.__aenter__.called) + self.assertTrue(cm_mock.__aexit__.called) + cm_mock.__aenter__.assert_awaited() + cm_mock.__aexit__.assert_awaited() + # We mock __aenter__ so it does not return self + self.assertIsNot(cm_mock, cm_result) + + for mock_type in [AsyncMock, MagicMock]: + with self.subTest(f"test context manager magics with {mock_type}"): + inner_test(mock_type) + def test_mock_customize_async_context_manager(self): instance = self.WithAsyncContextManager() mock_instance = MagicMock(instance) @@ -491,27 +544,30 @@ class AsyncIteratorTest(unittest.TestCase): 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 = run(iterator) - - mock_iterator = mock_instance.__aiter__() - if asyncio.iscoroutine(mock_iterator): - mock_iterator = run(mock_iterator) + def test_aiter_set_return_value(self): + mock_iter = AsyncMock(name="tester") + mock_iter.__aiter__.return_value = [1, 2, 3] + async def main(): + return [i async for i in mock_iter] + result = run(main()) + self.assertEqual(result, [1, 2, 3]) + + def test_mock_aiter_and_anext_asyncmock(self): + def inner_test(mock_type): + instance = self.WithAsyncIterator() + mock_instance = mock_type(instance) + # Check that the mock and the real thing bahave the same + # __aiter__ is not actually async, so not a coroutinefunction + self.assertFalse(asyncio.iscoroutinefunction(instance.__aiter__)) + self.assertFalse(asyncio.iscoroutinefunction(mock_instance.__aiter__)) + # __anext__ is async + self.assertTrue(asyncio.iscoroutinefunction(instance.__anext__)) + self.assertTrue(asyncio.iscoroutinefunction(mock_instance.__anext__)) + + for mock_type in [AsyncMock, MagicMock]: + with self.subTest(f"test aiter and anext corourtine with {mock_type}"): + inner_test(mock_type) - 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): @@ -522,19 +578,30 @@ class AsyncIteratorTest(unittest.TestCase): return accumulator expected = ["FOO", "BAR", "BAZ"] - with self.subTest("iterate through default value"): - mock_instance = MagicMock(self.WithAsyncIterator()) - self.assertEqual([], run(iterate(mock_instance))) + def test_default(mock_type): + mock_instance = mock_type(self.WithAsyncIterator()) + self.assertEqual(run(iterate(mock_instance)), []) + - with self.subTest("iterate through set return_value"): - mock_instance = MagicMock(self.WithAsyncIterator()) + def test_set_return_value(mock_type): + mock_instance = mock_type(self.WithAsyncIterator()) mock_instance.__aiter__.return_value = expected[:] - self.assertEqual(expected, run(iterate(mock_instance))) + self.assertEqual(run(iterate(mock_instance)), expected) - with self.subTest("iterate through set return_value iterator"): - mock_instance = MagicMock(self.WithAsyncIterator()) + def test_set_return_value_iter(mock_type): + mock_instance = mock_type(self.WithAsyncIterator()) mock_instance.__aiter__.return_value = iter(expected[:]) - self.assertEqual(expected, run(iterate(mock_instance))) + self.assertEqual(run(iterate(mock_instance)), expected) + + for mock_type in [AsyncMock, MagicMock]: + with self.subTest(f"default value with {mock_type}"): + test_default(mock_type) + + with self.subTest(f"set return_value with {mock_type}"): + test_set_return_value(mock_type) + + with self.subTest(f"set return_value iterator with {mock_type}"): + test_set_return_value_iter(mock_type) class AsyncMockAssert(unittest.TestCase): diff --git a/mock/tests/testmagicmethods.py b/mock/tests/testmagicmethods.py index fdb6f19..d256dd3 100644 --- a/mock/tests/testmagicmethods.py +++ b/mock/tests/testmagicmethods.py @@ -2,7 +2,8 @@ import math import unittest import os import sys -from mock import Mock, MagicMock +from mock import AsyncMock, Mock, MagicMock +from mock.backports import iscoroutinefunction from mock.mock import _magics @@ -271,6 +272,34 @@ class TestMockingMagicMethods(unittest.TestCase): self.assertEqual(mock != mock, False) + # This should be fixed with issue38163 + @unittest.expectedFailure + def test_asyncmock_defaults(self): + mock = AsyncMock() + self.assertEqual(int(mock), 1) + self.assertEqual(complex(mock), 1j) + self.assertEqual(float(mock), 1.0) + self.assertNotIn(object(), mock) + self.assertEqual(len(mock), 0) + self.assertEqual(list(mock), []) + self.assertEqual(hash(mock), object.__hash__(mock)) + self.assertEqual(str(mock), object.__str__(mock)) + self.assertTrue(bool(mock)) + self.assertEqual(round(mock), mock.__round__()) + self.assertEqual(math.trunc(mock), mock.__trunc__()) + self.assertEqual(math.floor(mock), mock.__floor__()) + self.assertEqual(math.ceil(mock), mock.__ceil__()) + self.assertTrue(iscoroutinefunction(mock.__aexit__)) + self.assertTrue(iscoroutinefunction(mock.__aenter__)) + self.assertIsInstance(mock.__aenter__, AsyncMock) + self.assertIsInstance(mock.__aexit__, AsyncMock) + + # in Python 3 oct and hex use __index__ + # so these tests are for __index__ in py3k + self.assertEqual(oct(mock), '0o1') + self.assertEqual(hex(mock), '0x1') + # how to test __sizeof__ ? + def test_magicmock_defaults(self): mock = MagicMock() self.assertEqual(int(mock), 1) @@ -286,6 +315,10 @@ class TestMockingMagicMethods(unittest.TestCase): self.assertEqual(math.trunc(mock), mock.__trunc__()) self.assertEqual(math.floor(mock), mock.__floor__()) self.assertEqual(math.ceil(mock), mock.__ceil__()) + self.assertTrue(iscoroutinefunction(mock.__aexit__)) + self.assertTrue(iscoroutinefunction(mock.__aenter__)) + self.assertIsInstance(mock.__aenter__, AsyncMock) + self.assertIsInstance(mock.__aexit__, AsyncMock) # in Python 3 oct and hex use __index__ # so these tests are for __index__ in py3k -- cgit v1.2.3 From d66e9d73d9d0894182aec074a6fb3c674788ff47 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Mon, 23 Sep 2019 20:49:40 -0700 Subject: bpo-38136: Updates await_count and call_count to be different things (GH-16192) Backports: ef048517755db1f0d211fb6dfc655a8b412cc96f Signed-off-by: Chris Withers --- NEWS.d/2019-09-16-09-54-42.bpo-38136.MdI-Zb.rst | 3 + mock/mock.py | 11 ++ mock/tests/testasync.py | 199 +++++++++++++++++++++--- mock/tests/testmock.py | 3 +- 4 files changed, 197 insertions(+), 19 deletions(-) create mode 100644 NEWS.d/2019-09-16-09-54-42.bpo-38136.MdI-Zb.rst diff --git a/NEWS.d/2019-09-16-09-54-42.bpo-38136.MdI-Zb.rst b/NEWS.d/2019-09-16-09-54-42.bpo-38136.MdI-Zb.rst new file mode 100644 index 0000000..78cad24 --- /dev/null +++ b/NEWS.d/2019-09-16-09-54-42.bpo-38136.MdI-Zb.rst @@ -0,0 +1,3 @@ +Changes AsyncMock call count and await count to be two different counters. +Now await count only counts when a coroutine has been awaited, not when it +has been called, and vice-versa. Update the documentation around this. diff --git a/mock/mock.py b/mock/mock.py index 85746a7..9cfc705 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -1083,15 +1083,21 @@ class CallableMixin(Base): # can't use self in-case a function / method we are mocking uses self # in the signature _mock_self._mock_check_sig(*args, **kwargs) + _mock_self._increment_mock_call(*args, **kwargs) return _mock_self._mock_call(*args, **kwargs) def _mock_call(_mock_self, *args, **kwargs): + return _mock_self._execute_mock_call(*args, **kwargs) + + def _increment_mock_call(_mock_self, *args, **kwargs): self = _mock_self self.called = True self.call_count += 1 # handle call_args + # needs to be set here so assertions on call arguments pass before + # execution in the case of awaited calls _call = _Call((args, kwargs), two=True) self.call_args = _call self.call_args_list.append(_call) @@ -1131,6 +1137,11 @@ class CallableMixin(Base): # follow the parental chain: _new_parent = _new_parent._mock_new_parent + def _execute_mock_call(_mock_self, *args, **kwargs): + self = _mock_self + # seperate from _increment_mock_call so that awaited functions are + # executed seperately from their call + effect = self.side_effect if effect is not None: if _is_exception(effect): diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 87b4878..9aea931 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -3,8 +3,9 @@ import asyncio import inspect import unittest -from mock import ANY, call, AsyncMock, patch, MagicMock, create_autospec -from mock.mock import _AwaitEvent +from mock import (ANY, call, AsyncMock, patch, MagicMock, + create_autospec, sentinel) +from mock.mock import _AwaitEvent, _CallList try: @@ -608,11 +609,173 @@ class AsyncMockAssert(unittest.TestCase): def setUp(self): self.mock = AsyncMock() - async def _runnable_test(self, *args): - if not args: - await self.mock() - else: - await self.mock(*args) + async def _runnable_test(self, *args, **kwargs): + await self.mock(*args, **kwargs) + + async def _await_coroutine(self, coroutine): + return await coroutine + + def test_assert_called_but_not_awaited(self): + mock = AsyncMock(AsyncClass) + with self.assertWarns(RuntimeWarning): + # Will raise a warning because never awaited + mock.async_method() + self.assertTrue(asyncio.iscoroutinefunction(mock.async_method)) + mock.async_method.assert_called() + mock.async_method.assert_called_once() + mock.async_method.assert_called_once_with() + with self.assertRaises(AssertionError): + mock.assert_awaited() + with self.assertRaises(AssertionError): + mock.async_method.assert_awaited() + + def test_assert_called_then_awaited(self): + mock = AsyncMock(AsyncClass) + mock_coroutine = mock.async_method() + mock.async_method.assert_called() + mock.async_method.assert_called_once() + mock.async_method.assert_called_once_with() + with self.assertRaises(AssertionError): + mock.async_method.assert_awaited() + + run(self._await_coroutine(mock_coroutine)) + # Assert we haven't re-called the function + mock.async_method.assert_called_once() + mock.async_method.assert_awaited() + mock.async_method.assert_awaited_once() + mock.async_method.assert_awaited_once_with() + + def test_assert_called_and_awaited_at_same_time(self): + with self.assertRaises(AssertionError): + self.mock.assert_awaited() + + with self.assertRaises(AssertionError): + self.mock.assert_called() + + run(self._runnable_test()) + self.mock.assert_called_once() + self.mock.assert_awaited_once() + + def test_assert_called_twice_and_awaited_once(self): + mock = AsyncMock(AsyncClass) + coroutine = mock.async_method() + with self.assertWarns(RuntimeWarning): + # The first call will be awaited so no warning there + # But this call will never get awaited, so it will warn here + mock.async_method() + with self.assertRaises(AssertionError): + mock.async_method.assert_awaited() + mock.async_method.assert_called() + run(self._await_coroutine(coroutine)) + mock.async_method.assert_awaited() + mock.async_method.assert_awaited_once() + + def test_assert_called_once_and_awaited_twice(self): + mock = AsyncMock(AsyncClass) + coroutine = mock.async_method() + mock.async_method.assert_called_once() + run(self._await_coroutine(coroutine)) + with self.assertRaises(RuntimeError): + # Cannot reuse already awaited coroutine + run(self._await_coroutine(coroutine)) + mock.async_method.assert_awaited() + + def test_assert_awaited_but_not_called(self): + with self.assertRaises(AssertionError): + self.mock.assert_awaited() + with self.assertRaises(AssertionError): + self.mock.assert_called() + with self.assertRaises(TypeError): + # You cannot await an AsyncMock, it must be a coroutine + run(self._await_coroutine(self.mock)) + + with self.assertRaises(AssertionError): + self.mock.assert_awaited() + with self.assertRaises(AssertionError): + self.mock.assert_called() + + def test_assert_has_calls_not_awaits(self): + kalls = [call('foo')] + with self.assertWarns(RuntimeWarning): + # Will raise a warning because never awaited + self.mock('foo') + self.mock.assert_has_calls(kalls) + with self.assertRaises(AssertionError): + self.mock.assert_has_awaits(kalls) + + def test_assert_has_mock_calls_on_async_mock_no_spec(self): + with self.assertWarns(RuntimeWarning): + # Will raise a warning because never awaited + self.mock() + kalls_empty = [('', (), {})] + self.assertEqual(self.mock.mock_calls, kalls_empty) + + with self.assertWarns(RuntimeWarning): + # Will raise a warning because never awaited + self.mock('foo') + self.mock('baz') + mock_kalls = ([call(), call('foo'), call('baz')]) + self.assertEqual(self.mock.mock_calls, mock_kalls) + + def test_assert_has_mock_calls_on_async_mock_with_spec(self): + a_class_mock = AsyncMock(AsyncClass) + with self.assertWarns(RuntimeWarning): + # Will raise a warning because never awaited + a_class_mock.async_method() + kalls_empty = [('', (), {})] + self.assertEqual(a_class_mock.async_method.mock_calls, kalls_empty) + self.assertEqual(a_class_mock.mock_calls, [call.async_method()]) + + with self.assertWarns(RuntimeWarning): + # Will raise a warning because never awaited + a_class_mock.async_method(1, 2, 3, a=4, b=5) + method_kalls = [call(), call(1, 2, 3, a=4, b=5)] + mock_kalls = [call.async_method(), call.async_method(1, 2, 3, a=4, b=5)] + self.assertEqual(a_class_mock.async_method.mock_calls, method_kalls) + self.assertEqual(a_class_mock.mock_calls, mock_kalls) + + def test_async_method_calls_recorded(self): + with self.assertWarns(RuntimeWarning): + # Will raise warnings because never awaited + self.mock.something(3, fish=None) + self.mock.something_else.something(6, cake=sentinel.Cake) + + self.assertEqual(self.mock.method_calls, [ + ("something", (3,), {'fish': None}), + ("something_else.something", (6,), {'cake': sentinel.Cake}) + ], + "method calls not recorded correctly") + self.assertEqual(self.mock.something_else.method_calls, + [("something", (6,), {'cake': sentinel.Cake})], + "method calls not recorded correctly") + + def test_async_arg_lists(self): + def assert_attrs(mock): + names = ('call_args_list', 'method_calls', 'mock_calls') + for name in names: + attr = getattr(mock, name) + self.assertIsInstance(attr, _CallList) + self.assertIsInstance(attr, list) + self.assertEqual(attr, []) + + assert_attrs(self.mock) + with self.assertWarns(RuntimeWarning): + # Will raise warnings because never awaited + self.mock() + self.mock(1, 2) + self.mock(a=3) + + self.mock.reset_mock() + assert_attrs(self.mock) + + a_mock = AsyncMock(AsyncClass) + with self.assertWarns(RuntimeWarning): + # Will raise warnings because never awaited + a_mock.async_method() + a_mock.async_method(1, a=3) + + a_mock.reset_mock() + assert_attrs(a_mock) def test_assert_awaited(self): with self.assertRaises(AssertionError): @@ -658,20 +821,20 @@ class AsyncMockAssert(unittest.TestCase): def test_assert_any_wait(self): with self.assertRaises(AssertionError): - self.mock.assert_any_await('NormalFoo') + self.mock.assert_any_await('foo') - run(self._runnable_test('foo')) + run(self._runnable_test('baz')) with self.assertRaises(AssertionError): - self.mock.assert_any_await('NormalFoo') + self.mock.assert_any_await('foo') - run(self._runnable_test('NormalFoo')) - self.mock.assert_any_await('NormalFoo') + run(self._runnable_test('foo')) + self.mock.assert_any_await('foo') run(self._runnable_test('SomethingElse')) - self.mock.assert_any_await('NormalFoo') + self.mock.assert_any_await('foo') def test_assert_has_awaits_no_order(self): - calls = [call('NormalFoo'), call('baz')] + calls = [call('foo'), call('baz')] with self.assertRaises(AssertionError) as cm: self.mock.assert_has_awaits(calls) @@ -681,7 +844,7 @@ class AsyncMockAssert(unittest.TestCase): with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls) - run(self._runnable_test('NormalFoo')) + run(self._runnable_test('foo')) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls) @@ -716,7 +879,7 @@ class AsyncMockAssert(unittest.TestCase): mock_with_spec.assert_any_await(ANY, 1) def test_assert_has_awaits_ordered(self): - calls = [call('NormalFoo'), call('baz')] + calls = [call('foo'), call('baz')] with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls, any_order=True) @@ -724,11 +887,11 @@ class AsyncMockAssert(unittest.TestCase): with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls, any_order=True) - run(self._runnable_test('foo')) + run(self._runnable_test('bamf')) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(calls, any_order=True) - run(self._runnable_test('NormalFoo')) + run(self._runnable_test('foo')) self.mock.assert_has_awaits(calls, any_order=True) run(self._runnable_test('qux')) diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 8ab87b0..67a8047 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -850,6 +850,7 @@ class MockTest(unittest.TestCase): def test_setting_call(self): mock = Mock() def __call__(self, a): + self._increment_mock_call(a) return self._mock_call(a) type(mock).__call__ = __call__ @@ -2025,7 +2026,7 @@ class MockTest(unittest.TestCase): ) mocks = [ - Mock, MagicMock, NonCallableMock, NonCallableMagicMock + Mock, MagicMock, NonCallableMock, NonCallableMagicMock, AsyncMock ] for mock in mocks: -- cgit v1.2.3 From e702e61a24abe7dfff374875358669fdbfaa2aa2 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 22 Jan 2020 07:25:16 +0000 Subject: turn warnings for async stuff into errors. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 647943c..d70c78e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,4 +44,5 @@ universal = 1 [tool:pytest] python_files=test*.py filterwarnings = + error::RuntimeWarning ignore::DeprecationWarning -- cgit v1.2.3 From a0e820d377a4724f5d1478a76a1b6ffbb6551e0c Mon Sep 17 00:00:00 2001 From: Samuel Freilich Date: Tue, 24 Sep 2019 15:08:31 -0400 Subject: bpo-36871: Handle spec errors in assert_has_calls (GH-16005) The fix in PR 13261 handled the underlying issue about the spec for specific methods not being applied correctly, but it didn't fix the issue that was causing the misleading error message. The code currently grabs a list of responses from _call_matcher (which may include exceptions). But it doesn't reach inside the list when checking if the result is an exception. This results in a misleading error message when one of the provided calls does not match the spec. https://bugs.python.org/issue36871 Automerge-Triggered-By: @gpshead Backports: b5a7a4f0c20717a4c92c371583b5521b83f40f32 Signed-off-by: Chris Withers --- NEWS.d/2019-09-24-18-45-46.bpo-36871.p47knk.rst | 3 +++ mock/mock.py | 26 ++++++++++++++++++++----- mock/tests/testasync.py | 21 ++++++++++++++++++++ mock/tests/testmock.py | 19 ++++++++++++++++++ 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 NEWS.d/2019-09-24-18-45-46.bpo-36871.p47knk.rst diff --git a/NEWS.d/2019-09-24-18-45-46.bpo-36871.p47knk.rst b/NEWS.d/2019-09-24-18-45-46.bpo-36871.p47knk.rst new file mode 100644 index 0000000..6b7b19a --- /dev/null +++ b/NEWS.d/2019-09-24-18-45-46.bpo-36871.p47knk.rst @@ -0,0 +1,3 @@ +Improve error handling for the assert_has_calls and assert_has_awaits methods of +mocks. Fixed a bug where any errors encountered while binding the expected calls +to the mock's spec were silently swallowed, leading to misleading error output. diff --git a/mock/mock.py b/mock/mock.py index 9cfc705..5337fe8 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -933,13 +933,21 @@ class NonCallableMock(Base): If `any_order` is True then the calls can be in any order, but they must all appear in `mock_calls`.""" expected = [self._call_matcher(c) for c in calls] - cause = expected if isinstance(expected, Exception) else None + cause = next((e for e in expected if isinstance(e, Exception)), None) all_calls = _CallList(self._call_matcher(c) for c in self.mock_calls) if not any_order: if expected not in all_calls: + if cause is None: + problem = 'Calls not found.' + else: + problem = ('Error processing expected calls.\n' + 'Errors: {}').format( + [e if isinstance(e, Exception) else None + for e in expected]) raise AssertionError( - 'Calls not found.\nExpected: %r%s' - % (_CallList(calls), self._calls_repr(prefix="Actual")) + f'{problem}\n' + f'Expected: {_CallList(calls)}\n' + f'Actual: {self._calls_repr(prefix="Actual")}' ) from cause return @@ -2264,12 +2272,20 @@ class AsyncMockMixin(Base): """ self = _mock_self expected = [self._call_matcher(c) for c in calls] - cause = expected if isinstance(expected, Exception) else None + cause = cause = next((e for e in expected if isinstance(e, Exception)), 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: + if cause is None: + problem = 'Awaits not found.' + else: + problem = ('Error processing expected awaits.\n' + 'Errors: {}').format( + [e if isinstance(e, Exception) else None + for e in expected]) raise AssertionError( - f'Awaits not found.\nExpected: {_CallList(calls)}\n' + f'{problem}\n' + f'Expected: {_CallList(calls)}\n' f'Actual: {self.await_args_list}' ) from cause return diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 9aea931..41d1b01 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -1,6 +1,7 @@ import asyncio import inspect +import re import unittest from mock import (ANY, call, AsyncMock, patch, MagicMock, @@ -903,3 +904,23 @@ class AsyncMockAssert(unittest.TestCase): run(self._runnable_test()) with self.assertRaises(AssertionError): self.mock.assert_not_awaited() + + def test_assert_has_awaits_not_matching_spec_error(self): + async def f(): pass + + mock = AsyncMock(spec=f) + + with self.assertRaisesRegex( + AssertionError, + re.escape('Awaits not found.\nExpected:')) as cm: + mock.assert_has_awaits([call()]) + self.assertIsNone(cm.exception.__cause__) + + with self.assertRaisesRegex( + AssertionError, + re.escape('Error processing expected awaits.\n' + "Errors: [None, TypeError('too many positional " + "arguments')]\n" + 'Expected:')) as cm: + mock.assert_has_awaits([call(), call('wrong')]) + self.assertIsInstance(cm.exception.__cause__, TypeError) diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 67a8047..880cc1e 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -1435,6 +1435,25 @@ class MockTest(unittest.TestCase): mock.assert_has_calls(calls[:-1]) mock.assert_has_calls(calls[:-1], any_order=True) + def test_assert_has_calls_not_matching_spec_error(self): + def f(): pass + + mock = Mock(spec=f) + + with self.assertRaisesRegex( + AssertionError, + re.escape('Calls not found.\nExpected:')) as cm: + mock.assert_has_calls([call()]) + self.assertIsNone(cm.exception.__cause__) + + with self.assertRaisesRegex( + AssertionError, + re.escape('Error processing expected calls.\n' + "Errors: [None, TypeError('too many positional " + "arguments')]\n" + 'Expected:')) as cm: + mock.assert_has_calls([call(), call('wrong')]) + self.assertIsInstance(cm.exception.__cause__, TypeError) def test_assert_any_call(self): mock = Mock() -- cgit v1.2.3 From 3bdebf260ea3b7143e4946863fafe63dbe8cb5ee Mon Sep 17 00:00:00 2001 From: Samuel Freilich Date: Tue, 24 Sep 2019 18:04:29 -0400 Subject: bpo-36871: Avoid duplicated 'Actual:' in assertion message (GH-16361) Fixes an issue caught after merge of PR 16005. Tightened test assertions to check the entire assertion message. Backports: 2180f6b058effbf49ec819f7cedbe76ddd4b700c Signed-off-by: Chris Withers --- mock/mock.py | 4 ++-- mock/tests/testasync.py | 25 ++++++++++++++++--------- mock/tests/testmock.py | 21 ++++++++++++++------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/mock/mock.py b/mock/mock.py index 5337fe8..f862ff9 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -946,8 +946,8 @@ class NonCallableMock(Base): for e in expected]) raise AssertionError( f'{problem}\n' - f'Expected: {_CallList(calls)}\n' - f'Actual: {self._calls_repr(prefix="Actual")}' + f'Expected: {_CallList(calls)}' + f'{self._calls_repr(prefix="Actual").rstrip(".")}' ) from cause return diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 41d1b01..981b801 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -906,21 +906,28 @@ class AsyncMockAssert(unittest.TestCase): self.mock.assert_not_awaited() def test_assert_has_awaits_not_matching_spec_error(self): - async def f(): pass + async def f(x=None): pass - mock = AsyncMock(spec=f) + self.mock = AsyncMock(spec=f) + asyncio.run(self._runnable_test(1)) with self.assertRaisesRegex( AssertionError, - re.escape('Awaits not found.\nExpected:')) as cm: - mock.assert_has_awaits([call()]) + '^{}$'.format( + re.escape('Awaits not found.\n' + 'Expected: [call()]\n' + 'Actual: [call(1)]'))) as cm: + self.mock.assert_has_awaits([call()]) self.assertIsNone(cm.exception.__cause__) with self.assertRaisesRegex( AssertionError, - re.escape('Error processing expected awaits.\n' - "Errors: [None, TypeError('too many positional " - "arguments')]\n" - 'Expected:')) as cm: - mock.assert_has_awaits([call(), call('wrong')]) + '^{}$'.format( + re.escape( + 'Error processing expected awaits.\n' + "Errors: [None, TypeError('too many positional " + "arguments')]\n" + 'Expected: [call(), call(1, 2)]\n' + 'Actual: [call(1)]'))) as cm: + self.mock.assert_has_awaits([call(), call(1, 2)]) self.assertIsInstance(cm.exception.__cause__, TypeError) diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 880cc1e..461670c 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -1436,23 +1436,30 @@ class MockTest(unittest.TestCase): mock.assert_has_calls(calls[:-1], any_order=True) def test_assert_has_calls_not_matching_spec_error(self): - def f(): pass + def f(x=None): pass mock = Mock(spec=f) + mock(1) with self.assertRaisesRegex( AssertionError, - re.escape('Calls not found.\nExpected:')) as cm: + '^{}$'.format( + re.escape('Calls not found.\n' + 'Expected: [call()]\n' + 'Actual: [call(1)]'))) as cm: mock.assert_has_calls([call()]) self.assertIsNone(cm.exception.__cause__) + with self.assertRaisesRegex( AssertionError, - re.escape('Error processing expected calls.\n' - "Errors: [None, TypeError('too many positional " - "arguments')]\n" - 'Expected:')) as cm: - mock.assert_has_calls([call(), call('wrong')]) + '^{}$'.format( + re.escape( + 'Error processing expected calls.\n' + "Errors: [None, TypeError('too many positional arguments')]\n" + "Expected: [call(), call(1, 2)]\n" + 'Actual: [call(1)]'))) as cm: + mock.assert_has_calls([call(), call(1, 2)]) self.assertIsInstance(cm.exception.__cause__, TypeError) def test_assert_any_call(self): -- cgit v1.2.3 From ee8bbd4effef511f527e27d1bf27978650b4ac9e Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 22 Jan 2020 18:52:28 +0000 Subject: support for py3.6 --- mock/tests/testasync.py | 4 +++- mock/tests/testmock.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 981b801..315322c 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -928,6 +928,8 @@ class AsyncMockAssert(unittest.TestCase): "Errors: [None, TypeError('too many positional " "arguments')]\n" 'Expected: [call(), call(1, 2)]\n' - 'Actual: [call(1)]'))) as cm: + 'Actual: [call(1)]').replace( + "arguments\\'", "arguments\\',?") + )) as cm: self.mock.assert_has_awaits([call(), call(1, 2)]) self.assertIsInstance(cm.exception.__cause__, TypeError) diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 461670c..c97d7fc 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -1458,7 +1458,9 @@ class MockTest(unittest.TestCase): 'Error processing expected calls.\n' "Errors: [None, TypeError('too many positional arguments')]\n" "Expected: [call(), call(1, 2)]\n" - 'Actual: [call(1)]'))) as cm: + 'Actual: [call(1)]').replace( + "arguments\\'", "arguments\\',?" + ))) as cm: mock.assert_has_calls([call(), call(1, 2)]) self.assertIsInstance(cm.exception.__cause__, TypeError) -- cgit v1.2.3 From be7df8183b74c4c83da3321941a964b279976e8f Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Sat, 28 Sep 2019 18:42:44 -0700 Subject: bpo-38108: Makes mock objects inherit from Base (GH-16060) Backports: 9a7d9519506ae807ca48ff02e2ea117ebac3450e Signed-off-by: Chris Withers --- NEWS.d/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst | 2 + mock/mock.py | 54 +++++++++-------------- mock/tests/testasync.py | 57 ++++++++++++++++--------- mock/tests/testmagicmethods.py | 3 -- 4 files changed, 59 insertions(+), 57 deletions(-) create mode 100644 NEWS.d/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst diff --git a/NEWS.d/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst b/NEWS.d/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst new file mode 100644 index 0000000..d7eea36 --- /dev/null +++ b/NEWS.d/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst @@ -0,0 +1,2 @@ +Any synchronous magic methods on an AsyncMock now return a MagicMock. Any +asynchronous magic methods on a MagicMock now return an AsyncMock. diff --git a/mock/mock.py b/mock/mock.py index f862ff9..44ac03c 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -411,7 +411,7 @@ class NonCallableMock(Base): if spec_arg and _is_async_obj(spec_arg): bases = (AsyncMockMixin, cls) new = type(cls.__name__, bases, {'__doc__': cls.__doc__}) - instance = object.__new__(new) + instance = _safe_super(NonCallableMock, cls).__new__(new) return instance @@ -997,17 +997,18 @@ class NonCallableMock(Base): _type = type(self) if issubclass(_type, MagicMock) and _new_name in _async_method_magics: + # Any asynchronous magic becomes an AsyncMock klass = AsyncMock - elif _new_name in _sync_async_magics: - # Special case these ones b/c users will assume they are async, - # but they are actually sync (ie. __aiter__) - klass = MagicMock elif issubclass(_type, AsyncMockMixin): - klass = AsyncMock + if _new_name in _all_sync_magics: + # Any synchronous magic becomes a MagicMock + klass = MagicMock + else: + klass = AsyncMock elif 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] @@ -1895,6 +1896,7 @@ magic_methods = ( "round trunc floor ceil " "bool next " "fspath " + "aiter " ) if IS_PYPY: @@ -2037,7 +2039,7 @@ def _set_return_value(mock, method, name): -class MagicMixin(object): +class MagicMixin(Base): def __init__(self, *args, **kw): self._mock_set_magics() # make magic work for kwargs in init _safe_super(MagicMixin, self).__init__(*args, **kw) @@ -2045,13 +2047,14 @@ class MagicMixin(object): def _mock_set_magics(self): - these_magics = _magics + orig_magics = _magics | _async_method_magics + these_magics = orig_magics if getattr(self, "_mock_methods", None) is not None: - these_magics = _magics.intersection(self._mock_methods) + these_magics = orig_magics.intersection(self._mock_methods) remove_magics = set() - remove_magics = _magics - these_magics + remove_magics = orig_magics - these_magics for entry in remove_magics: if entry in type(self).__dict__: @@ -2079,33 +2082,14 @@ class NonCallableMagicMock(MagicMixin, NonCallableMock): self._mock_set_magics() -class AsyncMagicMixin: +class AsyncMagicMixin(MagicMixin): def __init__(self, *args, **kw): - self._mock_set_async_magics() # make magic work for kwargs in init + self._mock_set_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)) + self._mock_set_magics() # fix magic broken by upper level init -class MagicMock(MagicMixin, AsyncMagicMixin, Mock): +class MagicMock(MagicMixin, Mock): """ MagicMock is a subclass of Mock with default implementations of most of the magic methods. You can use MagicMock without having to @@ -2127,7 +2111,7 @@ class MagicMock(MagicMixin, AsyncMagicMixin, Mock): -class MagicProxy(object): +class MagicProxy(Base): def __init__(self, name, parent): self.name = name self.parent = parent diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 315322c..ebf5cb2 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -393,6 +393,43 @@ class AsyncArguments(unittest.TestCase): RuntimeError('coroutine raised StopIteration') ) +class AsyncMagicMethods(unittest.TestCase): + def test_async_magic_methods_return_async_mocks(self): + m_mock = MagicMock() + self.assertIsInstance(m_mock.__aenter__, AsyncMock) + self.assertIsInstance(m_mock.__aexit__, AsyncMock) + self.assertIsInstance(m_mock.__anext__, AsyncMock) + # __aiter__ is actually a synchronous object + # so should return a MagicMock + self.assertIsInstance(m_mock.__aiter__, MagicMock) + + def test_sync_magic_methods_return_magic_mocks(self): + a_mock = AsyncMock() + self.assertIsInstance(a_mock.__enter__, MagicMock) + self.assertIsInstance(a_mock.__exit__, MagicMock) + self.assertIsInstance(a_mock.__next__, MagicMock) + self.assertIsInstance(a_mock.__len__, MagicMock) + + def test_magicmock_has_async_magic_methods(self): + m_mock = MagicMock() + self.assertTrue(hasattr(m_mock, "__aenter__")) + self.assertTrue(hasattr(m_mock, "__aexit__")) + self.assertTrue(hasattr(m_mock, "__anext__")) + + def test_asyncmock_has_sync_magic_methods(self): + a_mock = AsyncMock() + self.assertTrue(hasattr(a_mock, "__enter__")) + self.assertTrue(hasattr(a_mock, "__exit__")) + self.assertTrue(hasattr(a_mock, "__next__")) + self.assertTrue(hasattr(a_mock, "__len__")) + + def test_magic_methods_are_async_functions(self): + m_mock = MagicMock() + self.assertIsInstance(m_mock.__aenter__, AsyncMock) + self.assertIsInstance(m_mock.__aexit__, AsyncMock) + # AsyncMocks are also coroutine functions + self.assertTrue(asyncio.iscoroutinefunction(m_mock.__aenter__)) + self.assertTrue(asyncio.iscoroutinefunction(m_mock.__aexit__)) class AsyncContextManagerTest(unittest.TestCase): @@ -420,24 +457,6 @@ class AsyncContextManagerTest(unittest.TestCase): val = await response.json() return val - def test_async_magic_methods_are_async_mocks_with_magicmock(self): - cm_mock = MagicMock(self.WithAsyncContextManager()) - self.assertIsInstance(cm_mock.__aenter__, AsyncMock) - self.assertIsInstance(cm_mock.__aexit__, AsyncMock) - - def test_magicmock_has_async_magic_methods(self): - cm = MagicMock(name='magic_cm') - self.assertTrue(hasattr(cm, "__aenter__")) - self.assertTrue(hasattr(cm, "__aexit__")) - - def test_magic_methods_are_async_functions(self): - cm = MagicMock(name='magic_cm') - self.assertIsInstance(cm.__aenter__, AsyncMock) - self.assertIsInstance(cm.__aexit__, AsyncMock) - # AsyncMocks are also coroutine functions - self.assertTrue(asyncio.iscoroutinefunction(cm.__aenter__)) - self.assertTrue(asyncio.iscoroutinefunction(cm.__aexit__)) - def test_set_return_value_of_aenter(self): def inner_test(mock_type): pc = self.ProductionCode() @@ -909,7 +928,7 @@ class AsyncMockAssert(unittest.TestCase): async def f(x=None): pass self.mock = AsyncMock(spec=f) - asyncio.run(self._runnable_test(1)) + run(self._runnable_test(1)) with self.assertRaisesRegex( AssertionError, diff --git a/mock/tests/testmagicmethods.py b/mock/tests/testmagicmethods.py index d256dd3..afd4dbe 100644 --- a/mock/tests/testmagicmethods.py +++ b/mock/tests/testmagicmethods.py @@ -271,9 +271,6 @@ class TestMockingMagicMethods(unittest.TestCase): self.assertEqual(mock == mock, True) self.assertEqual(mock != mock, False) - - # This should be fixed with issue38163 - @unittest.expectedFailure def test_asyncmock_defaults(self): mock = AsyncMock() self.assertEqual(int(mock), 1) -- cgit v1.2.3 From 189dffbe8106c7afb5e65211a6132235f62c23ca Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Sun, 29 Sep 2019 21:01:28 -0700 Subject: bpo-38161: Removes _AwaitEvent from AsyncMock. (GH-16443) Backports: 25e115ec00b5f75e3589c9f21013c47c21e1753f Signed-off-by: Chris Withers --- NEWS.d/2019-09-27-16-31-28.bpo-38161.zehai1.rst | 1 + mock/mock.py | 36 ------------------------- mock/tests/testasync.py | 4 +-- 3 files changed, 2 insertions(+), 39 deletions(-) create mode 100644 NEWS.d/2019-09-27-16-31-28.bpo-38161.zehai1.rst diff --git a/NEWS.d/2019-09-27-16-31-28.bpo-38161.zehai1.rst b/NEWS.d/2019-09-27-16-31-28.bpo-38161.zehai1.rst new file mode 100644 index 0000000..0077033 --- /dev/null +++ b/NEWS.d/2019-09-27-16-31-28.bpo-38161.zehai1.rst @@ -0,0 +1 @@ +Removes _AwaitEvent from AsyncMock. diff --git a/mock/mock.py b/mock/mock.py index 44ac03c..909e646 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -248,7 +248,6 @@ def _setup_async_mock(mock): mock.await_count = 0 mock.await_args = None mock.await_args_list = _CallList() - mock.awaited = _AwaitEvent(mock) # Mock is not configured yet so the attributes are set # to a function and then the corresponding mock helper function @@ -2130,7 +2129,6 @@ class MagicProxy(Base): class AsyncMockMixin(Base): - awaited = _delegating_property('awaited') await_count = _delegating_property('await_count') await_args = _delegating_property('await_args') await_args_list = _delegating_property('await_args_list') @@ -2144,7 +2142,6 @@ class AsyncMockMixin(Base): # It is set through __dict__ because when spec_set is True, this # attribute is likely undefined. 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() @@ -2174,7 +2171,6 @@ class AsyncMockMixin(Base): self.await_count += 1 self.await_args = _call self.await_args_list.append(_call) - await self.awaited._notify() return await proxy() @@ -2906,35 +2902,3 @@ class _AsyncIterator: 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/mock/tests/testasync.py b/mock/tests/testasync.py index ebf5cb2..273b406 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -6,7 +6,7 @@ import unittest from mock import (ANY, call, AsyncMock, patch, MagicMock, create_autospec, sentinel) -from mock.mock import _AwaitEvent, _CallList +from mock.mock import _CallList try: @@ -192,7 +192,6 @@ class AsyncAutospecTest(unittest.TestCase): self.assertEqual(spec.await_count, 0) self.assertIsNone(spec.await_args) self.assertEqual(spec.await_args_list, []) - self.assertIsInstance(spec.awaited, _AwaitEvent) spec.assert_not_awaited() run(main()) @@ -226,7 +225,6 @@ class AsyncAutospecTest(unittest.TestCase): self.assertEqual(mock_method.await_count, 0) self.assertEqual(mock_method.await_args_list, []) self.assertIsNone(mock_method.await_args) - self.assertIsInstance(mock_method.awaited, _AwaitEvent) mock_method.assert_not_awaited() await awaitable -- cgit v1.2.3 From ec2bfa7997580e981232a400401c12998db67c44 Mon Sep 17 00:00:00 2001 From: Lisa Roach Date: Sun, 29 Sep 2019 21:56:47 -0700 Subject: bpo-38163: Child mocks detect their type as sync or async (GH-16471) Backports: 3667e1ee6c90e6d3b6a745cd590ece87118f81ad Signed-off-by: Chris Withers --- NEWS.d/2019-09-28-20-16-40.bpo-38163.x51-vK.rst | 4 ++ mock/mock.py | 5 +- mock/tests/testasync.py | 67 ++++++++++++++++--------- 3 files changed, 49 insertions(+), 27 deletions(-) create mode 100644 NEWS.d/2019-09-28-20-16-40.bpo-38163.x51-vK.rst diff --git a/NEWS.d/2019-09-28-20-16-40.bpo-38163.x51-vK.rst b/NEWS.d/2019-09-28-20-16-40.bpo-38163.x51-vK.rst new file mode 100644 index 0000000..5f7db26 --- /dev/null +++ b/NEWS.d/2019-09-28-20-16-40.bpo-38163.x51-vK.rst @@ -0,0 +1,4 @@ +Child mocks will now detect their type as either synchronous or +asynchronous, asynchronous child mocks will be AsyncMocks and synchronous +child mocks will be either MagicMock or Mock (depending on their parent +type). diff --git a/mock/mock.py b/mock/mock.py index 909e646..fb96c83 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -999,8 +999,9 @@ class NonCallableMock(Base): # Any asynchronous magic becomes an AsyncMock klass = AsyncMock elif issubclass(_type, AsyncMockMixin): - if _new_name in _all_sync_magics: - # Any synchronous magic becomes a MagicMock + if (_new_name in _all_sync_magics or + self._mock_methods and _new_name in self._mock_methods): + # Any synchronous method on AsyncMock becomes a MagicMock klass = MagicMock else: klass = AsyncMock diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 273b406..a2d5bb2 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -4,7 +4,7 @@ import inspect import re import unittest -from mock import (ANY, call, AsyncMock, patch, MagicMock, +from mock import (ANY, call, AsyncMock, patch, MagicMock, Mock, create_autospec, sentinel) from mock.mock import _CallList @@ -246,33 +246,50 @@ class AsyncAutospecTest(unittest.TestCase): class AsyncSpecTest(unittest.TestCase): - def test_spec_as_async_positional_magicmock(self): - mock = MagicMock(async_func) - self.assertIsInstance(mock, MagicMock) - m = mock() - self.assertTrue(inspect.isawaitable(m)) - run(m) + def test_spec_normal_methods_on_class(self): + def inner_test(mock_type): + mock = mock_type(AsyncClass) + self.assertIsInstance(mock.async_method, AsyncMock) + self.assertIsInstance(mock.normal_method, MagicMock) - def test_spec_as_async_kw_magicmock(self): - mock = MagicMock(spec=async_func) - self.assertIsInstance(mock, MagicMock) - m = mock() - self.assertTrue(inspect.isawaitable(m)) - run(m) + for mock_type in [AsyncMock, MagicMock]: + with self.subTest(f"test method types with {mock_type}"): + inner_test(mock_type) - def test_spec_as_async_kw_AsyncMock(self): - mock = AsyncMock(spec=async_func) - self.assertIsInstance(mock, AsyncMock) - m = mock() - self.assertTrue(inspect.isawaitable(m)) - run(m) + def test_spec_normal_methods_on_class_with_mock(self): + mock = Mock(AsyncClass) + self.assertIsInstance(mock.async_method, AsyncMock) + self.assertIsInstance(mock.normal_method, Mock) - def test_spec_as_async_positional_AsyncMock(self): - mock = AsyncMock(async_func) - self.assertIsInstance(mock, AsyncMock) - m = mock() - self.assertTrue(inspect.isawaitable(m)) - run(m) + def test_spec_mock_type_kw(self): + def inner_test(mock_type): + async_mock = mock_type(spec=async_func) + self.assertIsInstance(async_mock, mock_type) + with self.assertWarns(RuntimeWarning): + # Will raise a warning because never awaited + self.assertTrue(inspect.isawaitable(async_mock())) + + sync_mock = mock_type(spec=normal_func) + self.assertIsInstance(sync_mock, mock_type) + + for mock_type in [AsyncMock, MagicMock, Mock]: + with self.subTest(f"test spec kwarg with {mock_type}"): + inner_test(mock_type) + + def test_spec_mock_type_positional(self): + def inner_test(mock_type): + async_mock = mock_type(async_func) + self.assertIsInstance(async_mock, mock_type) + with self.assertWarns(RuntimeWarning): + # Will raise a warning because never awaited + self.assertTrue(inspect.isawaitable(async_mock())) + + sync_mock = mock_type(normal_func) + self.assertIsInstance(sync_mock, mock_type) + + for mock_type in [AsyncMock, MagicMock, Mock]: + with self.subTest(f"test spec positional with {mock_type}"): + inner_test(mock_type) def test_spec_as_normal_kw_AsyncMock(self): mock = AsyncMock(spec=normal_func) -- cgit v1.2.3 From 0fda551a97c2a99f6dd3e426b4ceedcde4d38d1b Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 19 Nov 2019 19:45:20 +0000 Subject: bpo-38839: Fix some unused functions in tests (GH-17189) Backports: 892221bfa04a41cf581f988ba19dc263f557e157 Signed-off-by: Chris Withers --- NEWS.d/2019-11-18-22-10-55.bpo-38839.di6tXv.rst | 1 + mock/tests/testasync.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 NEWS.d/2019-11-18-22-10-55.bpo-38839.di6tXv.rst diff --git a/NEWS.d/2019-11-18-22-10-55.bpo-38839.di6tXv.rst b/NEWS.d/2019-11-18-22-10-55.bpo-38839.di6tXv.rst new file mode 100644 index 0000000..80c5a5b --- /dev/null +++ b/NEWS.d/2019-11-18-22-10-55.bpo-38839.di6tXv.rst @@ -0,0 +1 @@ +Fix some unused functions in tests. Patch by Adam Johnson. diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index a2d5bb2..2eda1e5 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -349,6 +349,7 @@ class AsyncSpecSetTest(unittest.TestCase): @patch.object(AsyncClass, 'async_method', spec_set=True) def test_async(async_method): self.assertIsInstance(async_method, AsyncMock) + test_async() def test_is_async_AsyncMock(self): mock = AsyncMock(spec_set=AsyncClass.async_method) -- cgit v1.2.3 From 30cc1881168041d1ddede3ecd15b69407a564e7f Mon Sep 17 00:00:00 2001 From: Jason Fried Date: Wed, 20 Nov 2019 16:27:51 -0800 Subject: bpo-38857: AsyncMock fix for awaitable values and StopIteration fix [3.8] (GH-17269) Backports: 046442d02bcc6e848e71e93e47f6cde9e279e993 Signed-off-by: Chris Withers --- NEWS.d/2019-11-19-16-28-25.bpo-38857.YPUkU9.rst | 4 ++ NEWS.d/2019-11-19-16-30-46.bpo-38859.AZUzL8.rst | 3 + mock/mock.py | 62 +++++++++++--------- mock/tests/testasync.py | 75 +++++++++++++++++++------ 4 files changed, 102 insertions(+), 42 deletions(-) create mode 100644 NEWS.d/2019-11-19-16-28-25.bpo-38857.YPUkU9.rst create mode 100644 NEWS.d/2019-11-19-16-30-46.bpo-38859.AZUzL8.rst diff --git a/NEWS.d/2019-11-19-16-28-25.bpo-38857.YPUkU9.rst b/NEWS.d/2019-11-19-16-28-25.bpo-38857.YPUkU9.rst new file mode 100644 index 0000000..f28df28 --- /dev/null +++ b/NEWS.d/2019-11-19-16-28-25.bpo-38857.YPUkU9.rst @@ -0,0 +1,4 @@ +AsyncMock fix for return values that are awaitable types. This also covers +side_effect iterable values that happend to be awaitable, and wraps +callables that return an awaitable type. Before these awaitables were being +awaited instead of being returned as is. diff --git a/NEWS.d/2019-11-19-16-30-46.bpo-38859.AZUzL8.rst b/NEWS.d/2019-11-19-16-30-46.bpo-38859.AZUzL8.rst new file mode 100644 index 0000000..c059539 --- /dev/null +++ b/NEWS.d/2019-11-19-16-30-46.bpo-38859.AZUzL8.rst @@ -0,0 +1,3 @@ +AsyncMock now returns StopAsyncIteration on the exaustion of a side_effects +iterable. Since PEP-479 its Impossible to raise a StopIteration exception +from a coroutine. diff --git a/mock/mock.py b/mock/mock.py index fb96c83..807d7b7 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -1148,8 +1148,8 @@ class CallableMixin(Base): def _execute_mock_call(_mock_self, *args, **kwargs): self = _mock_self - # seperate from _increment_mock_call so that awaited functions are - # executed seperately from their call + # separate from _increment_mock_call so that awaited functions are + # executed separately from their call, also AsyncMock overrides this method effect = self.side_effect if effect is not None: @@ -2150,30 +2150,46 @@ class AsyncMockMixin(Base): code_mock.co_flags = inspect.CO_COROUTINE self.__dict__['__code__'] = code_mock - async def _mock_call(_mock_self, *args, **kwargs): + async def _execute_mock_call(_mock_self, *args, **kwargs): self = _mock_self - try: - result = super()._mock_call(*args, **kwargs) - except (BaseException, StopIteration) as e: - side_effect = self.side_effect - if side_effect is not None and not callable(side_effect): - raise - return await _raise(e) + # This is nearly just like super(), except for sepcial handling + # of coroutines _call = self.call_args + self.await_count += 1 + self.await_args = _call + self.await_args_list.append(_call) - 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) + effect = self.side_effect + if effect is not None: + if _is_exception(effect): + raise effect + elif not _callable(effect): + try: + result = next(effect) + except StopIteration: + # It is impossible to propogate a StopIteration + # through coroutines because of PEP 479 + raise StopAsyncIteration + if _is_exception(result): + raise result + elif asyncio.iscoroutinefunction(effect): + result = await effect(*args, **kwargs) + else: + result = effect(*args, **kwargs) - return await proxy() + if result is not DEFAULT: + return result + + if self._mock_return_value is not DEFAULT: + return self.return_value + + if self._mock_wraps is not None: + if asyncio.iscoroutinefunction(self._mock_wraps): + return await self._mock_wraps(*args, **kwargs) + return self._mock_wraps(*args, **kwargs) + + return self.return_value def assert_awaited(_mock_self): """ @@ -2880,10 +2896,6 @@ def seal(mock): seal(m) -async def _raise(exception): - raise exception - - class _AsyncIterator: """ Wraps an iterator in an asynchronous iterator. diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 2eda1e5..e839fca 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -8,7 +8,6 @@ from mock import (ANY, call, AsyncMock, patch, MagicMock, Mock, create_autospec, sentinel) from mock.mock import _CallList - try: from asyncio import run except ImportError: @@ -372,42 +371,84 @@ class AsyncSpecSetTest(unittest.TestCase): self.assertIsInstance(cm, MagicMock) -class AsyncArguments(unittest.TestCase): - def test_add_return_value(self): +class AsyncArguments(unittest.IsolatedAsyncioTestCase): + async def test_add_return_value(self): async def addition(self, var): return var + 1 mock = AsyncMock(addition, return_value=10) - output = run(mock(5)) + output = await mock(5) self.assertEqual(output, 10) - def test_add_side_effect_exception(self): + async def test_add_side_effect_exception(self): async def addition(var): return var + 1 mock = AsyncMock(addition, side_effect=Exception('err')) with self.assertRaises(Exception): - run(mock(5)) + await mock(5) - def test_add_side_effect_function(self): + async def test_add_side_effect_function(self): async def addition(var): return var + 1 mock = AsyncMock(side_effect=addition) - result = run(mock(5)) + result = await mock(5) self.assertEqual(result, 6) - def test_add_side_effect_iterable(self): + async def test_add_side_effect_iterable(self): vals = [1, 2, 3] mock = AsyncMock(side_effect=vals) for item in vals: - self.assertEqual(item, run(mock())) - - with self.assertRaises(RuntimeError) as e: - run(mock()) - self.assertEqual( - e.exception, - RuntimeError('coroutine raised StopIteration') - ) + self.assertEqual(item, await mock()) + + with self.assertRaises(StopAsyncIteration) as e: + await mock() + + async def test_return_value_AsyncMock(self): + value = AsyncMock(return_value=10) + mock = AsyncMock(return_value=value) + result = await mock() + self.assertIs(result, value) + + async def test_return_value_awaitable(self): + fut = asyncio.Future() + fut.set_result(None) + mock = AsyncMock(return_value=fut) + result = await mock() + self.assertIsInstance(result, asyncio.Future) + + async def test_side_effect_awaitable_values(self): + fut = asyncio.Future() + fut.set_result(None) + + mock = AsyncMock(side_effect=[fut]) + result = await mock() + self.assertIsInstance(result, asyncio.Future) + + with self.assertRaises(StopAsyncIteration): + await mock() + + async def test_side_effect_is_AsyncMock(self): + effect = AsyncMock(return_value=10) + mock = AsyncMock(side_effect=effect) + + result = await mock() + self.assertEqual(result, 10) + + async def test_wraps_coroutine(self): + value = asyncio.Future() + + ran = False + async def inner(): + nonlocal ran + ran = True + return value + + mock = AsyncMock(wraps=inner) + result = await mock() + self.assertEqual(result, value) + mock.assert_awaited() + self.assertTrue(ran) class AsyncMagicMethods(unittest.TestCase): def test_async_magic_methods_return_async_mocks(self): -- cgit v1.2.3 From a6768e64120cd6c963035044e9d70fb85f2dabc0 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Fri, 24 Jan 2020 07:29:40 +0000 Subject: minimal backport of IsolatedAsyncioTestCase to get tests passing on Py3.6 --- mock/backports.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++- mock/tests/testasync.py | 4 +++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/mock/backports.py b/mock/backports.py index fdca197..80e8a98 100644 --- a/mock/backports.py +++ b/mock/backports.py @@ -3,9 +3,10 @@ import sys if sys.version_info[:2] < (3, 8): - import functools + import asyncio, functools from asyncio.coroutines import _is_coroutine from inspect import ismethod, isfunction, CO_COROUTINE + from unittest import TestCase def _unwrap_partial(func): while isinstance(func, functools.partial): @@ -33,7 +34,67 @@ if sys.version_info[:2] < (3, 8): getattr(obj, '_is_coroutine', None) is _is_coroutine ) + + class IsolatedAsyncioTestCase(TestCase): + + def __init__(self, methodName='runTest'): + super().__init__(methodName) + self._asyncioTestLoop = None + self._asyncioCallsQueue = None + + async def _asyncioLoopRunner(self, fut): + self._asyncioCallsQueue = queue = asyncio.Queue() + fut.set_result(None) + while True: + query = await queue.get() + queue.task_done() + if query is None: + return + fut, awaitable = query + try: + ret = await awaitable + if not fut.cancelled(): + fut.set_result(ret) + except asyncio.CancelledError: + raise + except Exception as ex: + if not fut.cancelled(): + fut.set_exception(ex) + + def _setupAsyncioLoop(self): + assert self._asyncioTestLoop is None + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.set_debug(True) + self._asyncioTestLoop = loop + fut = loop.create_future() + self._asyncioCallsTask = loop.create_task(self._asyncioLoopRunner(fut)) + loop.run_until_complete(fut) + + def _tearDownAsyncioLoop(self): + assert self._asyncioTestLoop is not None + loop = self._asyncioTestLoop + self._asyncioTestLoop = None + self._asyncioCallsQueue.put_nowait(None) + loop.run_until_complete(self._asyncioCallsQueue.join()) + + try: + # shutdown asyncgens + loop.run_until_complete(loop.shutdown_asyncgens()) + finally: + asyncio.set_event_loop(None) + loop.close() + + def run(self, result=None): + self._setupAsyncioLoop() + try: + return super().run(result) + finally: + self._tearDownAsyncioLoop() + + else: from asyncio import iscoroutinefunction + from unittest import IsolatedAsyncioTestCase diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index e839fca..b0bbe47 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -6,8 +6,10 @@ import unittest from mock import (ANY, call, AsyncMock, patch, MagicMock, Mock, create_autospec, sentinel) +from mock.backports import IsolatedAsyncioTestCase from mock.mock import _CallList + try: from asyncio import run except ImportError: @@ -371,7 +373,7 @@ class AsyncSpecSetTest(unittest.TestCase): self.assertIsInstance(cm, MagicMock) -class AsyncArguments(unittest.IsolatedAsyncioTestCase): +class AsyncArguments(IsolatedAsyncioTestCase): async def test_add_return_value(self): async def addition(self, var): return var + 1 -- cgit v1.2.3 From c000fff5bdc49c620ea6bc9160214858b64f611e Mon Sep 17 00:00:00 2001 From: Elena Oat Date: Sun, 8 Dec 2019 12:14:38 -0800 Subject: bpo-38669: patch.object now raises a helpful error (GH17034) This means a clearer message is now shown when patch.object is called with two string arguments, rather than a class and a string argument. Backports: cd90a52983db34896a6335a572d55bdda274778f Signed-off-by: Chris Withers --- NEWS.d/2019-11-04-02-54-16.bpo-38669.pazXZ8.rst | 1 + mock/mock.py | 4 ++++ mock/tests/testpatch.py | 4 ++++ 3 files changed, 9 insertions(+) create mode 100644 NEWS.d/2019-11-04-02-54-16.bpo-38669.pazXZ8.rst diff --git a/NEWS.d/2019-11-04-02-54-16.bpo-38669.pazXZ8.rst b/NEWS.d/2019-11-04-02-54-16.bpo-38669.pazXZ8.rst new file mode 100644 index 0000000..5060ecf --- /dev/null +++ b/NEWS.d/2019-11-04-02-54-16.bpo-38669.pazXZ8.rst @@ -0,0 +1 @@ +Raise :exc:`TypeError` when passing target as a string with :meth:`unittest.mock.patch.object`. \ No newline at end of file diff --git a/mock/mock.py b/mock/mock.py index 807d7b7..2c68abc 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -1610,6 +1610,10 @@ def _patch_object( When used as a class decorator `patch.object` honours `patch.TEST_PREFIX` for choosing which methods to wrap. """ + if type(target) is str: + raise TypeError( + f"{target!r} must be the actual object to be patched, not a str" + ) getter = lambda: target return _patch( getter, attribute, new, spec, create, diff --git a/mock/tests/testpatch.py b/mock/tests/testpatch.py index ae5cdff..e30f581 100644 --- a/mock/tests/testpatch.py +++ b/mock/tests/testpatch.py @@ -105,6 +105,10 @@ class PatchTest(unittest.TestCase): self.assertEqual(Something.attribute, sentinel.Original, "patch not restored") + def test_patchobject_with_string_as_target(self): + msg = "'Something' must be the actual object to be patched, not a str" + with self.assertRaisesRegex(TypeError, msg): + patch.object('Something', 'do_something') def test_patchobject_with_none(self): class Something(object): -- cgit v1.2.3 From 695580a480ce046136aa652191d9b5c6926015a9 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 13 Jan 2020 19:11:34 +0000 Subject: remove unused __version__ from mock.py (#17977) This isn't included in `__all__` and could be a source of confusion. Backports: 31d6de5aba009914efa8f0f3c3d7da35217578eb Signed-off-by: Chris Withers --- mock/mock.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mock/mock.py b/mock/mock.py index 2c68abc..8ed47e7 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -23,8 +23,6 @@ __all__ = ( ) -__version__ = '1.0' - import asyncio import contextlib import io -- cgit v1.2.3 From 7a35f3a96bd043073f089a840965166923808de5 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Wed, 15 Jan 2020 15:19:49 +0530 Subject: Improve test coverage for AsyncMock. (GH-17906) * Add test for nested async decorator patch. * Add test for side_effect and wraps with a function. * Add test for side_effect with an exception in the iterable. Backports: 54f743eb315f00b0ff45e115dde7a5d506034153 Signed-off-by: Chris Withers --- mock/tests/testasync.py | 53 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index b0bbe47..1b66a04 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -87,9 +87,17 @@ class AsyncPatchDecoratorTest(unittest.TestCase): test_async() def test_async_def_patch(self): - @patch(f"{__name__}.async_func", AsyncMock()) - async def test_async(): + @patch(f"{__name__}.async_func", return_value=1) + @patch(f"{__name__}.async_func_args", return_value=2) + async def test_async(func_args_mock, func_mock): + self.assertEqual(func_args_mock._mock_name, "async_func_args") + self.assertEqual(func_mock._mock_name, "async_func") + self.assertIsInstance(async_func, AsyncMock) + self.assertIsInstance(async_func_args, AsyncMock) + + self.assertEqual(await async_func(), 1) + self.assertEqual(await async_func_args(1, 2, c=3), 2) run(test_async()) self.assertTrue(inspect.iscoroutinefunction(async_func)) @@ -390,22 +398,40 @@ class AsyncArguments(IsolatedAsyncioTestCase): with self.assertRaises(Exception): await mock(5) - async def test_add_side_effect_function(self): + async def test_add_side_effect_coroutine(self): async def addition(var): return var + 1 mock = AsyncMock(side_effect=addition) result = await mock(5) self.assertEqual(result, 6) + async def test_add_side_effect_normal_function(self): + def addition(var): + return var + 1 + mock = AsyncMock(side_effect=addition) + result = await mock(5) + self.assertEqual(result, 6) + async def test_add_side_effect_iterable(self): vals = [1, 2, 3] mock = AsyncMock(side_effect=vals) for item in vals: - self.assertEqual(item, await mock()) + self.assertEqual(await mock(), item) with self.assertRaises(StopAsyncIteration) as e: await mock() + async def test_add_side_effect_exception_iterable(self): + class SampleException(Exception): + pass + + vals = [1, SampleException("foo")] + mock = AsyncMock(side_effect=vals) + self.assertEqual(await mock(), 1) + + with self.assertRaises(SampleException) as e: + await mock() + async def test_return_value_AsyncMock(self): value = AsyncMock(return_value=10) mock = AsyncMock(return_value=value) @@ -452,6 +478,21 @@ class AsyncArguments(IsolatedAsyncioTestCase): mock.assert_awaited() self.assertTrue(ran) + async def test_wraps_normal_function(self): + value = 1 + + ran = False + def inner(): + nonlocal ran + ran = True + return value + + mock = AsyncMock(wraps=inner) + result = await mock() + self.assertEqual(result, value) + mock.assert_awaited() + self.assertTrue(ran) + class AsyncMagicMethods(unittest.TestCase): def test_async_magic_methods_return_async_mocks(self): m_mock = MagicMock() @@ -875,6 +916,10 @@ class AsyncMockAssert(unittest.TestCase): self.mock.assert_awaited_once() def test_assert_awaited_with(self): + msg = 'Not awaited' + with self.assertRaisesRegex(AssertionError, msg): + self.mock.assert_awaited_with('foo') + run(self._runnable_test()) msg = 'expected await not found' with self.assertRaisesRegex(AssertionError, msg): -- cgit v1.2.3 From aa5f6e9e2df8a77b2f81714018043efc392ae676 Mon Sep 17 00:00:00 2001 From: Emmanuel Arias Date: Fri, 24 Jan 2020 05:14:14 -0300 Subject: bpo-24928: Add test case for patch.dict using OrderedDict (GH -11437) * add test for path.dict using OrderedDict Co-authored-by: Yu Tomita nekobon@users.noreply.github.com Backports: 1d0c5e16eab29d55773cc4196bb90d2bf12e09dd Signed-off-by: Chris Withers --- mock/tests/testpatch.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/mock/tests/testpatch.py b/mock/tests/testpatch.py index e30f581..5316b9c 100644 --- a/mock/tests/testpatch.py +++ b/mock/tests/testpatch.py @@ -4,6 +4,7 @@ import os import sys +from collections import OrderedDict import unittest from mock.tests import support @@ -1834,6 +1835,25 @@ class PatchTest(unittest.TestCase): self.assertEqual(foo(), 1) self.assertEqual(foo(), 0) + def test_patch_orderdict(self): + foo = OrderedDict() + foo['a'] = object() + foo['b'] = 'python' + + original = foo.copy() + update_values = list(zip('cdefghijklmnopqrstuvwxyz', range(26))) + patched_values = list(foo.items()) + update_values + + with patch.dict(foo, OrderedDict(update_values)): + self.assertEqual(list(foo.items()), patched_values) + + self.assertEqual(foo, original) + + with patch.dict(foo, update_values): + self.assertEqual(list(foo.items()), patched_values) + + self.assertEqual(foo, original) + def test_dotted_but_module_not_loaded(self): # This exercises the AttributeError branch of _dot_lookup. -- cgit v1.2.3 From 11df596649d109d3d36f3a5062bd32f42493808a Mon Sep 17 00:00:00 2001 From: Mario Corchero Date: Fri, 24 Jan 2020 08:38:33 +0000 Subject: Fix `mock.patch.dict` to be stopped with `mock.patch.stopall` (#17606) As the function was not registering in the active patches, the mocks started by `mock.patch.dict` were not being stopped when `mock.patch.stopall` was being called. Backports: e131c9720d087c0c4988bd2a5c62020feb9d1d77 Signed-off-by: Chris Withers --- NEWS.d/2019-12-14-14-38-40.bpo-21600.kC4Cgh.rst | 2 + mock/mock.py | 19 +++++++++- mock/tests/testpatch.py | 50 +++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 NEWS.d/2019-12-14-14-38-40.bpo-21600.kC4Cgh.rst diff --git a/NEWS.d/2019-12-14-14-38-40.bpo-21600.kC4Cgh.rst b/NEWS.d/2019-12-14-14-38-40.bpo-21600.kC4Cgh.rst new file mode 100644 index 0000000..0f72639 --- /dev/null +++ b/NEWS.d/2019-12-14-14-38-40.bpo-21600.kC4Cgh.rst @@ -0,0 +1,2 @@ +Fix :func:`mock.patch.stopall` to stop active patches that were created with +:func:`mock.patch.dict`. diff --git a/mock/mock.py b/mock/mock.py index 8ed47e7..f924205 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -1860,8 +1860,23 @@ class _patch_dict(object): self._unpatch_dict() return False - start = __enter__ - stop = __exit__ + + def start(self): + """Activate a patch, returning any created mock.""" + result = self.__enter__() + _patch._active_patches.append(self) + return result + + + def stop(self): + """Stop an active patch.""" + try: + _patch._active_patches.remove(self) + except ValueError: + # If the patch hasn't been started this will fail + pass + + return self.__exit__() def _clear_dict(in_dict): diff --git a/mock/tests/testpatch.py b/mock/tests/testpatch.py index 5316b9c..1d3050e 100644 --- a/mock/tests/testpatch.py +++ b/mock/tests/testpatch.py @@ -1808,6 +1808,56 @@ class PatchTest(unittest.TestCase): self.assertEqual(stopped, ["three", "two", "one"]) + def test_patch_dict_stopall(self): + dic1 = {} + dic2 = {1: 'a'} + dic3 = {1: 'A', 2: 'B'} + origdic1 = dic1.copy() + origdic2 = dic2.copy() + origdic3 = dic3.copy() + patch.dict(dic1, {1: 'I', 2: 'II'}).start() + patch.dict(dic2, {2: 'b'}).start() + + @patch.dict(dic3) + def patched(): + del dic3[1] + + patched() + self.assertNotEqual(dic1, origdic1) + self.assertNotEqual(dic2, origdic2) + self.assertEqual(dic3, origdic3) + + patch.stopall() + + self.assertEqual(dic1, origdic1) + self.assertEqual(dic2, origdic2) + self.assertEqual(dic3, origdic3) + + + def test_patch_and_patch_dict_stopall(self): + original_unlink = os.unlink + original_chdir = os.chdir + dic1 = {} + dic2 = {1: 'A', 2: 'B'} + origdic1 = dic1.copy() + origdic2 = dic2.copy() + + patch('os.unlink', something).start() + patch('os.chdir', something_else).start() + patch.dict(dic1, {1: 'I', 2: 'II'}).start() + patch.dict(dic2).start() + del dic2[1] + + self.assertIsNot(os.unlink, original_unlink) + self.assertIsNot(os.chdir, original_chdir) + self.assertNotEqual(dic1, origdic1) + self.assertNotEqual(dic2, origdic2) + patch.stopall() + self.assertIs(os.unlink, original_unlink) + self.assertIs(os.chdir, original_chdir) + self.assertEqual(dic1, origdic1) + self.assertEqual(dic2, origdic2) + def test_special_attrs(self): def foo(x=0): -- cgit v1.2.3 From 5de813ca343c6bc4bc990b6d1a3df1a0407ddaa6 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Fri, 24 Jan 2020 18:44:29 +0530 Subject: bpo-38473: Handle autospecced functions and methods used with attach_mock (GH-16784) Backports: 66b00a9d3aacf6ed49412f48743e4913104a2bb3 Signed-off-by: Chris Withers --- NEWS.d/2019-10-14-21-14-55.bpo-38473.uXpVld.rst | 2 ++ mock/mock.py | 4 ++++ mock/tests/testmock.py | 29 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 NEWS.d/2019-10-14-21-14-55.bpo-38473.uXpVld.rst diff --git a/NEWS.d/2019-10-14-21-14-55.bpo-38473.uXpVld.rst b/NEWS.d/2019-10-14-21-14-55.bpo-38473.uXpVld.rst new file mode 100644 index 0000000..de80e89 --- /dev/null +++ b/NEWS.d/2019-10-14-21-14-55.bpo-38473.uXpVld.rst @@ -0,0 +1,2 @@ +Use signature from inner mock for autospecced methods attached with +:func:`unittest.mock.attach_mock`. Patch by Karthikeyan Singaravelan. diff --git a/mock/mock.py b/mock/mock.py index f924205..e453ec4 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -819,6 +819,10 @@ class NonCallableMock(Base): if child is None or isinstance(child, _SpecState): break else: + # If an autospecced object is attached using attach_mock the + # child would be a function with mock object as attribute from + # which signature has to be derived. + child = _extract_mock(child) children = child._mock_children sig = child._spec_signature diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index c97d7fc..7574002 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -1924,6 +1924,35 @@ class MockTest(unittest.TestCase): self.assertEqual(mock_func.mock._extract_mock_name(), 'mock.child') + def test_attach_mock_patch_autospec_signature(self): + with mock.patch(f'{__name__}.Something.meth', autospec=True) as mocked: + manager = Mock() + manager.attach_mock(mocked, 'attach_meth') + obj = Something() + obj.meth(1, 2, 3, d=4) + manager.assert_has_calls([call.attach_meth(mock.ANY, 1, 2, 3, d=4)]) + obj.meth.assert_has_calls([call(mock.ANY, 1, 2, 3, d=4)]) + mocked.assert_has_calls([call(mock.ANY, 1, 2, 3, d=4)]) + + with mock.patch(f'{__name__}.something', autospec=True) as mocked: + manager = Mock() + manager.attach_mock(mocked, 'attach_func') + something(1) + manager.assert_has_calls([call.attach_func(1)]) + something.assert_has_calls([call(1)]) + mocked.assert_has_calls([call(1)]) + + with mock.patch(f'{__name__}.Something', autospec=True) as mocked: + manager = Mock() + manager.attach_mock(mocked, 'attach_obj') + obj = Something() + obj.meth(1, 2, 3, d=4) + manager.assert_has_calls([call.attach_obj(), + call.attach_obj().meth(1, 2, 3, d=4)]) + obj.meth.assert_has_calls([call(1, 2, 3, d=4)]) + mocked.assert_has_calls([call(), call().meth(1, 2, 3, d=4)]) + + def test_attribute_deletion(self): for mock in (Mock(), MagicMock(), NonCallableMagicMock(), NonCallableMock()): -- cgit v1.2.3 From 610e9e3ae0cac170883c73ddc6a5bb164e9451d4 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Fri, 24 Jan 2020 18:37:55 +0000 Subject: flip more to iscoroutinefunction backport --- mock/mock.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mock/mock.py b/mock/mock.py index e453ec4..236a678 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -488,7 +488,7 @@ class NonCallableMock(Base): _spec_asyncs = [] for attr in dir(spec): - if asyncio.iscoroutinefunction(getattr(spec, attr, None)): + if iscoroutinefunction(getattr(spec, attr, None)): _spec_asyncs.append(attr) if spec is not None and not _is_list(spec): @@ -2194,7 +2194,7 @@ class AsyncMockMixin(Base): raise StopAsyncIteration if _is_exception(result): raise result - elif asyncio.iscoroutinefunction(effect): + elif iscoroutinefunction(effect): result = await effect(*args, **kwargs) else: result = effect(*args, **kwargs) @@ -2206,7 +2206,7 @@ class AsyncMockMixin(Base): return self.return_value if self._mock_wraps is not None: - if asyncio.iscoroutinefunction(self._mock_wraps): + if iscoroutinefunction(self._mock_wraps): return await self._mock_wraps(*args, **kwargs) return self._mock_wraps(*args, **kwargs) @@ -2717,7 +2717,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, skipfirst = _must_skip(spec, entry, is_type) kwargs['_eat_self'] = skipfirst - if asyncio.iscoroutinefunction(original): + if iscoroutinefunction(original): child_klass = AsyncMock else: child_klass = MagicMock -- cgit v1.2.3 From 5a12423241f1168c0609c28e88c1f2e40ba40e38 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Sat, 25 Jan 2020 10:44:19 +0000 Subject: this one no longer fails on pypy --- mock/tests/testmagicmethods.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mock/tests/testmagicmethods.py b/mock/tests/testmagicmethods.py index afd4dbe..e1f1ee0 100644 --- a/mock/tests/testmagicmethods.py +++ b/mock/tests/testmagicmethods.py @@ -1,7 +1,6 @@ import math import unittest import os -import sys from mock import AsyncMock, Mock, MagicMock from mock.backports import iscoroutinefunction from mock.mock import _magics @@ -429,7 +428,6 @@ class TestMockingMagicMethods(unittest.TestCase): self.assertEqual(dir(mock), ['foo']) - @unittest.skipIf('PyPy' in sys.version, "This fails differently on pypy") def test_bound_methods(self): m = Mock() -- cgit v1.2.3 From 7a5fc548cd3c6dac3c601a866bf910420d797a96 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Sat, 25 Jan 2020 10:44:42 +0000 Subject: pypy actually gets this right, so make it clear in the test --- mock/tests/testhelpers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mock/tests/testhelpers.py b/mock/tests/testhelpers.py index ae4c847..3dd95f2 100644 --- a/mock/tests/testhelpers.py +++ b/mock/tests/testhelpers.py @@ -912,8 +912,6 @@ class SpecSignatureTest(unittest.TestCase): check_data_descriptor(foo.desc) - @pytest.mark.skipif(IS_PYPY, - reason="https://bitbucket.org/pypy/pypy/issues/3010") def test_autospec_on_bound_builtin_function(self): meth = types.MethodType(time.ctime, time.time()) self.assertIsInstance(meth(), str) @@ -923,8 +921,13 @@ class SpecSignatureTest(unittest.TestCase): mocked() mocked.assert_called_once_with() mocked.reset_mock() - mocked(4, 5, 6) - mocked.assert_called_once_with(4, 5, 6) + # but pypy gets this right: + if IS_PYPY: + with self.assertRaises(TypeError): + mocked(4, 5, 6) + else: + mocked(4, 5, 6) + mocked.assert_called_once_with(4, 5, 6) def test_autospec_getattr_partial_function(self): -- cgit v1.2.3 From 8d3d8f29fa9aa9fcac375e6481480d57886478ef Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 27 Jan 2020 07:50:25 +0000 Subject: move testasync to backports.iscoroutinefunction to keep inline with upstream imports --- mock/tests/testasync.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 1b66a04..8d35742 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -6,7 +6,7 @@ import unittest from mock import (ANY, call, AsyncMock, patch, MagicMock, Mock, create_autospec, sentinel) -from mock.backports import IsolatedAsyncioTestCase +from mock.backports import IsolatedAsyncioTestCase, iscoroutinefunction from mock.mock import _CallList @@ -60,7 +60,7 @@ class AsyncPatchDecoratorTest(unittest.TestCase): def test_is_coroutine_function_patch(self): @patch.object(AsyncClass, 'async_method') def test_async(mock_method): - self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + self.assertTrue(iscoroutinefunction(mock_method)) test_async() def test_is_async_patch(self): @@ -107,7 +107,7 @@ class AsyncPatchCMTest(unittest.TestCase): def test_is_async_function_cm(self): def test_async(): with patch.object(AsyncClass, 'async_method') as mock_method: - self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + self.assertTrue(iscoroutinefunction(mock_method)) test_async() @@ -139,12 +139,12 @@ class AsyncPatchCMTest(unittest.TestCase): class AsyncMockTest(unittest.TestCase): def test_iscoroutinefunction_default(self): mock = AsyncMock() - self.assertTrue(asyncio.iscoroutinefunction(mock)) + self.assertTrue(iscoroutinefunction(mock)) def test_iscoroutinefunction_function(self): async def foo(): pass mock = AsyncMock(foo) - self.assertTrue(asyncio.iscoroutinefunction(mock)) + self.assertTrue(iscoroutinefunction(mock)) self.assertTrue(inspect.iscoroutinefunction(mock)) def test_isawaitable(self): @@ -157,7 +157,7 @@ class AsyncMockTest(unittest.TestCase): def test_iscoroutinefunction_normal_function(self): def foo(): pass mock = AsyncMock(foo) - self.assertTrue(asyncio.iscoroutinefunction(mock)) + self.assertTrue(iscoroutinefunction(mock)) self.assertTrue(inspect.iscoroutinefunction(mock)) def test_future_isfuture(self): @@ -205,7 +205,7 @@ class AsyncAutospecTest(unittest.TestCase): run(main()) - self.assertTrue(asyncio.iscoroutinefunction(spec)) + self.assertTrue(iscoroutinefunction(spec)) self.assertTrue(asyncio.iscoroutine(awaitable)) self.assertEqual(spec.await_count, 1) self.assertEqual(spec.await_args, call(1, 2, c=3)) @@ -226,7 +226,7 @@ class AsyncAutospecTest(unittest.TestCase): awaitable = mock_method(1, 2, c=3) self.assertIsInstance(mock_method.mock, AsyncMock) - self.assertTrue(asyncio.iscoroutinefunction(mock_method)) + self.assertTrue(iscoroutinefunction(mock_method)) self.assertTrue(asyncio.iscoroutine(awaitable)) self.assertTrue(inspect.isawaitable(awaitable)) @@ -362,13 +362,13 @@ class AsyncSpecSetTest(unittest.TestCase): def test_is_async_AsyncMock(self): mock = AsyncMock(spec_set=AsyncClass.async_method) - self.assertTrue(asyncio.iscoroutinefunction(mock)) + self.assertTrue(iscoroutinefunction(mock)) 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(iscoroutinefunction(mock.async_method)) + self.assertFalse(iscoroutinefunction(mock.normal_method)) self.assertIsInstance(mock.async_method, AsyncMock) self.assertIsInstance(mock.normal_method, MagicMock) self.assertIsInstance(mock, MagicMock) @@ -528,8 +528,8 @@ class AsyncMagicMethods(unittest.TestCase): self.assertIsInstance(m_mock.__aenter__, AsyncMock) self.assertIsInstance(m_mock.__aexit__, AsyncMock) # AsyncMocks are also coroutine functions - self.assertTrue(asyncio.iscoroutinefunction(m_mock.__aenter__)) - self.assertTrue(asyncio.iscoroutinefunction(m_mock.__aexit__)) + self.assertTrue(iscoroutinefunction(m_mock.__aenter__)) + self.assertTrue(iscoroutinefunction(m_mock.__aexit__)) class AsyncContextManagerTest(unittest.TestCase): @@ -679,11 +679,11 @@ class AsyncIteratorTest(unittest.TestCase): mock_instance = mock_type(instance) # Check that the mock and the real thing bahave the same # __aiter__ is not actually async, so not a coroutinefunction - self.assertFalse(asyncio.iscoroutinefunction(instance.__aiter__)) - self.assertFalse(asyncio.iscoroutinefunction(mock_instance.__aiter__)) + self.assertFalse(iscoroutinefunction(instance.__aiter__)) + self.assertFalse(iscoroutinefunction(mock_instance.__aiter__)) # __anext__ is async - self.assertTrue(asyncio.iscoroutinefunction(instance.__anext__)) - self.assertTrue(asyncio.iscoroutinefunction(mock_instance.__anext__)) + self.assertTrue(iscoroutinefunction(instance.__anext__)) + self.assertTrue(iscoroutinefunction(mock_instance.__anext__)) for mock_type in [AsyncMock, MagicMock]: with self.subTest(f"test aiter and anext corourtine with {mock_type}"): @@ -740,7 +740,7 @@ class AsyncMockAssert(unittest.TestCase): with self.assertWarns(RuntimeWarning): # Will raise a warning because never awaited mock.async_method() - self.assertTrue(asyncio.iscoroutinefunction(mock.async_method)) + self.assertTrue(iscoroutinefunction(mock.async_method)) mock.async_method.assert_called() mock.async_method.assert_called_once() mock.async_method.assert_called_once_with() -- cgit v1.2.3 From ad2b38dacd3743add10481bde39de984057a5b5c Mon Sep 17 00:00:00 2001 From: Matthew Kokotovich Date: Sat, 25 Jan 2020 04:17:47 -0600 Subject: bpo-39082: Allow AsyncMock to correctly patch static/class methods (GH-18116) Backports: 62865f4532094017a9b780b704686ca9734bc329 Signed-off-by: Chris Withers --- NEWS.d/2020-01-24-13-24-35.bpo-39082.qKgrq_.rst | 1 + mock/mock.py | 2 ++ mock/tests/testasync.py | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 NEWS.d/2020-01-24-13-24-35.bpo-39082.qKgrq_.rst diff --git a/NEWS.d/2020-01-24-13-24-35.bpo-39082.qKgrq_.rst b/NEWS.d/2020-01-24-13-24-35.bpo-39082.qKgrq_.rst new file mode 100644 index 0000000..52c4ee1 --- /dev/null +++ b/NEWS.d/2020-01-24-13-24-35.bpo-39082.qKgrq_.rst @@ -0,0 +1 @@ +Allow AsyncMock to correctly patch static/class methods diff --git a/mock/mock.py b/mock/mock.py index 236a678..1762ed0 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -48,6 +48,8 @@ _safe_super = super def _is_async_obj(obj): if _is_instance_mock(obj) and not isinstance(obj, AsyncMock): return False + if hasattr(obj, '__func__'): + obj = getattr(obj, '__func__') return iscoroutinefunction(obj) or inspect.isawaitable(obj) diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 8d35742..0c0d1ef 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -34,6 +34,15 @@ class AsyncClass: def normal_method(self): pass + @classmethod + async def async_class_method(cls): + pass + + @staticmethod + async def async_static_method(): + pass + + class AwaitableClass: def __await__(self): yield @@ -86,6 +95,20 @@ class AsyncPatchDecoratorTest(unittest.TestCase): test_async() + def test_is_AsyncMock_patch_staticmethod(self): + @patch.object(AsyncClass, 'async_static_method') + def test_async(mock_method): + self.assertIsInstance(mock_method, AsyncMock) + + test_async() + + def test_is_AsyncMock_patch_classmethod(self): + @patch.object(AsyncClass, 'async_class_method') + def test_async(mock_method): + self.assertIsInstance(mock_method, AsyncMock) + + test_async() + def test_async_def_patch(self): @patch(f"{__name__}.async_func", return_value=1) @patch(f"{__name__}.async_func_args", return_value=2) -- cgit v1.2.3 From 1d85e1adde1bebbd5fe26de82ceecbf88ba886b5 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Sat, 25 Jan 2020 07:53:54 -0300 Subject: bpo-37955: correct mock.patch docs with respect to the returned type (GH-15521) Backports: 40c080934b3d49311209b1cb690c2ea1e04df7e7 Signed-off-by: Chris Withers --- mock/mock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mock/mock.py b/mock/mock.py index 1762ed0..3831ba0 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -1739,7 +1739,8 @@ def patch( "as"; very useful if `patch` is creating a mock object for you. `patch` takes arbitrary keyword arguments. These will be passed to - the `Mock` (or `new_callable`) on construction. + `AsyncMock` if the patched object is asynchronous, to `MagicMock` + otherwise or to `new_callable` if specified. `patch.dict(...)`, `patch.multiple(...)` and `patch.object(...)` are available for alternate use-cases. -- cgit v1.2.3 From 0dc01d64279df3c51df2b2e3c6312110fce02973 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Sat, 25 Jan 2020 16:44:46 +0100 Subject: bpo-38932: Mock fully resets child objects on reset_mock(). (GH-17409) Backports: aef7dc89879d099dc704bd8037b8a7686fb72838 Signed-off-by: Chris Withers --- NEWS.d/2020-01-25-13-41-27.bpo-38932.1pu_8I.rst | 1 + mock/mock.py | 2 +- mock/tests/testmock.py | 14 +++++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 NEWS.d/2020-01-25-13-41-27.bpo-38932.1pu_8I.rst diff --git a/NEWS.d/2020-01-25-13-41-27.bpo-38932.1pu_8I.rst b/NEWS.d/2020-01-25-13-41-27.bpo-38932.1pu_8I.rst new file mode 100644 index 0000000..d9ce8e8 --- /dev/null +++ b/NEWS.d/2020-01-25-13-41-27.bpo-38932.1pu_8I.rst @@ -0,0 +1 @@ +Mock fully resets child objects on reset_mock(). Patch by Vegard Stikbakke diff --git a/mock/mock.py b/mock/mock.py index 3831ba0..34d0c39 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -594,7 +594,7 @@ class NonCallableMock(Base): for child in self._mock_children.values(): if isinstance(child, _SpecState) or child is _deleted: continue - child.reset_mock(visited) + child.reset_mock(visited, return_value=return_value, side_effect=side_effect) ret = self._mock_return_value if _is_instance_mock(ret) and ret is not self: diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 7574002..75597b9 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -1638,11 +1638,23 @@ class MockTest(unittest.TestCase): self.assertNotEqual(m.side_effect, None) def test_reset_sideeffect(self): - m = Mock(return_value=10, side_effect=[2,3]) + m = Mock(return_value=10, side_effect=[2, 3]) m.reset_mock(side_effect=True) self.assertEqual(m.return_value, 10) self.assertEqual(m.side_effect, None) + def test_reset_return_with_children(self): + m = MagicMock(f=MagicMock(return_value=1)) + self.assertEqual(m.f(), 1) + m.reset_mock(return_value=True) + self.assertNotEqual(m.f(), 1) + + def test_reset_return_with_children_side_effect(self): + m = MagicMock(f=MagicMock(side_effect=[2, 3])) + self.assertNotEqual(m.f.side_effect, None) + m.reset_mock(side_effect=True) + self.assertEqual(m.f.side_effect, None) + def test_mock_add_spec(self): class _One(object): one = 1 -- cgit v1.2.3 From 740932702ba4fc01a8bbad3d5a1ce9ee799e3b9b Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Mon, 27 Jan 2020 12:18:15 +0530 Subject: bpo-25597: Ensure wraps' return value is used for magic methods in MagicMock (#16029) Backports: 72b1004657e60c900e4cd031b2635b587f4b280e Signed-off-by: Chris Withers --- NEWS.d/2019-09-12-12-11-05.bpo-25597.mPMzVx.rst | 3 ++ mock/mock.py | 6 ++++ mock/tests/testmock.py | 47 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 NEWS.d/2019-09-12-12-11-05.bpo-25597.mPMzVx.rst diff --git a/NEWS.d/2019-09-12-12-11-05.bpo-25597.mPMzVx.rst b/NEWS.d/2019-09-12-12-11-05.bpo-25597.mPMzVx.rst new file mode 100644 index 0000000..5ad8c6d --- /dev/null +++ b/NEWS.d/2019-09-12-12-11-05.bpo-25597.mPMzVx.rst @@ -0,0 +1,3 @@ +Ensure, if ``wraps`` is supplied to :class:`unittest.mock.MagicMock`, it is used +to calculate return values for the magic methods instead of using the default +return values. Patch by Karthikeyan Singaravelan. diff --git a/mock/mock.py b/mock/mock.py index 34d0c39..4815e82 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -2046,6 +2046,12 @@ _side_effect_methods = { def _set_return_value(mock, method, name): + # If _mock_wraps is present then attach it so that wrapped object + # is used for return value is used when called. + if mock._mock_wraps is not None: + method._mock_wraps = getattr(mock._mock_wraps, name) + return + fixed = _return_values.get(name, DEFAULT) if fixed is not DEFAULT: method.return_value = fixed diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 75597b9..7264cc2 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -715,6 +715,53 @@ class MockTest(unittest.TestCase): self.assertRaises(StopIteration, mock.method) + def test_magic_method_wraps_dict(self): + data = {'foo': 'bar'} + + wrapped_dict = MagicMock(wraps=data) + self.assertEqual(wrapped_dict.get('foo'), 'bar') + self.assertEqual(wrapped_dict['foo'], 'bar') + self.assertTrue('foo' in wrapped_dict) + + # return_value is non-sentinel and takes precedence over wrapped value. + wrapped_dict.get.return_value = 'return_value' + self.assertEqual(wrapped_dict.get('foo'), 'return_value') + + # return_value is sentinel and hence wrapped value is returned. + wrapped_dict.get.return_value = sentinel.DEFAULT + self.assertEqual(wrapped_dict.get('foo'), 'bar') + + self.assertEqual(wrapped_dict.get('baz'), None) + with self.assertRaises(KeyError): + wrapped_dict['baz'] + self.assertFalse('bar' in wrapped_dict) + + data['baz'] = 'spam' + self.assertEqual(wrapped_dict.get('baz'), 'spam') + self.assertEqual(wrapped_dict['baz'], 'spam') + self.assertTrue('baz' in wrapped_dict) + + del data['baz'] + self.assertEqual(wrapped_dict.get('baz'), None) + + + def test_magic_method_wraps_class(self): + + class Foo: + + def __getitem__(self, index): + return index + + def __custom_method__(self): + return "foo" + + + klass = MagicMock(wraps=Foo) + obj = klass() + self.assertEqual(obj.__getitem__(2), 2) + self.assertEqual(obj.__custom_method__(), "foo") + + def test_exceptional_side_effect(self): mock = Mock(side_effect=AttributeError) self.assertRaises(AttributeError, mock) -- cgit v1.2.3 From 04ebc282849a202c50f9ab08c6cde6ee67286c2a Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 27 Jan 2020 18:17:37 +0000 Subject: Backports: c7dd3c7d87d6961756d99b57aa13db7c7a03e1f8, skipped: already applied --- lastsync.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lastsync.txt b/lastsync.txt index b7a4c8d..efd2b77 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -a9187c31185fe7ea47271839898416400cc3d976 +c7dd3c7d87d6961756d99b57aa13db7c7a03e1f8 -- cgit v1.2.3 From a4d8faec42cf0d0235f2fdaa889568d0bcd0d757 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 27 Jan 2020 14:55:56 +0000 Subject: Clarify and fix assertions that mocks have not been awaited (GH-18196) - The gc.collect is needed for other implementations, such as pypy - Using context managers over multiple lines will only catch the warning from the first line in the context! - remove a skip for a test that no longer fails on pypy Backports: a46575a8f2ded8b49e26c25bb67192e1500e76ca Signed-off-by: Chris Withers --- mock/mock.py | 1 + mock/tests/testasync.py | 55 +++++++++++++++++++++++++++---------------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/mock/mock.py b/mock/mock.py index 4815e82..3a006b6 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -30,6 +30,7 @@ import inspect import pprint import sys import builtins +from asyncio import iscoroutinefunction from types import CodeType, ModuleType, MethodType from unittest.util import safe_repr from functools import wraps, partial diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 0c0d1ef..676a4e3 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -1,8 +1,10 @@ import asyncio +import gc import inspect import re import unittest +from contextlib import contextmanager from mock import (ANY, call, AsyncMock, patch, MagicMock, Mock, create_autospec, sentinel) @@ -65,6 +67,15 @@ async_foo_name = f'{__name__}.AsyncClass' normal_foo_name = f'{__name__}.NormalClass' +@contextmanager +def assertNeverAwaited(test): + with test.assertWarnsRegex(RuntimeWarning, "was never awaited$"): + yield + # In non-CPython implementations of Python, this is needed because timely + # deallocation is not guaranteed by the garbage collector. + gc.collect() + + class AsyncPatchDecoratorTest(unittest.TestCase): def test_is_coroutine_function_patch(self): @patch.object(AsyncClass, 'async_method') @@ -297,8 +308,7 @@ class AsyncSpecTest(unittest.TestCase): def inner_test(mock_type): async_mock = mock_type(spec=async_func) self.assertIsInstance(async_mock, mock_type) - with self.assertWarns(RuntimeWarning): - # Will raise a warning because never awaited + with assertNeverAwaited(self): self.assertTrue(inspect.isawaitable(async_mock())) sync_mock = mock_type(spec=normal_func) @@ -312,8 +322,7 @@ class AsyncSpecTest(unittest.TestCase): def inner_test(mock_type): async_mock = mock_type(async_func) self.assertIsInstance(async_mock, mock_type) - with self.assertWarns(RuntimeWarning): - # Will raise a warning because never awaited + with assertNeverAwaited(self): self.assertTrue(inspect.isawaitable(async_mock())) sync_mock = mock_type(normal_func) @@ -760,8 +769,7 @@ class AsyncMockAssert(unittest.TestCase): def test_assert_called_but_not_awaited(self): mock = AsyncMock(AsyncClass) - with self.assertWarns(RuntimeWarning): - # Will raise a warning because never awaited + with assertNeverAwaited(self): mock.async_method() self.assertTrue(iscoroutinefunction(mock.async_method)) mock.async_method.assert_called() @@ -802,9 +810,9 @@ class AsyncMockAssert(unittest.TestCase): def test_assert_called_twice_and_awaited_once(self): mock = AsyncMock(AsyncClass) coroutine = mock.async_method() - with self.assertWarns(RuntimeWarning): - # The first call will be awaited so no warning there - # But this call will never get awaited, so it will warn here + # The first call will be awaited so no warning there + # But this call will never get awaited, so it will warn here + with assertNeverAwaited(self): mock.async_method() with self.assertRaises(AssertionError): mock.async_method.assert_awaited() @@ -839,38 +847,34 @@ class AsyncMockAssert(unittest.TestCase): def test_assert_has_calls_not_awaits(self): kalls = [call('foo')] - with self.assertWarns(RuntimeWarning): - # Will raise a warning because never awaited + with assertNeverAwaited(self): self.mock('foo') self.mock.assert_has_calls(kalls) with self.assertRaises(AssertionError): self.mock.assert_has_awaits(kalls) def test_assert_has_mock_calls_on_async_mock_no_spec(self): - with self.assertWarns(RuntimeWarning): - # Will raise a warning because never awaited + with assertNeverAwaited(self): self.mock() kalls_empty = [('', (), {})] self.assertEqual(self.mock.mock_calls, kalls_empty) - with self.assertWarns(RuntimeWarning): - # Will raise a warning because never awaited + with assertNeverAwaited(self): self.mock('foo') + with assertNeverAwaited(self): self.mock('baz') mock_kalls = ([call(), call('foo'), call('baz')]) self.assertEqual(self.mock.mock_calls, mock_kalls) def test_assert_has_mock_calls_on_async_mock_with_spec(self): a_class_mock = AsyncMock(AsyncClass) - with self.assertWarns(RuntimeWarning): - # Will raise a warning because never awaited + with assertNeverAwaited(self): a_class_mock.async_method() kalls_empty = [('', (), {})] self.assertEqual(a_class_mock.async_method.mock_calls, kalls_empty) self.assertEqual(a_class_mock.mock_calls, [call.async_method()]) - with self.assertWarns(RuntimeWarning): - # Will raise a warning because never awaited + with assertNeverAwaited(self): a_class_mock.async_method(1, 2, 3, a=4, b=5) method_kalls = [call(), call(1, 2, 3, a=4, b=5)] mock_kalls = [call.async_method(), call.async_method(1, 2, 3, a=4, b=5)] @@ -878,9 +882,9 @@ class AsyncMockAssert(unittest.TestCase): self.assertEqual(a_class_mock.mock_calls, mock_kalls) def test_async_method_calls_recorded(self): - with self.assertWarns(RuntimeWarning): - # Will raise warnings because never awaited + with assertNeverAwaited(self): self.mock.something(3, fish=None) + with assertNeverAwaited(self): self.mock.something_else.something(6, cake=sentinel.Cake) self.assertEqual(self.mock.method_calls, [ @@ -902,19 +906,20 @@ class AsyncMockAssert(unittest.TestCase): self.assertEqual(attr, []) assert_attrs(self.mock) - with self.assertWarns(RuntimeWarning): - # Will raise warnings because never awaited + with assertNeverAwaited(self): self.mock() + with assertNeverAwaited(self): self.mock(1, 2) + with assertNeverAwaited(self): self.mock(a=3) self.mock.reset_mock() assert_attrs(self.mock) a_mock = AsyncMock(AsyncClass) - with self.assertWarns(RuntimeWarning): - # Will raise warnings because never awaited + with assertNeverAwaited(self): a_mock.async_method() + with assertNeverAwaited(self): a_mock.async_method(1, a=3) a_mock.reset_mock() -- cgit v1.2.3 From af0e3edb1f8df0f22751bd068cdbccc39d377392 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 27 Jan 2020 18:38:51 +0000 Subject: fixup: simplify IsolatedAsyncioTestCase backport --- mock/backports.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/mock/backports.py b/mock/backports.py index 80e8a98..6f20494 100644 --- a/mock/backports.py +++ b/mock/backports.py @@ -48,18 +48,7 @@ if sys.version_info[:2] < (3, 8): while True: query = await queue.get() queue.task_done() - if query is None: - return - fut, awaitable = query - try: - ret = await awaitable - if not fut.cancelled(): - fut.set_result(ret) - except asyncio.CancelledError: - raise - except Exception as ex: - if not fut.cancelled(): - fut.set_exception(ex) + assert query is None def _setupAsyncioLoop(self): assert self._asyncioTestLoop is None -- cgit v1.2.3 From d6deb20320a60af7045d4ee5959b762a88dba743 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Mon, 27 Jan 2020 18:41:10 +0000 Subject: fixup: uncache backport --- mock/tests/support.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/mock/tests/support.py b/mock/tests/support.py index 79576dd..85fd0a3 100644 --- a/mock/tests/support.py +++ b/mock/tests/support.py @@ -28,9 +28,7 @@ def uncache(*names): """ for name in names: - if name in ('sys', 'marshal', 'imp'): - raise ValueError( - "cannot uncache {0}".format(name)) + assert name not in ('sys', 'marshal', 'imp') try: del sys.modules[name] except KeyError: @@ -39,10 +37,7 @@ def uncache(*names): yield finally: for name in names: - try: - del sys.modules[name] - except KeyError: - pass + del sys.modules[name] class _ALWAYS_EQ: -- cgit v1.2.3 From 608b1bd15975abdcd7db51462c53ad0df7376c99 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 28 Jan 2020 07:25:36 +0000 Subject: ignore empty yield methods too --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index f3f4763..7b4f0a2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,6 +6,7 @@ exclude_lines = pragma: no cover if __name__ == .__main__.: : pass + : yield [paths] source = -- cgit v1.2.3 From 0c6893a81d6a6ef106d3f2698e0cf12a021eecc7 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 28 Jan 2020 07:25:50 +0000 Subject: instructions for running coverage on cpython's mock package --- docs/index.txt | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index f03f838..1f2b32a 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -165,5 +165,25 @@ Backporting process 5. Rinse and repeat until ``backport.py`` reports no more patches need applying. -6. If ``backport.py`` has updated ``lastsync.txt``, now would be a good time - to commit that change. +6. If ``backport.py`` has updated ``lastsync.txt`` but not committed it, + now would be a good time to commit that change. + +Checking coverage in upstream +----------------------------- + +Assuming you have the checkout structure as above, and you have compiled your cpython +master branch, then roughly as follows: + +.. code-block:: bash + + ~/vcs/cpython/python.exe -m venv ~/virtualenvs/cpython-master + source ~/virtualenvs/cpython-master/bin/activate + pip install -U setuptools pip + pip install pytest pytest-cov + cd vcs/cpython/Lib/unittest + pytest --cov unittest.mock --cov unittest.test.testmock \ + --cov-config ~/vcs/git/mock/.coveragerc \ + --cov-report term-missing:skip-covered \ + test/testmock/test* + +Ignore `test/testmock/__*__.py` as these aren't present in the backport. -- cgit v1.2.3 From 5db45d4d68bbf1550a83f6390b00bde223547365 Mon Sep 17 00:00:00 2001 From: Carl Friedrich Bolz-Tereick Date: Wed, 29 Jan 2020 16:43:37 +0100 Subject: bpo-39485: fix corner-case in method-detection of mock (GH-18252) Replace check for whether something is a method in the mock module. The previous version fails on PyPy, because there no method wrappers exist (everything looks like a regular Python-defined function). Thus the isinstance(getattr(result, '__get__', None), MethodWrapperTypes) check returns True for any descriptor, not just methods. This condition could also return erroneously True in CPython for C-defined descriptors. Instead to decide whether something is a method, just check directly whether it's a function defined on the class. This passes all tests on CPython and fixes the bug on PyPy. Backports: a327677905956ae0b239ff430a1346dfe265709e Signed-off-by: Chris Withers --- NEWS.d/2020-01-29-14-58-27.bpo-39485.Zy3ot6.rst | 3 +++ mock/mock.py | 6 +----- 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 NEWS.d/2020-01-29-14-58-27.bpo-39485.Zy3ot6.rst diff --git a/NEWS.d/2020-01-29-14-58-27.bpo-39485.Zy3ot6.rst b/NEWS.d/2020-01-29-14-58-27.bpo-39485.Zy3ot6.rst new file mode 100644 index 0000000..f62c31f --- /dev/null +++ b/NEWS.d/2020-01-29-14-58-27.bpo-39485.Zy3ot6.rst @@ -0,0 +1,3 @@ +Fix a bug in :func:`unittest.mock.create_autospec` that would complain about +the wrong number of arguments for custom descriptors defined in an extension +module returning functions. diff --git a/mock/mock.py b/mock/mock.py index 3a006b6..555973a 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -2764,7 +2764,7 @@ def _must_skip(spec, entry, is_type): continue if isinstance(result, (staticmethod, classmethod)): return False - elif isinstance(getattr(result, '__get__', None), MethodWrapperTypes): + elif isinstance(result, FunctionTypes): # Normal method => skip if looked up on type # (if looked up on instance, self is already skipped) return is_type @@ -2794,10 +2794,6 @@ FunctionTypes = ( type(ANY.__eq__), ) -MethodWrapperTypes = ( - type(ANY.__eq__.__get__), -) - file_spec = None -- cgit v1.2.3 From 9b05bea15f383b90c155cff08032bb2db7fbae96 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 16:24:54 +0000 Subject: Get mock coverage back to 100% (GH-18228) * use the `: pass` and `: yield` patterns for code that isn't expected to ever be executed. * The _Call items passed to _AnyComparer are only ever of length two, so assert instead of if/else * fix typo * Fix bug, where stop-without-start patching dict blows up with `TypeError: 'NoneType' object is not iterable`, highlighted by lack of coverage of an except branch. * The fix for bpo-37972 means _Call.count and _Call.index are no longer needed. * add coverage for calling next() on a mock_open with readline.return_value set. * __aiter__ is defined on the Mock so the one on _AsyncIterator is never called. Backports: db5e86adbce12350c26e7ffc2c6673369971a2dc Signed-off-by: Chris Withers --- mock/mock.py | 17 ++++---------- mock/tests/testasync.py | 59 +++++++++++++++---------------------------------- mock/tests/testmock.py | 5 +++++ mock/tests/testpatch.py | 8 +++++++ 4 files changed, 35 insertions(+), 54 deletions(-) diff --git a/mock/mock.py b/mock/mock.py index 555973a..b2fb2c7 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -1049,8 +1049,7 @@ class _AnyComparer(list): the left.""" def __contains__(self, item): for _call in self: - if len(item) != len(_call): - continue + assert len(item) == len(_call) if all([ expected == actual for expected, actual in zip(item, _call) @@ -1865,7 +1864,8 @@ class _patch_dict(object): def __exit__(self, *args): """Unpatch the dict.""" - self._unpatch_dict() + if self._original is not None: + self._unpatch_dict() return False @@ -2183,7 +2183,7 @@ class AsyncMockMixin(Base): async def _execute_mock_call(_mock_self, *args, **kwargs): self = _mock_self - # This is nearly just like super(), except for sepcial handling + # This is nearly just like super(), except for special handling # of coroutines _call = self.call_args @@ -2557,12 +2557,6 @@ class _Call(tuple): return _Call(name=name, parent=self, from_kall=False) - def count(self, *args, **kwargs): - return self.__getattr__('count')(*args, **kwargs) - - def index(self, *args, **kwargs): - return self.__getattr__('index')(*args, **kwargs) - def _get_call_arguments(self): if len(self) == 2: args, kwargs = self @@ -2933,9 +2927,6 @@ class _AsyncIterator: code_mock.co_flags = inspect.CO_ITERABLE_COROUTINE self.__dict__['__code__'] = code_mock - def __aiter__(self): - return self - async def __anext__(self): try: return next(self.iterator) diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 676a4e3..8afa49c 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -29,38 +29,28 @@ def tearDownModule(): class AsyncClass: - def __init__(self): - pass - async def async_method(self): - pass - def normal_method(self): - pass + def __init__(self): pass + async def async_method(self): pass + def normal_method(self): pass @classmethod - async def async_class_method(cls): - pass + async def async_class_method(cls): pass @staticmethod - async def async_static_method(): - pass + async def async_static_method(): pass class AwaitableClass: - def __await__(self): - yield + def __await__(self): yield -async def async_func(): - pass +async def async_func(): pass -async def async_func_args(a, b, *, c): - pass +async def async_func_args(a, b, *, c): pass -def normal_func(): - pass +def normal_func(): pass class NormalClass(object): - def a(self): - pass + def a(self): pass async_foo_name = f'{__name__}.AsyncClass' @@ -415,8 +405,7 @@ class AsyncSpecSetTest(unittest.TestCase): class AsyncArguments(IsolatedAsyncioTestCase): async def test_add_return_value(self): - async def addition(self, var): - return var + 1 + async def addition(self, var): pass mock = AsyncMock(addition, return_value=10) output = await mock(5) @@ -424,8 +413,7 @@ class AsyncArguments(IsolatedAsyncioTestCase): self.assertEqual(output, 10) async def test_add_side_effect_exception(self): - async def addition(var): - return var + 1 + async def addition(var): pass mock = AsyncMock(addition, side_effect=Exception('err')) with self.assertRaises(Exception): await mock(5) @@ -566,18 +554,14 @@ class AsyncMagicMethods(unittest.TestCase): class AsyncContextManagerTest(unittest.TestCase): class WithAsyncContextManager: - async def __aenter__(self, *args, **kwargs): - return self + async def __aenter__(self, *args, **kwargs): pass - async def __aexit__(self, *args, **kwargs): - pass + async def __aexit__(self, *args, **kwargs): pass class WithSyncContextManager: - def __enter__(self, *args, **kwargs): - return self + def __enter__(self, *args, **kwargs): pass - def __exit__(self, *args, **kwargs): - pass + def __exit__(self, *args, **kwargs): pass class ProductionCode: # Example real-world(ish) code @@ -686,16 +670,9 @@ class AsyncIteratorTest(unittest.TestCase): def __init__(self): self.items = ["foo", "NormalFoo", "baz"] - def __aiter__(self): - return self - - async def __anext__(self): - try: - return self.items.pop() - except IndexError: - pass + def __aiter__(self): pass - raise StopAsyncIteration + async def __anext__(self): pass def test_aiter_set_return_value(self): mock_iter = AsyncMock(name="tester") diff --git a/mock/tests/testmock.py b/mock/tests/testmock.py index 7264cc2..8bb8759 100644 --- a/mock/tests/testmock.py +++ b/mock/tests/testmock.py @@ -1870,6 +1870,11 @@ class MockTest(unittest.TestCase): with self.assertRaises(StopIteration): next(f1) + def test_mock_open_next_with_readline_with_return_value(self): + mopen = mock.mock_open(read_data='foo\nbarn') + mopen.return_value.readline.return_value = 'abc' + self.assertEqual('abc', next(mopen())) + def test_mock_open_write(self): # Test exception in file writing write() mock_namedtemp = mock.mock_open(mock.MagicMock(name='JLV')) diff --git a/mock/tests/testpatch.py b/mock/tests/testpatch.py index 1d3050e..fbf4a53 100644 --- a/mock/tests/testpatch.py +++ b/mock/tests/testpatch.py @@ -770,6 +770,14 @@ class PatchTest(unittest.TestCase): self.assertEqual(d, original) + def test_patch_dict_stop_without_start(self): + d = {'foo': 'bar'} + original = d.copy() + patcher = patch.dict(d, [('spam', 'eggs')], clear=True) + self.assertEqual(patcher.stop(), False) + self.assertEqual(d, original) + + def test_patch_dict_class_decorator(self): this = self d = {'spam': 'eggs'} -- cgit v1.2.3 From a294948da73d1135e6ea955cb46413903a0906da Mon Sep 17 00:00:00 2001 From: blhsing Date: Wed, 11 Sep 2019 07:28:06 -0700 Subject: bpo-37972: unittest.mock._Call now passes on __getitem__ to the __getattr__ chaining so that call() can be subscriptable (GH-15565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bpo-37972: unittest.mock._Call now passes on __getitem__ to the __getattr__ chaining so that call() can be subscriptable * 📜🤖 Added by blurb_it. * Update 2019-08-28-21-40-12.bpo-37972.kP-n4L.rst added name of the contributor * bpo-37972: made all dunder methods chainable for _Call * bpo-37972: delegate only attributes of tuple instead to __getattr__ Backports: 72c359912d36705a94fca8b63d80451905a14ae4 Signed-off-by: Chris Withers --- NEWS.d/2019-08-28-21-40-12.bpo-37972.kP-n4L.rst | 5 +++++ mock/mock.py | 6 ++++++ mock/tests/testhelpers.py | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 NEWS.d/2019-08-28-21-40-12.bpo-37972.kP-n4L.rst diff --git a/NEWS.d/2019-08-28-21-40-12.bpo-37972.kP-n4L.rst b/NEWS.d/2019-08-28-21-40-12.bpo-37972.kP-n4L.rst new file mode 100644 index 0000000..22cb052 --- /dev/null +++ b/NEWS.d/2019-08-28-21-40-12.bpo-37972.kP-n4L.rst @@ -0,0 +1,5 @@ +Subscripts to the `unittest.mock.call` objects now receive the same chaining mechanism as any other custom attributes, so that the following usage no longer raises a `TypeError`: + + call().foo().__getitem__('bar') + +Patch by blhsing diff --git a/mock/mock.py b/mock/mock.py index b2fb2c7..e91920d 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -2557,6 +2557,12 @@ class _Call(tuple): return _Call(name=name, parent=self, from_kall=False) + def __getattribute__(self, attr): + if attr in tuple.__dict__: + raise AttributeError + return tuple.__getattribute__(self, attr) + + def _get_call_arguments(self): if len(self) == 2: args, kwargs = self diff --git a/mock/tests/testhelpers.py b/mock/tests/testhelpers.py index 3dd95f2..eea9fe2 100644 --- a/mock/tests/testhelpers.py +++ b/mock/tests/testhelpers.py @@ -359,6 +359,26 @@ class CallTest(unittest.TestCase): self.assertEqual(_Call((('bar', 'barz'),),)[0], '') self.assertEqual(_Call((('bar', 'barz'), {'hello': 'world'}),)[0], '') + def test_dunder_call(self): + m = MagicMock() + m().foo()['bar']() + self.assertEqual( + m.mock_calls, + [call(), call().foo(), call().foo().__getitem__('bar'), call().foo().__getitem__()()] + ) + m = MagicMock() + m().foo()['bar'] = 1 + self.assertEqual( + m.mock_calls, + [call(), call().foo(), call().foo().__setitem__('bar', 1)] + ) + m = MagicMock() + iter(m().foo()) + self.assertEqual( + m.mock_calls, + [call(), call().foo(), call().foo().__iter__()] + ) + class SpecSignatureTest(unittest.TestCase): -- cgit v1.2.3 From 3610394f7dfeb8c8a9ba2c789ed1ccd020abddec Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Fri, 24 Jan 2020 07:34:27 +0000 Subject: latest sync point --- lastsync.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lastsync.txt b/lastsync.txt index efd2b77..823b97b 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -c7dd3c7d87d6961756d99b57aa13db7c7a03e1f8 +db5e86adbce12350c26e7ffc2c6673369971a2dc -- cgit v1.2.3 From 37f664b1dd378d7de97624bcda9b5ee09abc87a7 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 19:26:01 +0000 Subject: fixup: point sphinx conf at new version location --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d2be5a5..0197463 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -71,7 +71,7 @@ copyright = u'2007-2015, Michael Foord & the mock team' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. Supplied by pbr. # -version = release = mock.mock.__version__ +version = release = mock.__version__ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: (Set from pbr) -- cgit v1.2.3 From 7fd17496a8b840d6f51dfdc639a8310ec8efa36e Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 19:39:12 +0000 Subject: bug fix on committing changed version file --- release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.py b/release.py index 7ef4f16..3fa9406 100644 --- a/release.py +++ b/release.py @@ -69,7 +69,7 @@ def git(command): def git_commit(new_version): git('rm NEWS.d/*') git('add CHANGELOG.rst') - git('add mock/mock.py') + git('add mock/__init__.py') git(f'commit -m "Preparing for {new_version} release."') -- cgit v1.2.3 From fe43b42af2907a2990cd7223338b381a3e7ca01a Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 19:41:50 +0000 Subject: another code coverage pattern --- docs/index.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/index.txt b/docs/index.txt index 1f2b32a..bc3df99 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -117,6 +117,20 @@ Backporting rules def will_never_be_called(): pass +- If code such as this causes coverage checking to drop below 100%: + + .. code-block:: python + + def will_never_be_called(): + yield + + It should be adjusted to the following pattern, preferably upstream, + so that the ``.coveragerc`` in this repo knows to ignore it: + + .. code-block:: python + + def will_never_be_called(): yield + Backporting process ------------------- -- cgit v1.2.3 From 9004d447d9c8dcf3d4e039628d31c3e01c74f0f5 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 19:42:00 +0000 Subject: ReST bugfix --- docs/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index bc3df99..95f6817 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -200,4 +200,4 @@ master branch, then roughly as follows: --cov-report term-missing:skip-covered \ test/testmock/test* -Ignore `test/testmock/__*__.py` as these aren't present in the backport. +Ignore ``test/testmock/__*__.py`` as these aren't present in the backport. -- cgit v1.2.3 From ba8cbf9dff0f44b9d4f281487c046de09095dbbb Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 19:29:46 +0000 Subject: Preparing for 4.1.0 release. --- CHANGELOG.rst | 105 ++++++++++++++++++++++++ NEWS.d/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst | 3 - NEWS.d/2019-06-22-22-00-35.bpo-37212.Zhv-tq.rst | 2 - NEWS.d/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst | 2 - NEWS.d/2019-07-19-20-13-48.bpo-37555.S5am28.rst | 2 - NEWS.d/2019-07-26-00-12-29.bpo-37685.TqckMZ.rst | 4 - NEWS.d/2019-08-28-21-40-12.bpo-37972.kP-n4L.rst | 5 -- NEWS.d/2019-09-10-10-59-50.bpo-37251.8zn2o3.rst | 3 - NEWS.d/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst | 2 - NEWS.d/2019-09-12-12-11-05.bpo-25597.mPMzVx.rst | 3 - NEWS.d/2019-09-15-21-31-18.bpo-37828.gLLDX7.rst | 2 - NEWS.d/2019-09-16-09-54-42.bpo-38136.MdI-Zb.rst | 3 - NEWS.d/2019-09-24-18-45-46.bpo-36871.p47knk.rst | 3 - NEWS.d/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst | 2 - NEWS.d/2019-09-27-16-31-28.bpo-38161.zehai1.rst | 1 - NEWS.d/2019-09-28-20-16-40.bpo-38163.x51-vK.rst | 4 - NEWS.d/2019-10-14-21-14-55.bpo-38473.uXpVld.rst | 2 - NEWS.d/2019-11-04-02-54-16.bpo-38669.pazXZ8.rst | 1 - NEWS.d/2019-11-18-22-10-55.bpo-38839.di6tXv.rst | 1 - NEWS.d/2019-11-19-16-28-25.bpo-38857.YPUkU9.rst | 4 - NEWS.d/2019-11-19-16-30-46.bpo-38859.AZUzL8.rst | 3 - NEWS.d/2019-12-14-14-38-40.bpo-21600.kC4Cgh.rst | 2 - NEWS.d/2020-01-24-13-24-35.bpo-39082.qKgrq_.rst | 1 - NEWS.d/2020-01-25-13-41-27.bpo-38932.1pu_8I.rst | 1 - NEWS.d/2020-01-29-14-58-27.bpo-39485.Zy3ot6.rst | 3 - 25 files changed, 105 insertions(+), 59 deletions(-) delete mode 100644 NEWS.d/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst delete mode 100644 NEWS.d/2019-06-22-22-00-35.bpo-37212.Zhv-tq.rst delete mode 100644 NEWS.d/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst delete mode 100644 NEWS.d/2019-07-19-20-13-48.bpo-37555.S5am28.rst delete mode 100644 NEWS.d/2019-07-26-00-12-29.bpo-37685.TqckMZ.rst delete mode 100644 NEWS.d/2019-08-28-21-40-12.bpo-37972.kP-n4L.rst delete mode 100644 NEWS.d/2019-09-10-10-59-50.bpo-37251.8zn2o3.rst delete mode 100644 NEWS.d/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst delete mode 100644 NEWS.d/2019-09-12-12-11-05.bpo-25597.mPMzVx.rst delete mode 100644 NEWS.d/2019-09-15-21-31-18.bpo-37828.gLLDX7.rst delete mode 100644 NEWS.d/2019-09-16-09-54-42.bpo-38136.MdI-Zb.rst delete mode 100644 NEWS.d/2019-09-24-18-45-46.bpo-36871.p47knk.rst delete mode 100644 NEWS.d/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst delete mode 100644 NEWS.d/2019-09-27-16-31-28.bpo-38161.zehai1.rst delete mode 100644 NEWS.d/2019-09-28-20-16-40.bpo-38163.x51-vK.rst delete mode 100644 NEWS.d/2019-10-14-21-14-55.bpo-38473.uXpVld.rst delete mode 100644 NEWS.d/2019-11-04-02-54-16.bpo-38669.pazXZ8.rst delete mode 100644 NEWS.d/2019-11-18-22-10-55.bpo-38839.di6tXv.rst delete mode 100644 NEWS.d/2019-11-19-16-28-25.bpo-38857.YPUkU9.rst delete mode 100644 NEWS.d/2019-11-19-16-30-46.bpo-38859.AZUzL8.rst delete mode 100644 NEWS.d/2019-12-14-14-38-40.bpo-21600.kC4Cgh.rst delete mode 100644 NEWS.d/2020-01-24-13-24-35.bpo-39082.qKgrq_.rst delete mode 100644 NEWS.d/2020-01-25-13-41-27.bpo-38932.1pu_8I.rst delete mode 100644 NEWS.d/2020-01-29-14-58-27.bpo-39485.Zy3ot6.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 919648b..7f14e60 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,108 @@ +4.0.0b1 +------- + +- The release is a fresh cut of cpython's `4a686504`__. All changes to :mod:`mock` + from that commit and before are included in this release along with the + subsequent changes listed below. + + __ https://github.com/python/cpython/commit/4a686504eb2bbf69adf78077458508a7ba131667 + +- Issue #37972: Subscripts to the `unittest.mock.call` objects now receive + the same chaining mechanism as any other custom attributes, so that the + following usage no longer raises a `TypeError`: + + call().foo().__getitem__('bar') + + Patch by blhsing + +- Issue #38839: Fix some unused functions in tests. Patch by Adam Johnson. + +- Issue #39485: Fix a bug in :func:`unittest.mock.create_autospec` that + would complain about the wrong number of arguments for custom descriptors + defined in an extension module returning functions. + +- Issue #39082: Allow AsyncMock to correctly patch static/class methods + +- Issue #38093: Fixes AsyncMock so it doesn't crash when used with + AsyncContextManagers or AsyncIterators. + +- Issue #38859: AsyncMock now returns StopAsyncIteration on the exaustion of + a side_effects iterable. Since PEP-479 its Impossible to raise a + StopIteration exception from a coroutine. + +- Issue #38163: Child mocks will now detect their type as either synchronous + or asynchronous, asynchronous child mocks will be AsyncMocks and + synchronous child mocks will be either MagicMock or Mock (depending on + their parent type). + +- Issue #38473: Use signature from inner mock for autospecced methods + attached with :func:`unittest.mock.attach_mock`. Patch by Karthikeyan + Singaravelan. + +- Issue #38136: Changes AsyncMock call count and await count to be two + different counters. Now await count only counts when a coroutine has been + awaited, not when it has been called, and vice-versa. Update the + documentation around this. + +- Issue #37555: Fix `NonCallableMock._call_matcher` returning tuple instead + of `_Call` object when `self._spec_signature` exists. Patch by Elizabeth + Uselton + +- Issue #37251: Remove `__code__` check in AsyncMock that incorrectly + evaluated function specs as async objects but failed to evaluate classes + with `__await__` but no `__code__` attribute defined as async objects. + +- Issue #38669: Raise :exc:`TypeError` when passing target as a string with + :meth:`unittest.mock.patch.object`. + +- Issue #25597: Ensure, if ``wraps`` is supplied to + :class:`unittest.mock.MagicMock`, it is used to calculate return values + for the magic methods instead of using the default return values. Patch by + Karthikeyan Singaravelan. + +- Issue #38108: Any synchronous magic methods on an AsyncMock now return a + MagicMock. Any asynchronous magic methods on a MagicMock now return an + AsyncMock. + +- Issue #21478: Record calls to parent when autospecced object is attached + to a mock using :func:`unittest.mock.attach_mock`. Patch by Karthikeyan + Singaravelan. + +- Issue #38857: AsyncMock fix for return values that are awaitable types. + This also covers side_effect iterable values that happend to be awaitable, + and wraps callables that return an awaitable type. Before these awaitables + were being awaited instead of being returned as is. + +- Issue #38932: Mock fully resets child objects on reset_mock(). Patch by + Vegard Stikbakke + +- Issue #37685: Fixed ``__eq__``, ``__lt__`` etc implementations in some + classes. They now return :data:`NotImplemented` for unsupported type of + the other operand. This allows the other operand to play role (for example + the equality comparison with :data:`~unittest.mock.ANY` will return + ``True``). + +- Issue #37212: :func:`unittest.mock.call` now preserves the order of + keyword arguments in repr output. Patch by Karthikeyan Singaravelan. + +- Issue #37828: Fix default mock name in + :meth:`unittest.mock.Mock.assert_called` exceptions. Patch by Abraham + Toriz Cruz. + +- Issue #36871: Improve error handling for the assert_has_calls and + assert_has_awaits methods of mocks. Fixed a bug where any errors + encountered while binding the expected calls to the mock's spec were + silently swallowed, leading to misleading error output. + +- Issue #21600: Fix :func:`mock.patch.stopall` to stop active patches that + were created with :func:`mock.patch.dict`. + +- Issue #38161: Removes _AwaitEvent from AsyncMock. + +- Issue #36871: Ensure method signature is used instead of constructor + signature of a class while asserting mock object against method calls. + Patch by Karthikeyan Singaravelan. + 3.0.5 ----- diff --git a/NEWS.d/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst b/NEWS.d/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst deleted file mode 100644 index 218795f..0000000 --- a/NEWS.d/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst +++ /dev/null @@ -1,3 +0,0 @@ -Ensure method signature is used instead of constructor signature of a class -while asserting mock object against method calls. Patch by Karthikeyan -Singaravelan. diff --git a/NEWS.d/2019-06-22-22-00-35.bpo-37212.Zhv-tq.rst b/NEWS.d/2019-06-22-22-00-35.bpo-37212.Zhv-tq.rst deleted file mode 100644 index 520a022..0000000 --- a/NEWS.d/2019-06-22-22-00-35.bpo-37212.Zhv-tq.rst +++ /dev/null @@ -1,2 +0,0 @@ -:func:`unittest.mock.call` now preserves the order of keyword arguments in -repr output. Patch by Karthikeyan Singaravelan. diff --git a/NEWS.d/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst b/NEWS.d/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst deleted file mode 100644 index 0ac9b8e..0000000 --- a/NEWS.d/2019-07-10-23-07-11.bpo-21478.cCw9rF.rst +++ /dev/null @@ -1,2 +0,0 @@ -Record calls to parent when autospecced object is attached to a mock using -:func:`unittest.mock.attach_mock`. Patch by Karthikeyan Singaravelan. diff --git a/NEWS.d/2019-07-19-20-13-48.bpo-37555.S5am28.rst b/NEWS.d/2019-07-19-20-13-48.bpo-37555.S5am28.rst deleted file mode 100644 index 16d1d62..0000000 --- a/NEWS.d/2019-07-19-20-13-48.bpo-37555.S5am28.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix `NonCallableMock._call_matcher` returning tuple instead of `_Call` object -when `self._spec_signature` exists. Patch by Elizabeth Uselton \ No newline at end of file diff --git a/NEWS.d/2019-07-26-00-12-29.bpo-37685.TqckMZ.rst b/NEWS.d/2019-07-26-00-12-29.bpo-37685.TqckMZ.rst deleted file mode 100644 index d1179a6..0000000 --- a/NEWS.d/2019-07-26-00-12-29.bpo-37685.TqckMZ.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fixed ``__eq__``, ``__lt__`` etc implementations in some classes. They now -return :data:`NotImplemented` for unsupported type of the other operand. -This allows the other operand to play role (for example the equality -comparison with :data:`~unittest.mock.ANY` will return ``True``). diff --git a/NEWS.d/2019-08-28-21-40-12.bpo-37972.kP-n4L.rst b/NEWS.d/2019-08-28-21-40-12.bpo-37972.kP-n4L.rst deleted file mode 100644 index 22cb052..0000000 --- a/NEWS.d/2019-08-28-21-40-12.bpo-37972.kP-n4L.rst +++ /dev/null @@ -1,5 +0,0 @@ -Subscripts to the `unittest.mock.call` objects now receive the same chaining mechanism as any other custom attributes, so that the following usage no longer raises a `TypeError`: - - call().foo().__getitem__('bar') - -Patch by blhsing diff --git a/NEWS.d/2019-09-10-10-59-50.bpo-37251.8zn2o3.rst b/NEWS.d/2019-09-10-10-59-50.bpo-37251.8zn2o3.rst deleted file mode 100644 index 27fd1e4..0000000 --- a/NEWS.d/2019-09-10-10-59-50.bpo-37251.8zn2o3.rst +++ /dev/null @@ -1,3 +0,0 @@ -Remove `__code__` check in AsyncMock that incorrectly -evaluated function specs as async objects but failed to evaluate classes -with `__await__` but no `__code__` attribute defined as async objects. diff --git a/NEWS.d/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst b/NEWS.d/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst deleted file mode 100644 index 24a5301..0000000 --- a/NEWS.d/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixes AsyncMock so it doesn't crash when used with AsyncContextManagers -or AsyncIterators. diff --git a/NEWS.d/2019-09-12-12-11-05.bpo-25597.mPMzVx.rst b/NEWS.d/2019-09-12-12-11-05.bpo-25597.mPMzVx.rst deleted file mode 100644 index 5ad8c6d..0000000 --- a/NEWS.d/2019-09-12-12-11-05.bpo-25597.mPMzVx.rst +++ /dev/null @@ -1,3 +0,0 @@ -Ensure, if ``wraps`` is supplied to :class:`unittest.mock.MagicMock`, it is used -to calculate return values for the magic methods instead of using the default -return values. Patch by Karthikeyan Singaravelan. diff --git a/NEWS.d/2019-09-15-21-31-18.bpo-37828.gLLDX7.rst b/NEWS.d/2019-09-15-21-31-18.bpo-37828.gLLDX7.rst deleted file mode 100644 index c364009..0000000 --- a/NEWS.d/2019-09-15-21-31-18.bpo-37828.gLLDX7.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix default mock name in :meth:`unittest.mock.Mock.assert_called` exceptions. -Patch by Abraham Toriz Cruz. diff --git a/NEWS.d/2019-09-16-09-54-42.bpo-38136.MdI-Zb.rst b/NEWS.d/2019-09-16-09-54-42.bpo-38136.MdI-Zb.rst deleted file mode 100644 index 78cad24..0000000 --- a/NEWS.d/2019-09-16-09-54-42.bpo-38136.MdI-Zb.rst +++ /dev/null @@ -1,3 +0,0 @@ -Changes AsyncMock call count and await count to be two different counters. -Now await count only counts when a coroutine has been awaited, not when it -has been called, and vice-versa. Update the documentation around this. diff --git a/NEWS.d/2019-09-24-18-45-46.bpo-36871.p47knk.rst b/NEWS.d/2019-09-24-18-45-46.bpo-36871.p47knk.rst deleted file mode 100644 index 6b7b19a..0000000 --- a/NEWS.d/2019-09-24-18-45-46.bpo-36871.p47knk.rst +++ /dev/null @@ -1,3 +0,0 @@ -Improve error handling for the assert_has_calls and assert_has_awaits methods of -mocks. Fixed a bug where any errors encountered while binding the expected calls -to the mock's spec were silently swallowed, leading to misleading error output. diff --git a/NEWS.d/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst b/NEWS.d/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst deleted file mode 100644 index d7eea36..0000000 --- a/NEWS.d/2019-09-25-21-37-02.bpo-38108.Jr9HU6.rst +++ /dev/null @@ -1,2 +0,0 @@ -Any synchronous magic methods on an AsyncMock now return a MagicMock. Any -asynchronous magic methods on a MagicMock now return an AsyncMock. diff --git a/NEWS.d/2019-09-27-16-31-28.bpo-38161.zehai1.rst b/NEWS.d/2019-09-27-16-31-28.bpo-38161.zehai1.rst deleted file mode 100644 index 0077033..0000000 --- a/NEWS.d/2019-09-27-16-31-28.bpo-38161.zehai1.rst +++ /dev/null @@ -1 +0,0 @@ -Removes _AwaitEvent from AsyncMock. diff --git a/NEWS.d/2019-09-28-20-16-40.bpo-38163.x51-vK.rst b/NEWS.d/2019-09-28-20-16-40.bpo-38163.x51-vK.rst deleted file mode 100644 index 5f7db26..0000000 --- a/NEWS.d/2019-09-28-20-16-40.bpo-38163.x51-vK.rst +++ /dev/null @@ -1,4 +0,0 @@ -Child mocks will now detect their type as either synchronous or -asynchronous, asynchronous child mocks will be AsyncMocks and synchronous -child mocks will be either MagicMock or Mock (depending on their parent -type). diff --git a/NEWS.d/2019-10-14-21-14-55.bpo-38473.uXpVld.rst b/NEWS.d/2019-10-14-21-14-55.bpo-38473.uXpVld.rst deleted file mode 100644 index de80e89..0000000 --- a/NEWS.d/2019-10-14-21-14-55.bpo-38473.uXpVld.rst +++ /dev/null @@ -1,2 +0,0 @@ -Use signature from inner mock for autospecced methods attached with -:func:`unittest.mock.attach_mock`. Patch by Karthikeyan Singaravelan. diff --git a/NEWS.d/2019-11-04-02-54-16.bpo-38669.pazXZ8.rst b/NEWS.d/2019-11-04-02-54-16.bpo-38669.pazXZ8.rst deleted file mode 100644 index 5060ecf..0000000 --- a/NEWS.d/2019-11-04-02-54-16.bpo-38669.pazXZ8.rst +++ /dev/null @@ -1 +0,0 @@ -Raise :exc:`TypeError` when passing target as a string with :meth:`unittest.mock.patch.object`. \ No newline at end of file diff --git a/NEWS.d/2019-11-18-22-10-55.bpo-38839.di6tXv.rst b/NEWS.d/2019-11-18-22-10-55.bpo-38839.di6tXv.rst deleted file mode 100644 index 80c5a5b..0000000 --- a/NEWS.d/2019-11-18-22-10-55.bpo-38839.di6tXv.rst +++ /dev/null @@ -1 +0,0 @@ -Fix some unused functions in tests. Patch by Adam Johnson. diff --git a/NEWS.d/2019-11-19-16-28-25.bpo-38857.YPUkU9.rst b/NEWS.d/2019-11-19-16-28-25.bpo-38857.YPUkU9.rst deleted file mode 100644 index f28df28..0000000 --- a/NEWS.d/2019-11-19-16-28-25.bpo-38857.YPUkU9.rst +++ /dev/null @@ -1,4 +0,0 @@ -AsyncMock fix for return values that are awaitable types. This also covers -side_effect iterable values that happend to be awaitable, and wraps -callables that return an awaitable type. Before these awaitables were being -awaited instead of being returned as is. diff --git a/NEWS.d/2019-11-19-16-30-46.bpo-38859.AZUzL8.rst b/NEWS.d/2019-11-19-16-30-46.bpo-38859.AZUzL8.rst deleted file mode 100644 index c059539..0000000 --- a/NEWS.d/2019-11-19-16-30-46.bpo-38859.AZUzL8.rst +++ /dev/null @@ -1,3 +0,0 @@ -AsyncMock now returns StopAsyncIteration on the exaustion of a side_effects -iterable. Since PEP-479 its Impossible to raise a StopIteration exception -from a coroutine. diff --git a/NEWS.d/2019-12-14-14-38-40.bpo-21600.kC4Cgh.rst b/NEWS.d/2019-12-14-14-38-40.bpo-21600.kC4Cgh.rst deleted file mode 100644 index 0f72639..0000000 --- a/NEWS.d/2019-12-14-14-38-40.bpo-21600.kC4Cgh.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix :func:`mock.patch.stopall` to stop active patches that were created with -:func:`mock.patch.dict`. diff --git a/NEWS.d/2020-01-24-13-24-35.bpo-39082.qKgrq_.rst b/NEWS.d/2020-01-24-13-24-35.bpo-39082.qKgrq_.rst deleted file mode 100644 index 52c4ee1..0000000 --- a/NEWS.d/2020-01-24-13-24-35.bpo-39082.qKgrq_.rst +++ /dev/null @@ -1 +0,0 @@ -Allow AsyncMock to correctly patch static/class methods diff --git a/NEWS.d/2020-01-25-13-41-27.bpo-38932.1pu_8I.rst b/NEWS.d/2020-01-25-13-41-27.bpo-38932.1pu_8I.rst deleted file mode 100644 index d9ce8e8..0000000 --- a/NEWS.d/2020-01-25-13-41-27.bpo-38932.1pu_8I.rst +++ /dev/null @@ -1 +0,0 @@ -Mock fully resets child objects on reset_mock(). Patch by Vegard Stikbakke diff --git a/NEWS.d/2020-01-29-14-58-27.bpo-39485.Zy3ot6.rst b/NEWS.d/2020-01-29-14-58-27.bpo-39485.Zy3ot6.rst deleted file mode 100644 index f62c31f..0000000 --- a/NEWS.d/2020-01-29-14-58-27.bpo-39485.Zy3ot6.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix a bug in :func:`unittest.mock.create_autospec` that would complain about -the wrong number of arguments for custom descriptors defined in an extension -module returning functions. -- cgit v1.2.3 From 35c733d6128877141e06a28ed670535f67580455 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 19:53:11 +0000 Subject: fixup: package checks --- .circleci/config.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d3c6284..395c8fa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -58,12 +58,6 @@ common: &common branches: only: master - - check-package: - name: check-package-python27 - image: circleci/python:3.7 - requires: - - package - - check-package: name: check-package-python37 image: circleci/python:3.7 @@ -78,7 +72,7 @@ common: &common - check-package: name: check-package-pypy36 - image: pypy:2.7 + image: pypy:3.6 python: pypy requires: - package -- cgit v1.2.3 From 4bc7455e26f2cef983d878a7a46ce961d961888e Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 19:56:18 +0000 Subject: paranoid packaging check for 3.6 --- .circleci/config.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 395c8fa..1e51e9f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -59,7 +59,13 @@ common: &common only: master - check-package: - name: check-package-python37 + name: check-package-python36 + image: circleci/python:3.6 + requires: + - package + + - check-package: + name: check-package-python36 image: circleci/python:3.7 requires: - package @@ -81,6 +87,7 @@ common: &common name: release config: .carthorse.yml requires: + - check-package-python36 - check-package-python37 - check-package-python38 - check-package-pypy36 -- cgit v1.2.3 From 7a4a7d20bbdd0da2ffa945e91969afd639da67e6 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 20:48:51 +0000 Subject: another packaging test bugfix --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e51e9f..c470185 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -79,7 +79,6 @@ common: &common - check-package: name: check-package-pypy36 image: pypy:3.6 - python: pypy requires: - package -- cgit v1.2.3 From 8fb893c3485eee683348e113f90334b6341c99a3 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 20:50:11 +0000 Subject: *sigh* --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c470185..ca01623 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -65,7 +65,7 @@ common: &common - package - check-package: - name: check-package-python36 + name: check-package-python37 image: circleci/python:3.7 requires: - package -- cgit v1.2.3 From 87ca4065d461e46430070a0f1680e45c9c80a1f0 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 21:14:42 +0000 Subject: this skip is no longer needed --- mock/tests/testhelpers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mock/tests/testhelpers.py b/mock/tests/testhelpers.py index eea9fe2..2a28796 100644 --- a/mock/tests/testhelpers.py +++ b/mock/tests/testhelpers.py @@ -502,8 +502,6 @@ class SpecSignatureTest(unittest.TestCase): self._check_someclass_mock(mock) - @pytest.mark.skipif(IS_PYPY, - reason="https://bitbucket.org/pypy/pypy/issues/3010") def test_spec_has_descriptor_returning_function(self): class CrazyDescriptor(object): -- cgit v1.2.3 From 9e5e0380626fd3c540aa4799df0e794cf24d16aa Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 29 Jan 2020 21:14:49 +0000 Subject: *sigh* --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index ca01623..9855ef7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -79,6 +79,7 @@ common: &common - check-package: name: check-package-pypy36 image: pypy:3.6 + python: pypy3 requires: - package -- cgit v1.2.3 From ea9f71536da0ce3bd31e31b5f428f3495c6ab0dc Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 5 Feb 2020 08:13:41 +0000 Subject: Prepare for 4.0.0 release --- CHANGELOG.rst | 5 +++++ mock/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7f14e60..d3efd61 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,8 @@ +4.0.0 +----- + +- No Changes from 4.0.0b1. + 4.0.0b1 ------- diff --git a/mock/__init__.py b/mock/__init__.py index cdf5a16..3624b22 100644 --- a/mock/__init__.py +++ b/mock/__init__.py @@ -7,7 +7,7 @@ IS_PYPY = 'PyPy' in sys.version import mock.mock as _mock from mock.mock import * -__version__ = '4.0.0b1' +__version__ = '4.0.0' version_info = tuple(int(p) for p in re.match(r'(\d+).(\d+).(\d+)', __version__).groups()) -- cgit v1.2.3 From bab8ceab6219866b1589bec37e74c7a6dd85bc80 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Thu, 6 Feb 2020 15:42:01 +0000 Subject: Remove universal wheel tag and prepare for a 4.0.1 release --- CHANGELOG.rst | 5 +++++ mock/__init__.py | 2 +- setup.cfg | 3 --- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d3efd61..adf3e28 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,8 @@ +4.0.1 +----- + +- Remove the universal marker from the wheel. + 4.0.0 ----- diff --git a/mock/__init__.py b/mock/__init__.py index 3624b22..1b40467 100644 --- a/mock/__init__.py +++ b/mock/__init__.py @@ -7,7 +7,7 @@ IS_PYPY = 'PyPy' in sys.version import mock.mock as _mock from mock.mock import * -__version__ = '4.0.0' +__version__ = '4.0.1' version_info = tuple(int(p) for p in re.match(r'(\d+).(\d+).(\d+)', __version__).groups()) diff --git a/setup.cfg b/setup.cfg index d70c78e..e25e6cc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,9 +38,6 @@ build = wheel blurb -[bdist_wheel] -universal = 1 - [tool:pytest] python_files=test*.py filterwarnings = -- cgit v1.2.3 From 742b7f025cfe641fcbb8ef6b1043d00ccfdcf840 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Wed, 11 Mar 2020 20:36:12 +0530 Subject: bpo-39915: Ensure await_args_list is updated according to the order in which coroutines were awaited (GH-18924) Create call objects with awaited arguments instead of using call_args which has only last call value. Backports: e553f204bf0e39b1d701a364bc71b286acb9433f Signed-off-by: Chris Withers --- NEWS.d/2020-03-10-19-38-47.bpo-39915.CjPeiY.rst | 4 ++++ mock/mock.py | 2 +- mock/tests/testasync.py | 11 +++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 NEWS.d/2020-03-10-19-38-47.bpo-39915.CjPeiY.rst diff --git a/NEWS.d/2020-03-10-19-38-47.bpo-39915.CjPeiY.rst b/NEWS.d/2020-03-10-19-38-47.bpo-39915.CjPeiY.rst new file mode 100644 index 0000000..2c36947 --- /dev/null +++ b/NEWS.d/2020-03-10-19-38-47.bpo-39915.CjPeiY.rst @@ -0,0 +1,4 @@ +Ensure :attr:`unittest.mock.AsyncMock.await_args_list` has call objects in +the order of awaited arguments instead of using +:attr:`unittest.mock.Mock.call_args` which has the last value of the call. +Patch by Karthikeyan Singaravelan. diff --git a/mock/mock.py b/mock/mock.py index e91920d..4766672 100644 --- a/mock/mock.py +++ b/mock/mock.py @@ -2186,7 +2186,7 @@ class AsyncMockMixin(Base): # This is nearly just like super(), except for special handling # of coroutines - _call = self.call_args + _call = _Call((args, kwargs), two=True) self.await_count += 1 self.await_args = _call self.await_args_list.append(_call) diff --git a/mock/tests/testasync.py b/mock/tests/testasync.py index 8afa49c..9fd2b65 100644 --- a/mock/tests/testasync.py +++ b/mock/tests/testasync.py @@ -513,6 +513,17 @@ class AsyncArguments(IsolatedAsyncioTestCase): mock.assert_awaited() self.assertTrue(ran) + async def test_await_args_list_order(self): + async_mock = AsyncMock() + mock2 = async_mock(2) + mock1 = async_mock(1) + await mock1 + await mock2 + async_mock.assert_has_awaits([call(1), call(2)]) + self.assertEqual(async_mock.await_args_list, [call(1), call(2)]) + self.assertEqual(async_mock.call_args_list, [call(2), call(1)]) + + class AsyncMagicMethods(unittest.TestCase): def test_async_magic_methods_return_async_mocks(self): m_mock = MagicMock() -- cgit v1.2.3 From d06f48d677ffd387fab97a628d9d96613f6af065 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 11 Mar 2020 17:04:44 +0000 Subject: latest sync point --- lastsync.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lastsync.txt b/lastsync.txt index 823b97b..c2edac8 100644 --- a/lastsync.txt +++ b/lastsync.txt @@ -1 +1 @@ -db5e86adbce12350c26e7ffc2c6673369971a2dc +e553f204bf0e39b1d701a364bc71b286acb9433f -- cgit v1.2.3 From 87546bf7b90531e70419e6343c0938dcab3fb36b Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 11 Mar 2020 17:08:10 +0000 Subject: Preparing for 4.0.2 release. --- CHANGELOG.rst | 8 ++++++++ NEWS.d/2020-03-10-19-38-47.bpo-39915.CjPeiY.rst | 4 ---- mock/__init__.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) delete mode 100644 NEWS.d/2020-03-10-19-38-47.bpo-39915.CjPeiY.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index adf3e28..7439fe5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,11 @@ +4.0.2 +----- + +- Issue #39915: Ensure :attr:`unittest.mock.AsyncMock.await_args_list` has + call objects in the order of awaited arguments instead of using + :attr:`unittest.mock.Mock.call_args` which has the last value of the call. + Patch by Karthikeyan Singaravelan. + 4.0.1 ----- diff --git a/NEWS.d/2020-03-10-19-38-47.bpo-39915.CjPeiY.rst b/NEWS.d/2020-03-10-19-38-47.bpo-39915.CjPeiY.rst deleted file mode 100644 index 2c36947..0000000 --- a/NEWS.d/2020-03-10-19-38-47.bpo-39915.CjPeiY.rst +++ /dev/null @@ -1,4 +0,0 @@ -Ensure :attr:`unittest.mock.AsyncMock.await_args_list` has call objects in -the order of awaited arguments instead of using -:attr:`unittest.mock.Mock.call_args` which has the last value of the call. -Patch by Karthikeyan Singaravelan. diff --git a/mock/__init__.py b/mock/__init__.py index 1b40467..180dee5 100644 --- a/mock/__init__.py +++ b/mock/__init__.py @@ -7,7 +7,7 @@ IS_PYPY = 'PyPy' in sys.version import mock.mock as _mock from mock.mock import * -__version__ = '4.0.1' +__version__ = '4.0.2' version_info = tuple(int(p) for p in re.match(r'(\d+).(\d+).(\d+)', __version__).groups()) -- cgit v1.2.3 From b5ce0a5c4d372b77deff46fec8edf974d7d1f875 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 11 Mar 2020 17:10:35 +0000 Subject: Add intersphinx. --- docs/conf.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 0197463..e978b46 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,13 @@ import mock # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.doctest'] +extensions = [ + 'sphinx.ext.intersphinx', + ] + +intersphinx_mapping = { + 'python': ('http://docs.python.org/dev', None), +} doctest_global_setup = """ import os -- cgit v1.2.3