diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 09beec4..26feeaf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,26 @@ -1.3 ---- +1.4.0 +----- + +* New configuration variable, ``mock_use_standalone_module`` (defaults to ``False``). This forces + the plugin to import ``mock`` instead of ``unittest.mock`` on Python 3. This is useful to import + and use a newer version than the one available in the Python distribution. + +* Previously the plugin would first try to import ``mock`` and fallback to ``unittest.mock`` + in case of an ``ImportError``, but this behavior has been removed because it could hide + hard to debug import errors (`#68`_). + +* Now ``mock`` (Python 2) and ``unittest.mock`` (Python 3) are lazy-loaded to make it possible to + implement the new ``mock_use_standlone_module`` configuration option. As a consequence of this + the undocumented ``pytest_mock.mock_module`` variable, which pointed to the actual mock module + being used by the plugin, has been removed. + +* `DEFAULT `_ is now available from + the ``mocker`` fixture. + +.. _#68: https://github.com/pytest-dev/pytest-mock/issues/68 + +1.3.0 +----- * Add support for Python 3.6. Thanks `@hackebrot`_ for the report (`#59`_). diff --git a/README.rst b/README.rst index bc87fa4..a35254a 100644 --- a/README.rst +++ b/README.rst @@ -68,6 +68,7 @@ Some objects from the ``mock`` module are accessible directly from ``mocker`` fo * `MagicMock `_ * `PropertyMock `_ * `ANY `_ +* `DEFAULT `_ *(Version 1.4)* * `call `_ *(Version 1.1)* * `sentinel `_ *(Version 1.2)* * `mock_open `_ @@ -153,6 +154,24 @@ anyway plus it generates confusing messages on Python 3.5 due to exception chain .. _advanced assertions: https://pytest.org/latest/assert.html +Use standalone "mock" package +----------------------------- + +*New in version 1.4.0.* + +Python 3 users might want to use a newest version of the ``mock`` package as published on PyPI +than the one that comes with the Python distribution. + +.. code-block:: ini + + [pytest] + mock_use_standalone_module = true + +This will force the plugin to import ``mock`` instead of the ``unittest.mock`` module bundled with +Python 3.3+. Note that this option is only used in Python 3+, as Python 2 users only have the option +to use the ``mock`` package from PyPI anyway. + + Requirements ============ diff --git a/pytest_mock.py b/pytest_mock.py index 7fc3afe..173731c 100644 --- a/pytest_mock.py +++ b/pytest_mock.py @@ -1,36 +1,50 @@ import inspect +import sys import pytest -try: - import mock as mock_module -except ImportError: - import unittest.mock as mock_module - from _pytest_mock_version import version __version__ = version +def _get_mock_module(config): + """ + Import and return the actual "mock" module. By default this is "mock" for Python 2 and + "unittest.mock" for Python 3, but the user can force to always use "mock" on Python 3 using + the mock_use_standalone_module ini option. + """ + if not hasattr(_get_mock_module, '_module'): + use_standalone_module = parse_ini_boolean(config.getini('mock_use_standalone_module')) + if sys.version_info[0] == 2 or use_standalone_module: + import mock + _get_mock_module._module = mock + else: + import unittest.mock + _get_mock_module._module = unittest.mock + + return _get_mock_module._module + + class MockFixture(object): """ Fixture that provides the same interface to functions in the mock module, ensuring that they are uninstalled at the end of each test. """ - Mock = mock_module.Mock - MagicMock = mock_module.MagicMock - PropertyMock = mock_module.PropertyMock - call = mock_module.call - ANY = mock_module.ANY - sentinel = mock_module.sentinel - mock_open = mock_module.mock_open - - def __init__(self): + def __init__(self, config): self._patches = [] # list of mock._patch objects self._mocks = [] # list of MagicMock objects - self.patch = self._Patcher(self._patches, self._mocks) - # temporary fix: this should be at class level, but is blowing - # up in Python 3.6 + self._mock_module = mock_module = _get_mock_module(config) + self.patch = self._Patcher(self._patches, self._mocks, mock_module) + # aliases for convenience + self.Mock = mock_module.Mock + self.MagicMock = mock_module.MagicMock + self.PropertyMock = mock_module.PropertyMock + self.call = mock_module.call + self.ANY = mock_module.ANY + self.DEFAULT = mock_module.DEFAULT + self.sentinel = mock_module.sentinel + self.mock_open = mock_module.mock_open self.sentinel = mock_module.sentinel self.mock_open = mock_module.mock_open @@ -90,7 +104,7 @@ def stub(self, name=None): :rtype: mock.MagicMock :return: Stub object. """ - return mock_module.MagicMock(spec=lambda *args, **kwargs: None, name=name) + return self._mock_module.MagicMock(spec=lambda *args, **kwargs: None, name=name) class _Patcher(object): """ @@ -98,9 +112,10 @@ class _Patcher(object): etc. We need this indirection to keep the same API of the mock package. """ - def __init__(self, patches, mocks): + def __init__(self, patches, mocks, mock_module): self._patches = patches self._mocks = mocks + self._mock_module = mock_module def _start_patch(self, mock_func, *args, **kwargs): """Patches something by calling the given function from the mock @@ -115,29 +130,29 @@ def _start_patch(self, mock_func, *args, **kwargs): def object(self, *args, **kwargs): """API to mock.patch.object""" - return self._start_patch(mock_module.patch.object, *args, **kwargs) + return self._start_patch(self._mock_module.patch.object, *args, **kwargs) def multiple(self, *args, **kwargs): """API to mock.patch.multiple""" - return self._start_patch(mock_module.patch.multiple, *args, + return self._start_patch(self._mock_module.patch.multiple, *args, **kwargs) def dict(self, *args, **kwargs): """API to mock.patch.dict""" - return self._start_patch(mock_module.patch.dict, *args, **kwargs) + return self._start_patch(self._mock_module.patch.dict, *args, **kwargs) def __call__(self, *args, **kwargs): """API to mock.patch""" - return self._start_patch(mock_module.patch, *args, **kwargs) + return self._start_patch(self._mock_module.patch, *args, **kwargs) @pytest.yield_fixture -def mocker(): +def mocker(pytestconfig): """ return an object that has the same interface to the `mock` module, but takes care of automatically undoing all patches after each test method. """ - result = MockFixture() + result = MockFixture(pytestconfig) yield result result.stopall() @@ -209,6 +224,8 @@ def wrap_assert_methods(config): if _mock_module_originals: return + mock_module = _get_mock_module(config) + wrappers = { 'assert_not_called': wrap_assert_not_called, 'assert_called_with': wrap_assert_called_with, @@ -247,6 +264,10 @@ def pytest_addoption(parser): 'Monkeypatch the mock library to improve reporting of the ' 'assert_called_... methods', default=True) + parser.addini('mock_use_standalone_module', + 'Use standalone "mock" (from PyPI) instead of builtin "unittest.mock" ' + 'on Python 3', + default=False) def parse_ini_boolean(value): diff --git a/test_pytest_mock.py b/test_pytest_mock.py index ad439d6..a906be5 100644 --- a/test_pytest_mock.py +++ b/test_pytest_mock.py @@ -69,10 +69,8 @@ def mock_using_patch(mocker): def mock_using_patch_multiple(mocker): - from pytest_mock import mock_module - - r = mocker.patch.multiple('os', remove=mock_module.DEFAULT, - listdir=mock_module.DEFAULT) + r = mocker.patch.multiple('os', remove=mocker.DEFAULT, + listdir=mocker.DEFAULT) return r['remove'], r['listdir'] @@ -133,10 +131,12 @@ def test_deprecated_mock(mock, tmpdir): @pytest.mark.parametrize('name', ['MagicMock', 'PropertyMock', 'Mock', 'call', 'ANY', 'sentinel', 'mock_open']) -def test_mocker_aliases(name): - from pytest_mock import mock_module, MockFixture +def test_mocker_aliases(name, pytestconfig): + from pytest_mock import _get_mock_module, MockFixture + + mock_module = _get_mock_module(pytestconfig) - mocker = MockFixture() + mocker = MockFixture(pytestconfig) assert getattr(mocker, name) is getattr(mock_module, name) @@ -203,8 +203,6 @@ def bar(self, arg): @skip_pypy def test_instance_method_by_class_spy(mocker): - from pytest_mock import mock_module - class Foo(object): def bar(self, arg): @@ -215,13 +213,12 @@ def bar(self, arg): other = Foo() assert foo.bar(arg=10) == 20 assert other.bar(arg=10) == 20 - calls = [mock_module.call(foo, arg=10), mock_module.call(other, arg=10)] + calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)] assert spy.call_args_list == calls @skip_pypy def test_instance_method_by_subclass_spy(mocker): - from pytest_mock import mock_module class Base(object): @@ -236,7 +233,7 @@ class Foo(Base): other = Foo() assert foo.bar(arg=10) == 20 assert other.bar(arg=10) == 20 - calls = [mock_module.call(foo, arg=10), mock_module.call(other, arg=10)] + calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)] assert spy.call_args_list == calls @@ -424,12 +421,11 @@ def test_assert_any_call_wrapper(mocker): def test_assert_has_calls(mocker): - from pytest_mock import mock_module stub = mocker.stub() stub("foo") - stub.assert_has_calls([mock_module.call("foo")]) + stub.assert_has_calls([mocker.call("foo")]) with assert_traceback(): - stub.assert_has_calls([mock_module.call("bar")]) + stub.assert_has_calls([mocker.call("bar")]) def test_monkeypatch_ini(mocker, testdir): @@ -447,11 +443,7 @@ def test_foo(mocker): [pytest] mock_traceback_monkeypatch = false """) - if hasattr(testdir, 'runpytest_subprocess'): - result = testdir.runpytest_subprocess() - else: - # pytest 2.7.X - result = testdir.runpytest() + result = runpytest_subprocess(testdir) assert result.ret == 0 @@ -487,14 +479,38 @@ def test_foo(mocker): stub(1, greet='hello') stub.assert_called_once_with(1, greet='hey') """) - if hasattr(testdir, 'runpytest_subprocess'): - result = testdir.runpytest_subprocess('--tb=native') - else: - # pytest 2.7.X - result = testdir.runpytest('--tb=native') + result = runpytest_subprocess(testdir, '--tb=native') assert result.ret == 1 assert 'During handling of the above exception' not in result.stdout.str() assert 'Differing items:' not in result.stdout.str() traceback_lines = [x for x in result.stdout.str().splitlines() if 'Traceback (most recent call last)' in x] assert len(traceback_lines) == 1 # make sure there are no duplicated tracebacks (#44) + + +@pytest.mark.skipif(sys.version_info[0] < 3, reason='Py3 only') +def test_standalone_mock(testdir): + """Check that the "mock_use_standalone" is being used. + """ + testdir.makepyfile(""" + def test_foo(mocker): + pass + """) + testdir.makeini(""" + [pytest] + mock_use_standalone_module = true + """) + result = runpytest_subprocess(testdir) + assert result.ret == 3 + result.stderr.fnmatch_lines([ + "*No module named 'mock'*", + ]) + + +def runpytest_subprocess(testdir, *args): + """Testdir.runpytest_subprocess only available in pytest-2.8+""" + if hasattr(testdir, 'runpytest_subprocess'): + return testdir.runpytest_subprocess(*args) + else: + # pytest 2.7.X + return testdir.runpytest(*args)