Skip to content

Commit 54c2e29

Browse files
committed
Add mock_use_standalone_module ini option and lazy-load of mock module
1 parent 5dfb9d7 commit 54c2e29

File tree

4 files changed

+126
-42
lines changed

4 files changed

+126
-42
lines changed

CHANGELOG.rst

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
1-
1.3
2-
---
1+
1.4.0
2+
-----
3+
4+
* New configuration variable, ``mock_use_standalone_module`` (defaults to ``False``). This forces
5+
the plugin to import ``mock`` instead of ``unittest.mock`` on Python 3. This is useful to import
6+
and use a newer version than the one available in the Python distribution.
7+
8+
* Previously the plugin would first try to import ``mock`` and fallback to ``unittest.mock``
9+
in case of an ``ImportError``, but this behavior has been removed because it could hide
10+
hard to debug import errors (`#68`_).
11+
12+
* Now ``mock`` (Python 2) and ``mock.unittest`` (Python 3) are lazy-loaded to make it possible to
13+
implement the new ``mock_use_standlone_module`` configuration option. As a consequence of this
14+
the undocumented ``pytest_mock.mock_module`` variable, which pointed to the actual mock module
15+
being used by the plugin, has been removed.
16+
17+
.. _#68: https://github.com/pytest-dev/pytest-mock/issues/68
18+
19+
1.3.0
20+
-----
321

422
* Add support for Python 3.6. Thanks `@hackebrot`_ for the report (`#59`_).
523

README.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Some objects from the ``mock`` module are accessible directly from ``mocker`` fo
6868
* `MagicMock <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock>`_
6969
* `PropertyMock <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.PropertyMock>`_
7070
* `ANY <https://docs.python.org/3/library/unittest.mock.html#any>`_
71+
* `DEFAULT <https://docs.python.org/3/library/unittest.mock.html#default>`_
7172
* `call <https://docs.python.org/3/library/unittest.mock.html#call>`_ *(Version 1.1)*
7273
* `sentinel <https://docs.python.org/3/library/unittest.mock.html#sentinel>`_ *(Version 1.2)*
7374
* `mock_open <https://docs.python.org/3/library/unittest.mock.html#mock-open>`_
@@ -153,6 +154,24 @@ anyway plus it generates confusing messages on Python 3.5 due to exception chain
153154
.. _advanced assertions: https://pytest.org/latest/assert.html
154155

155156

157+
Use standalone "mock" package
158+
-----------------------------
159+
160+
*New in version 1.4.0.*
161+
162+
Python 3 users might want to use a newest version of the ``mock`` package as published on PyPI
163+
than the one that comes with the Python distribution.
164+
165+
.. code-block:: ini
166+
167+
[pytest]
168+
mock_use_standalone_module = true
169+
170+
This will force the plugin to import ``mock`` instead of the ``unittest.mock`` module bundled with
171+
Python 3.3+. Note that this option is only used in Python 3+, as Python 2 users only have the option
172+
to use the ``mock`` package from PyPI anyway.
173+
174+
156175
Requirements
157176
============
158177

pytest_mock.py

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,56 @@
11
import inspect
2+
import sys
23

34
import pytest
45

5-
try:
6-
import mock as mock_module
7-
except ImportError:
8-
import unittest.mock as mock_module
9-
106
from _pytest_mock_version import version
117
__version__ = version
128

139

10+
def _get_mock_module(config):
11+
"""
12+
Import and return the actual "mock" module. By default this is "mock" for Python 2 and
13+
"unittest.mock" for Python 3, but the user can force to always use "mock" on Python 3 using
14+
the mock_use_standalone_module ini option.
15+
"""
16+
try:
17+
return _get_mock_module._module
18+
except AttributeError:
19+
if sys.version_info.major == 2:
20+
import mock
21+
_get_mock_module._module = mock
22+
return mock
23+
else:
24+
if parse_ini_boolean(config.getini('mock_use_standalone_module')):
25+
import mock
26+
module = mock
27+
else:
28+
import unittest.mock
29+
module = unittest.mock
30+
_get_mock_module._module = module
31+
return module
32+
33+
1434
class MockFixture(object):
1535
"""
1636
Fixture that provides the same interface to functions in the mock module,
1737
ensuring that they are uninstalled at the end of each test.
1838
"""
1939

20-
Mock = mock_module.Mock
21-
MagicMock = mock_module.MagicMock
22-
PropertyMock = mock_module.PropertyMock
23-
call = mock_module.call
24-
ANY = mock_module.ANY
25-
sentinel = mock_module.sentinel
26-
mock_open = mock_module.mock_open
27-
28-
def __init__(self):
40+
def __init__(self, config):
2941
self._patches = [] # list of mock._patch objects
3042
self._mocks = [] # list of MagicMock objects
31-
self.patch = self._Patcher(self._patches, self._mocks)
32-
# temporary fix: this should be at class level, but is blowing
33-
# up in Python 3.6
43+
self._mock_module = mock_module = _get_mock_module(config)
44+
self.patch = self._Patcher(self._patches, self._mocks, mock_module)
45+
# aliases for convenience
46+
self.Mock = mock_module.Mock
47+
self.MagicMock = mock_module.MagicMock
48+
self.PropertyMock = mock_module.PropertyMock
49+
self.call = mock_module.call
50+
self.ANY = mock_module.ANY
51+
self.DEFAULT = mock_module.DEFAULT
52+
self.sentinel = mock_module.sentinel
53+
self.mock_open = mock_module.mock_open
3454
self.sentinel = mock_module.sentinel
3555
self.mock_open = mock_module.mock_open
3656

@@ -90,17 +110,18 @@ def stub(self, name=None):
90110
:rtype: mock.MagicMock
91111
:return: Stub object.
92112
"""
93-
return mock_module.MagicMock(spec=lambda *args, **kwargs: None, name=name)
113+
return self._mock_module.MagicMock(spec=lambda *args, **kwargs: None, name=name)
94114

95115
class _Patcher(object):
96116
"""
97117
Object to provide the same interface as mock.patch, mock.patch.object,
98118
etc. We need this indirection to keep the same API of the mock package.
99119
"""
100120

101-
def __init__(self, patches, mocks):
121+
def __init__(self, patches, mocks, mock_module):
102122
self._patches = patches
103123
self._mocks = mocks
124+
self._mock_module = mock_module
104125

105126
def _start_patch(self, mock_func, *args, **kwargs):
106127
"""Patches something by calling the given function from the mock
@@ -115,29 +136,29 @@ def _start_patch(self, mock_func, *args, **kwargs):
115136

116137
def object(self, *args, **kwargs):
117138
"""API to mock.patch.object"""
118-
return self._start_patch(mock_module.patch.object, *args, **kwargs)
139+
return self._start_patch(self._mock_module.patch.object, *args, **kwargs)
119140

120141
def multiple(self, *args, **kwargs):
121142
"""API to mock.patch.multiple"""
122-
return self._start_patch(mock_module.patch.multiple, *args,
143+
return self._start_patch(self._mock_module.patch.multiple, *args,
123144
**kwargs)
124145

125146
def dict(self, *args, **kwargs):
126147
"""API to mock.patch.dict"""
127-
return self._start_patch(mock_module.patch.dict, *args, **kwargs)
148+
return self._start_patch(self._mock_module.patch.dict, *args, **kwargs)
128149

129150
def __call__(self, *args, **kwargs):
130151
"""API to mock.patch"""
131-
return self._start_patch(mock_module.patch, *args, **kwargs)
152+
return self._start_patch(self._mock_module.patch, *args, **kwargs)
132153

133154

134155
@pytest.yield_fixture
135-
def mocker():
156+
def mocker(pytestconfig):
136157
"""
137158
return an object that has the same interface to the `mock` module, but
138159
takes care of automatically undoing all patches after each test method.
139160
"""
140-
result = MockFixture()
161+
result = MockFixture(pytestconfig)
141162
yield result
142163
result.stopall()
143164

@@ -209,6 +230,8 @@ def wrap_assert_methods(config):
209230
if _mock_module_originals:
210231
return
211232

233+
mock_module = _get_mock_module(config)
234+
212235
wrappers = {
213236
'assert_not_called': wrap_assert_not_called,
214237
'assert_called_with': wrap_assert_called_with,
@@ -247,6 +270,10 @@ def pytest_addoption(parser):
247270
'Monkeypatch the mock library to improve reporting of the '
248271
'assert_called_... methods',
249272
default=True)
273+
parser.addini('mock_use_standalone_module',
274+
'Use standalone "mock" (from PyPI) instead of builtin "unittest.mock" '
275+
'on Python 3',
276+
default=False)
250277

251278

252279
def parse_ini_boolean(value):

test_pytest_mock.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,8 @@ def mock_using_patch(mocker):
6969

7070

7171
def mock_using_patch_multiple(mocker):
72-
from pytest_mock import mock_module
73-
74-
r = mocker.patch.multiple('os', remove=mock_module.DEFAULT,
75-
listdir=mock_module.DEFAULT)
72+
r = mocker.patch.multiple('os', remove=mocker.DEFAULT,
73+
listdir=mocker.DEFAULT)
7674
return r['remove'], r['listdir']
7775

7876

@@ -133,10 +131,12 @@ def test_deprecated_mock(mock, tmpdir):
133131

134132

135133
@pytest.mark.parametrize('name', ['MagicMock', 'PropertyMock', 'Mock', 'call', 'ANY', 'sentinel', 'mock_open'])
136-
def test_mocker_aliases(name):
137-
from pytest_mock import mock_module, MockFixture
134+
def test_mocker_aliases(name, pytestconfig):
135+
from pytest_mock import _get_mock_module, MockFixture
136+
137+
mock_module = _get_mock_module(pytestconfig)
138138

139-
mocker = MockFixture()
139+
mocker = MockFixture(pytestconfig)
140140
assert getattr(mocker, name) is getattr(mock_module, name)
141141

142142

@@ -203,8 +203,6 @@ def bar(self, arg):
203203

204204
@skip_pypy
205205
def test_instance_method_by_class_spy(mocker):
206-
from pytest_mock import mock_module
207-
208206
class Foo(object):
209207

210208
def bar(self, arg):
@@ -215,13 +213,12 @@ def bar(self, arg):
215213
other = Foo()
216214
assert foo.bar(arg=10) == 20
217215
assert other.bar(arg=10) == 20
218-
calls = [mock_module.call(foo, arg=10), mock_module.call(other, arg=10)]
216+
calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)]
219217
assert spy.call_args_list == calls
220218

221219

222220
@skip_pypy
223221
def test_instance_method_by_subclass_spy(mocker):
224-
from pytest_mock import mock_module
225222

226223
class Base(object):
227224

@@ -236,7 +233,7 @@ class Foo(Base):
236233
other = Foo()
237234
assert foo.bar(arg=10) == 20
238235
assert other.bar(arg=10) == 20
239-
calls = [mock_module.call(foo, arg=10), mock_module.call(other, arg=10)]
236+
calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)]
240237
assert spy.call_args_list == calls
241238

242239

@@ -424,12 +421,11 @@ def test_assert_any_call_wrapper(mocker):
424421

425422

426423
def test_assert_has_calls(mocker):
427-
from pytest_mock import mock_module
428424
stub = mocker.stub()
429425
stub("foo")
430-
stub.assert_has_calls([mock_module.call("foo")])
426+
stub.assert_has_calls([mocker.call("foo")])
431427
with assert_traceback():
432-
stub.assert_has_calls([mock_module.call("bar")])
428+
stub.assert_has_calls([mocker.call("bar")])
433429

434430

435431
def test_monkeypatch_ini(mocker, testdir):
@@ -498,3 +494,27 @@ def test_foo(mocker):
498494
traceback_lines = [x for x in result.stdout.str().splitlines()
499495
if 'Traceback (most recent call last)' in x]
500496
assert len(traceback_lines) == 1 # make sure there are no duplicated tracebacks (#44)
497+
498+
499+
@pytest.mark.skipif(sys.version_info.major < 3, reason='Py3 only')
500+
def test_standalone_mock(testdir):
501+
"""Check that the "mock_use_standalone" is being used.
502+
"""
503+
testdir.makepyfile("""
504+
def test_foo(mocker):
505+
pass
506+
""")
507+
testdir.makeini("""
508+
[pytest]
509+
mock_use_standalone_module = true
510+
""")
511+
if hasattr(testdir, 'runpytest_subprocess'):
512+
result = testdir.runpytest_subprocess()
513+
else:
514+
# pytest 2.7.X
515+
result = testdir.runpytest()
516+
assert result.ret == 3
517+
result.stderr.fnmatch_lines([
518+
"*No module named 'mock'*",
519+
])
520+

0 commit comments

Comments
 (0)