Skip to content

Commit

Permalink
Implement selective un-spying and un-patching (#319)
Browse files Browse the repository at this point in the history
Co-authored-by: Bruno Oliveira <[email protected]>

Fixes #259
  • Loading branch information
sgaist authored Oct 5, 2022
1 parent a1c7421 commit fbb5039
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 12 deletions.
22 changes: 22 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The supported methods are:
* `mocker.patch.multiple <https://docs.python.org/3/library/unittest.mock.html#patch-multiple>`_
* `mocker.patch.dict <https://docs.python.org/3/library/unittest.mock.html#patch-dict>`_
* `mocker.stopall <https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch.stopall>`_
* `mocker.stop <https://docs.python.org/3/library/unittest.mock.html#patch-methods-start-and-stop>`_
* ``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.

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

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

As of version 3.10, spying can be also selectively stopped.

.. code-block:: python
def test_with_unspy(mocker):
class Foo:
def bar(self):
return 42
spy = mocker.spy(Foo, "bar")
foo = Foo()
assert foo.bar() == 42
assert spy.call_count == 1
mocker.stop(spy)
assert foo.bar() == 42
assert spy.call_count == 1
``mocker.stop()`` can also be used by ``mocker.patch`` calls.


Stub
----

Expand Down
35 changes: 23 additions & 12 deletions src/pytest_mock/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,10 @@ class MockerFixture:
"""

def __init__(self, config: Any) -> None:
self._patches = [] # type: List[Any]
self._mocks = [] # type: List[Any]
self._patches_and_mocks: List[Tuple[Any, unittest.mock.MagicMock]] = []
self.mock_module = mock_module = get_mock_module(config)
self.patch = self._Patcher(
self._patches, self._mocks, mock_module
self._patches_and_mocks, mock_module
) # type: MockerFixture._Patcher
# aliases for convenience
self.Mock = mock_module.Mock
Expand Down Expand Up @@ -82,8 +81,10 @@ def resetall(
else:
supports_reset_mock_with_args = (self.Mock,)

for m in self._mocks:
for p, m in self._patches_and_mocks:
# See issue #237.
if not hasattr(m, "reset_mock"):
continue
if isinstance(m, supports_reset_mock_with_args):
m.reset_mock(return_value=return_value, side_effect=side_effect)
else:
Expand All @@ -94,10 +95,22 @@ def stopall(self) -> None:
Stop all patchers started by this fixture. Can be safely called multiple
times.
"""
for p in reversed(self._patches):
for p, m in reversed(self._patches_and_mocks):
p.stop()
self._patches[:] = []
self._mocks[:] = []
self._patches_and_mocks.clear()

def stop(self, mock: unittest.mock.MagicMock) -> None:
"""
Stops a previous patch or spy call by passing the ``MagicMock`` object
returned by it.
"""
for index, (p, m) in enumerate(self._patches_and_mocks):
if mock is m:
p.stop()
del self._patches_and_mocks[index]
break
else:
raise ValueError("This mock object is not registered")

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

DEFAULT = object()

def __init__(self, patches, mocks, mock_module):
self._patches = patches
self._mocks = mocks
def __init__(self, patches_and_mocks, mock_module):
self.__patches_and_mocks = patches_and_mocks
self.mock_module = mock_module

def _start_patch(
Expand All @@ -200,9 +212,8 @@ def _start_patch(
"""
p = mock_func(*args, **kwargs)
mocked = p.start() # type: unittest.mock.MagicMock
self._patches.append(p)
self.__patches_and_mocks.append((p, mocked))
if hasattr(mocked, "reset_mock"):
self._mocks.append(mocked)
# check if `mocked` is actually a mock object, as depending on autospec or target
# parameters `mocked` can be anything
if hasattr(mocked, "__enter__") and warn_on_mock_enter:
Expand Down
53 changes: 53 additions & 0 deletions tests/test_pytest_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1100,3 +1100,56 @@ def test_get_random_number():
result = testdir.runpytest_subprocess()
assert "AssertionError" not in result.stderr.str()
result.stdout.fnmatch_lines("* 1 passed in *")


def test_stop_patch(mocker):
class UnSpy:
def foo(self):
return 42

m = mocker.patch.object(UnSpy, "foo", return_value=0)
assert UnSpy().foo() == 0
mocker.stop(m)
assert UnSpy().foo() == 42

with pytest.raises(ValueError):
mocker.stop(m)


def test_stop_instance_patch(mocker):
class UnSpy:
def foo(self):
return 42

m = mocker.patch.object(UnSpy, "foo", return_value=0)
un_spy = UnSpy()
assert un_spy.foo() == 0
mocker.stop(m)
assert un_spy.foo() == 42


def test_stop_spy(mocker):
class UnSpy:
def foo(self):
return 42

spy = mocker.spy(UnSpy, "foo")
assert UnSpy().foo() == 42
assert spy.call_count == 1
mocker.stop(spy)
assert UnSpy().foo() == 42
assert spy.call_count == 1


def test_stop_instance_spy(mocker):
class UnSpy:
def foo(self):
return 42

spy = mocker.spy(UnSpy, "foo")
un_spy = UnSpy()
assert un_spy.foo() == 42
assert spy.call_count == 1
mocker.stop(spy)
assert un_spy.foo() == 42
assert spy.call_count == 1

0 comments on commit fbb5039

Please sign in to comment.