Skip to content

Commit fbb5039

Browse files
authored
Implement selective un-spying and un-patching (#319)
Co-authored-by: Bruno Oliveira <[email protected]> Fixes #259
1 parent a1c7421 commit fbb5039

File tree

3 files changed

+98
-12
lines changed

3 files changed

+98
-12
lines changed

docs/usage.rst

+22
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The supported methods are:
2121
* `mocker.patch.multiple <https://docs.python.org/3/library/unittest.mock.html#patch-multiple>`_
2222
* `mocker.patch.dict <https://docs.python.org/3/library/unittest.mock.html#patch-dict>`_
2323
* `mocker.stopall <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch.stopall>`_
24+
* `mocker.stop <https://docs.python.org/3/library/unittest.mock.html#patch-methods-start-and-stop>`_
2425
* ``mocker.resetall()``: calls `reset_mock() <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.reset_mock>`_ in all mocked objects up to this point.
2526

2627
Also, as a convenience, these names from the ``mock`` module are accessible directly from ``mocker``:
@@ -94,6 +95,27 @@ As of version 3.0.0, ``mocker.spy`` also works with ``async def`` functions.
9495

9596
.. _#175: https://github.com/pytest-dev/pytest-mock/issues/175
9697

98+
As of version 3.10, spying can be also selectively stopped.
99+
100+
.. code-block:: python
101+
102+
def test_with_unspy(mocker):
103+
class Foo:
104+
def bar(self):
105+
return 42
106+
107+
spy = mocker.spy(Foo, "bar")
108+
foo = Foo()
109+
assert foo.bar() == 42
110+
assert spy.call_count == 1
111+
mocker.stop(spy)
112+
assert foo.bar() == 42
113+
assert spy.call_count == 1
114+
115+
116+
``mocker.stop()`` can also be used by ``mocker.patch`` calls.
117+
118+
97119
Stub
98120
----
99121

src/pytest_mock/plugin.py

+23-12
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,10 @@ class MockerFixture:
4444
"""
4545

4646
def __init__(self, config: Any) -> None:
47-
self._patches = [] # type: List[Any]
48-
self._mocks = [] # type: List[Any]
47+
self._patches_and_mocks: List[Tuple[Any, unittest.mock.MagicMock]] = []
4948
self.mock_module = mock_module = get_mock_module(config)
5049
self.patch = self._Patcher(
51-
self._patches, self._mocks, mock_module
50+
self._patches_and_mocks, mock_module
5251
) # type: MockerFixture._Patcher
5352
# aliases for convenience
5453
self.Mock = mock_module.Mock
@@ -82,8 +81,10 @@ def resetall(
8281
else:
8382
supports_reset_mock_with_args = (self.Mock,)
8483

85-
for m in self._mocks:
84+
for p, m in self._patches_and_mocks:
8685
# See issue #237.
86+
if not hasattr(m, "reset_mock"):
87+
continue
8788
if isinstance(m, supports_reset_mock_with_args):
8889
m.reset_mock(return_value=return_value, side_effect=side_effect)
8990
else:
@@ -94,10 +95,22 @@ def stopall(self) -> None:
9495
Stop all patchers started by this fixture. Can be safely called multiple
9596
times.
9697
"""
97-
for p in reversed(self._patches):
98+
for p, m in reversed(self._patches_and_mocks):
9899
p.stop()
99-
self._patches[:] = []
100-
self._mocks[:] = []
100+
self._patches_and_mocks.clear()
101+
102+
def stop(self, mock: unittest.mock.MagicMock) -> None:
103+
"""
104+
Stops a previous patch or spy call by passing the ``MagicMock`` object
105+
returned by it.
106+
"""
107+
for index, (p, m) in enumerate(self._patches_and_mocks):
108+
if mock is m:
109+
p.stop()
110+
del self._patches_and_mocks[index]
111+
break
112+
else:
113+
raise ValueError("This mock object is not registered")
101114

102115
def spy(self, obj: object, name: str) -> unittest.mock.MagicMock:
103116
"""
@@ -186,9 +199,8 @@ class _Patcher:
186199

187200
DEFAULT = object()
188201

189-
def __init__(self, patches, mocks, mock_module):
190-
self._patches = patches
191-
self._mocks = mocks
202+
def __init__(self, patches_and_mocks, mock_module):
203+
self.__patches_and_mocks = patches_and_mocks
192204
self.mock_module = mock_module
193205

194206
def _start_patch(
@@ -200,9 +212,8 @@ def _start_patch(
200212
"""
201213
p = mock_func(*args, **kwargs)
202214
mocked = p.start() # type: unittest.mock.MagicMock
203-
self._patches.append(p)
215+
self.__patches_and_mocks.append((p, mocked))
204216
if hasattr(mocked, "reset_mock"):
205-
self._mocks.append(mocked)
206217
# check if `mocked` is actually a mock object, as depending on autospec or target
207218
# parameters `mocked` can be anything
208219
if hasattr(mocked, "__enter__") and warn_on_mock_enter:

tests/test_pytest_mock.py

+53
Original file line numberDiff line numberDiff line change
@@ -1100,3 +1100,56 @@ def test_get_random_number():
11001100
result = testdir.runpytest_subprocess()
11011101
assert "AssertionError" not in result.stderr.str()
11021102
result.stdout.fnmatch_lines("* 1 passed in *")
1103+
1104+
1105+
def test_stop_patch(mocker):
1106+
class UnSpy:
1107+
def foo(self):
1108+
return 42
1109+
1110+
m = mocker.patch.object(UnSpy, "foo", return_value=0)
1111+
assert UnSpy().foo() == 0
1112+
mocker.stop(m)
1113+
assert UnSpy().foo() == 42
1114+
1115+
with pytest.raises(ValueError):
1116+
mocker.stop(m)
1117+
1118+
1119+
def test_stop_instance_patch(mocker):
1120+
class UnSpy:
1121+
def foo(self):
1122+
return 42
1123+
1124+
m = mocker.patch.object(UnSpy, "foo", return_value=0)
1125+
un_spy = UnSpy()
1126+
assert un_spy.foo() == 0
1127+
mocker.stop(m)
1128+
assert un_spy.foo() == 42
1129+
1130+
1131+
def test_stop_spy(mocker):
1132+
class UnSpy:
1133+
def foo(self):
1134+
return 42
1135+
1136+
spy = mocker.spy(UnSpy, "foo")
1137+
assert UnSpy().foo() == 42
1138+
assert spy.call_count == 1
1139+
mocker.stop(spy)
1140+
assert UnSpy().foo() == 42
1141+
assert spy.call_count == 1
1142+
1143+
1144+
def test_stop_instance_spy(mocker):
1145+
class UnSpy:
1146+
def foo(self):
1147+
return 42
1148+
1149+
spy = mocker.spy(UnSpy, "foo")
1150+
un_spy = UnSpy()
1151+
assert un_spy.foo() == 42
1152+
assert spy.call_count == 1
1153+
mocker.stop(spy)
1154+
assert un_spy.foo() == 42
1155+
assert spy.call_count == 1

0 commit comments

Comments
 (0)