Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Releases
========

Unreleased
----------

* `#547 <https://github.com/pytest-dev/pytest-mock/issues/547>`_: Added ``SpyType`` for annotating ``mocker.spy`` results.

3.15.1
------

Expand Down
6 changes: 2 additions & 4 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,8 @@ also tracks function/method calls, return values and exceptions raised.
assert spy.call_count == 1
assert spy.spy_return == 42

The object returned by ``mocker.spy`` is a ``MagicMock`` object, so all standard checking functions
are available (like ``assert_called_once_with`` or ``call_count`` in the examples above).

In addition, spy objects contain four extra attributes:
The object returned by ``mocker.spy`` is a ``pytest_mock.SpyType`` object which subclasses ``MagicMock``, so all standard checking functions
are available (like ``assert_called_once_with`` or ``call_count`` in the examples above), in addition to four extra attributes:

* ``spy_return``: contains the last returned value of the spied function.
* ``spy_return_iter``: contains a duplicate of the last returned value of the spied function if the value was an iterator and spy was created using ``.spy(..., duplicate_iterators=True)``. Uses `tee <https://docs.python.org/3/library/itertools.html#itertools.tee>`__) to duplicate the iterator.
Expand Down
2 changes: 2 additions & 0 deletions src/pytest_mock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pytest_mock.plugin import MockerFixture
from pytest_mock.plugin import MockType
from pytest_mock.plugin import PytestMockWarning
from pytest_mock.plugin import SpyType
from pytest_mock.plugin import class_mocker
from pytest_mock.plugin import mocker
from pytest_mock.plugin import module_mocker
Expand All @@ -18,6 +19,7 @@
"MockFixture",
"MockType",
"PytestMockWarning",
"SpyType",
"pytest_addoption",
"pytest_configure",
"session_mocker",
Expand Down
20 changes: 16 additions & 4 deletions src/pytest_mock/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@
]


class SpyType(unittest.mock.Mock):
Comment thread
nicoddemus marked this conversation as resolved.
"""
Type stub used to annotate the result of ``mocker.spy``.
"""

spy_return: Any
spy_return_iter: Optional[Iterator[Any]]
spy_return_list: list[Any]
spy_exception: Optional[BaseException]


class PytestMockWarning(UserWarning):
"""Base class for all warnings emitted by pytest-mock."""

Expand Down Expand Up @@ -157,9 +168,7 @@ def stop(self, mock: unittest.mock.MagicMock) -> None:
"""
self._mock_cache.remove(mock)

def spy(
self, obj: object, name: str, duplicate_iterators: bool = False
) -> MockType:
def spy(self, obj: object, name: str, duplicate_iterators: bool = False) -> SpyType:
"""
Create a spy of method. It will run method normally, but it is now
possible to use `mock` call features with it, like call count.
Expand Down Expand Up @@ -210,7 +219,10 @@ async def async_wrapper(*args, **kwargs):

autospec = inspect.ismethod(method) or inspect.isfunction(method)

spy_obj = self.patch.object(obj, name, side_effect=wrapped, autospec=autospec)
spy_obj = cast(
SpyType,
self.patch.object(obj, name, side_effect=wrapped, autospec=autospec),
)
spy_obj.spy_return = None
spy_obj.spy_return_iter = None
spy_obj.spy_return_list = []
Expand Down
35 changes: 28 additions & 7 deletions tests/test_pytest_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from pytest_mock import MockerFixture
from pytest_mock import PytestMockWarning
from pytest_mock import SpyType

pytest_plugins = "pytester"

Expand Down Expand Up @@ -283,6 +284,29 @@ def bar(self, arg):
assert spy.spy_return_list == [20, 22, 24]


def assert_spy_has_no_return(spy: SpyType) -> None:
assert spy.spy_return is None
assert spy.spy_return_iter is None
assert spy.spy_return_list == []


def test_spy_type(mocker: MockerFixture) -> None:
class Foo:
def bar(self) -> str:
return "ok"

foo = Foo()
spy: SpyType = mocker.spy(foo, "bar")

assert_spy_has_no_return(spy)
assert spy.spy_exception is None
spy.assert_not_called()

assert foo.bar() == "ok"
assert spy.spy_return == "ok"
assert spy.spy_return_list == ["ok"]


# Ref: https://docs.python.org/3/library/exceptions.html#exception-hierarchy
@pytest.mark.parametrize(
"exc_cls",
Expand Down Expand Up @@ -357,14 +381,12 @@ def bar(self, x):
return x * 3

spy = mocker.spy(Foo, "bar")
assert spy.spy_return is None
assert spy.spy_return_iter is None
assert spy.spy_return_list == []
assert_spy_has_no_return(spy)
assert spy.spy_exception is None

Foo().bar(10)
assert spy.spy_return == 30
assert spy.spy_return_iter is None # type:ignore[unreachable]
assert spy.spy_return_iter is None
assert spy.spy_return_list == [30]
assert spy.spy_exception is None

Expand All @@ -373,9 +395,7 @@ def bar(self, x):

with pytest.raises(ValueError):
Foo().bar(0)
assert spy.spy_return is None
assert spy.spy_return_iter is None
assert spy.spy_return_list == []
assert_spy_has_no_return(spy)
assert str(spy.spy_exception) == "invalid x"

Foo().bar(15)
Expand Down Expand Up @@ -624,6 +644,7 @@ def bar(self) -> Any:
result_iterator = list(foo.bar())

assert result_iterator == [0, 1, 2]
assert spy.spy_return_iter is not None
assert list(spy.spy_return_iter) == result_iterator

assert foo.bar() == 99
Expand Down
Loading