From 3a32bc11405ebb6def501463b99711d9ba8339ef Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Sat, 1 Oct 2022 21:50:49 +0200 Subject: [PATCH] Implement selective un-spying Based on the patch of @nicoddemus in issue #259 Fixes #259 --- docs/usage.rst | 19 +++++++++++++++ src/pytest_mock/plugin.py | 31 ++++++++++++++---------- tests/test_pytest_mock.py | 50 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index f457c74..803927d 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -21,6 +21,7 @@ The supported methods are: * `mocker.patch.multiple `_ * `mocker.patch.dict `_ * `mocker.stopall `_ +* `mocker.stop `_ * ``mocker.resetall()``: calls `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``: @@ -94,6 +95,24 @@ 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 + + Stub ---- diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index 9554750..764b017 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -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 @@ -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: @@ -94,10 +95,18 @@ 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: + 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: """ @@ -186,9 +195,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( @@ -200,9 +208,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: diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index 8033475..97cb32d 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -1100,3 +1100,53 @@ 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 + + +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