Skip to content

Commit 6d5d6dc

Browse files
Implement spy_return_list (#417)
Fix #378 Co-authored-by: Bruno Oliveira <[email protected]>
1 parent dc28a0e commit 6d5d6dc

File tree

4 files changed

+93
-23
lines changed

4 files changed

+93
-23
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Releases
44
UNRELEASED
55
----------
66

7+
* `#417 <https://github.com/pytest-dev/pytest-mock/pull/417>`_: ``spy`` now has ``spy_return_list``, which is a list containing all the values returned by the spied function.
78
* ``pytest-mock`` now requires ``pytest>=6.2.5``.
89
* `#410 <https://github.com/pytest-dev/pytest-mock/pull/410>`_: pytest-mock's ``setup.py`` file is removed.
910
If you relied on this file, e.g. to install pytest using ``setup.py install``,

docs/usage.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ are available (like ``assert_called_once_with`` or ``call_count`` in the example
7979

8080
In addition, spy objects contain two extra attributes:
8181

82-
* ``spy_return``: contains the returned value of the spied function.
82+
* ``spy_return``: contains the last returned value of the spied function.
83+
* ``spy_return_list``: contains a list of all returned values of the spied function (new in ``3.13``).
8384
* ``spy_exception``: contain the last exception value raised by the spied function/method when
8485
it was last called, or ``None`` if no exception was raised.
8586

src/pytest_mock/plugin.py

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
import sys
66
import unittest.mock
77
import warnings
8+
from dataclasses import dataclass
9+
from dataclasses import field
810
from typing import Any
911
from typing import Callable
1012
from typing import Dict
1113
from typing import Generator
1214
from typing import Iterable
15+
from typing import Iterator
1316
from typing import List
1417
from typing import Mapping
1518
from typing import Optional
@@ -43,16 +46,55 @@ class PytestMockWarning(UserWarning):
4346
"""Base class for all warnings emitted by pytest-mock."""
4447

4548

49+
@dataclass
50+
class MockCacheItem:
51+
mock: MockType
52+
patch: Optional[Any] = None
53+
54+
55+
@dataclass
56+
class MockCache:
57+
cache: List[MockCacheItem] = field(default_factory=list)
58+
59+
def find(self, mock: MockType) -> MockCacheItem:
60+
the_mock = next(
61+
(mock_item for mock_item in self.cache if mock_item.mock == mock), None
62+
)
63+
if the_mock is None:
64+
raise ValueError("This mock object is not registered")
65+
return the_mock
66+
67+
def add(self, mock: MockType, **kwargs: Any) -> MockCacheItem:
68+
try:
69+
return self.find(mock)
70+
except ValueError:
71+
self.cache.append(MockCacheItem(mock=mock, **kwargs))
72+
return self.cache[-1]
73+
74+
def remove(self, mock: MockType) -> None:
75+
mock_item = self.find(mock)
76+
self.cache.remove(mock_item)
77+
78+
def clear(self) -> None:
79+
self.cache.clear()
80+
81+
def __iter__(self) -> Iterator[MockCacheItem]:
82+
return iter(self.cache)
83+
84+
def __reversed__(self) -> Iterator[MockCacheItem]:
85+
return reversed(self.cache)
86+
87+
4688
class MockerFixture:
4789
"""
4890
Fixture that provides the same interface to functions in the mock module,
4991
ensuring that they are uninstalled at the end of each test.
5092
"""
5193

5294
def __init__(self, config: Any) -> None:
53-
self._patches_and_mocks: List[Tuple[Any, unittest.mock.MagicMock]] = []
95+
self._mock_cache: MockCache = MockCache()
5496
self.mock_module = mock_module = get_mock_module(config)
55-
self.patch = self._Patcher(self._patches_and_mocks, mock_module) # type: MockerFixture._Patcher
97+
self.patch = self._Patcher(self._mock_cache, mock_module) # type: MockerFixture._Patcher
5698
# aliases for convenience
5799
self.Mock = mock_module.Mock
58100
self.MagicMock = mock_module.MagicMock
@@ -75,7 +117,7 @@ def create_autospec(
75117
m: MockType = self.mock_module.create_autospec(
76118
spec, spec_set, instance, **kwargs
77119
)
78-
self._patches_and_mocks.append((None, m))
120+
self._mock_cache.add(m)
79121
return m
80122

81123
def resetall(
@@ -93,37 +135,39 @@ def resetall(
93135
else:
94136
supports_reset_mock_with_args = (self.Mock,)
95137

96-
for p, m in self._patches_and_mocks:
138+
for mock_item in self._mock_cache:
97139
# See issue #237.
98-
if not hasattr(m, "reset_mock"):
140+
if not hasattr(mock_item.mock, "reset_mock"):
99141
continue
100-
if isinstance(m, supports_reset_mock_with_args):
101-
m.reset_mock(return_value=return_value, side_effect=side_effect)
142+
# NOTE: The mock may be a dictionary
143+
if hasattr(mock_item.mock, "spy_return_list"):
144+
mock_item.mock.spy_return_list = []
145+
if isinstance(mock_item.mock, supports_reset_mock_with_args):
146+
mock_item.mock.reset_mock(
147+
return_value=return_value, side_effect=side_effect
148+
)
102149
else:
103-
m.reset_mock()
150+
mock_item.mock.reset_mock()
104151

105152
def stopall(self) -> None:
106153
"""
107154
Stop all patchers started by this fixture. Can be safely called multiple
108155
times.
109156
"""
110-
for p, m in reversed(self._patches_and_mocks):
111-
if p is not None:
112-
p.stop()
113-
self._patches_and_mocks.clear()
157+
for mock_item in reversed(self._mock_cache):
158+
if mock_item.patch is not None:
159+
mock_item.patch.stop()
160+
self._mock_cache.clear()
114161

115162
def stop(self, mock: unittest.mock.MagicMock) -> None:
116163
"""
117164
Stops a previous patch or spy call by passing the ``MagicMock`` object
118165
returned by it.
119166
"""
120-
for index, (p, m) in enumerate(self._patches_and_mocks):
121-
if mock is m:
122-
p.stop()
123-
del self._patches_and_mocks[index]
124-
break
125-
else:
126-
raise ValueError("This mock object is not registered")
167+
mock_item = self._mock_cache.find(mock)
168+
if mock_item.patch:
169+
mock_item.patch.stop()
170+
self._mock_cache.remove(mock)
127171

128172
def spy(self, obj: object, name: str) -> MockType:
129173
"""
@@ -146,6 +190,7 @@ def wrapper(*args, **kwargs):
146190
raise
147191
else:
148192
spy_obj.spy_return = r
193+
spy_obj.spy_return_list.append(r)
149194
return r
150195

151196
async def async_wrapper(*args, **kwargs):
@@ -158,6 +203,7 @@ async def async_wrapper(*args, **kwargs):
158203
raise
159204
else:
160205
spy_obj.spy_return = r
206+
spy_obj.spy_return_list.append(r)
161207
return r
162208

163209
if asyncio.iscoroutinefunction(method):
@@ -169,6 +215,7 @@ async def async_wrapper(*args, **kwargs):
169215

170216
spy_obj = self.patch.object(obj, name, side_effect=wrapped, autospec=autospec)
171217
spy_obj.spy_return = None
218+
spy_obj.spy_return_list = []
172219
spy_obj.spy_exception = None
173220
return spy_obj
174221

@@ -206,8 +253,8 @@ class _Patcher:
206253

207254
DEFAULT = object()
208255

209-
def __init__(self, patches_and_mocks, mock_module):
210-
self.__patches_and_mocks = patches_and_mocks
256+
def __init__(self, mock_cache, mock_module):
257+
self.__mock_cache = mock_cache
211258
self.mock_module = mock_module
212259

213260
def _start_patch(
@@ -219,7 +266,7 @@ def _start_patch(
219266
"""
220267
p = mock_func(*args, **kwargs)
221268
mocked: MockType = p.start()
222-
self.__patches_and_mocks.append((p, mocked))
269+
self.__mock_cache.add(mock=mocked, patch=p)
223270
if hasattr(mocked, "reset_mock"):
224271
# check if `mocked` is actually a mock object, as depending on autospec or target
225272
# parameters `mocked` can be anything

tests/test_pytest_mock.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,13 @@ def bar(self, arg):
279279
assert other.bar(arg=10) == 20
280280
foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
281281
assert foo.bar.spy_return == 20 # type:ignore[attr-defined]
282+
assert foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
282283
spy.assert_called_once_with(arg=10)
283284
assert spy.spy_return == 20
285+
assert foo.bar(arg=11) == 22
286+
assert foo.bar(arg=12) == 24
287+
assert spy.spy_return == 24
288+
assert spy.spy_return_list == [20, 22, 24]
284289

285290

286291
# Ref: https://docs.python.org/3/library/exceptions.html#exception-hierarchy
@@ -358,10 +363,12 @@ def bar(self, x):
358363

359364
spy = mocker.spy(Foo, "bar")
360365
assert spy.spy_return is None
366+
assert spy.spy_return_list == []
361367
assert spy.spy_exception is None
362368

363369
Foo().bar(10)
364370
assert spy.spy_return == 30
371+
assert spy.spy_return_list == [30]
365372
assert spy.spy_exception is None
366373

367374
# Testing spy can still be reset (#237).
@@ -370,10 +377,12 @@ def bar(self, x):
370377
with pytest.raises(ValueError):
371378
Foo().bar(0)
372379
assert spy.spy_return is None
380+
assert spy.spy_return_list == []
373381
assert str(spy.spy_exception) == "invalid x"
374382

375383
Foo().bar(15)
376384
assert spy.spy_return == 45
385+
assert spy.spy_return_list == [45]
377386
assert spy.spy_exception is None
378387

379388

@@ -409,6 +418,7 @@ class Foo(Base):
409418
calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)]
410419
assert spy.call_args_list == calls
411420
assert spy.spy_return == 20
421+
assert spy.spy_return_list == [20, 20]
412422

413423

414424
@skip_pypy
@@ -422,8 +432,10 @@ def bar(cls, arg):
422432
assert Foo.bar(arg=10) == 20
423433
Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
424434
assert Foo.bar.spy_return == 20 # type:ignore[attr-defined]
435+
assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
425436
spy.assert_called_once_with(arg=10)
426437
assert spy.spy_return == 20
438+
assert spy.spy_return_list == [20]
427439

428440

429441
@skip_pypy
@@ -440,8 +452,10 @@ class Foo(Base):
440452
assert Foo.bar(arg=10) == 20
441453
Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
442454
assert Foo.bar.spy_return == 20 # type:ignore[attr-defined]
455+
assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
443456
spy.assert_called_once_with(arg=10)
444457
assert spy.spy_return == 20
458+
assert spy.spy_return_list == [20]
445459

446460

447461
@skip_pypy
@@ -460,8 +474,10 @@ def bar(cls, arg):
460474
assert Foo.bar(arg=10) == 20
461475
Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
462476
assert Foo.bar.spy_return == 20 # type:ignore[attr-defined]
477+
assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
463478
spy.assert_called_once_with(arg=10)
464479
assert spy.spy_return == 20
480+
assert spy.spy_return_list == [20]
465481

466482

467483
@skip_pypy
@@ -475,8 +491,10 @@ def bar(arg):
475491
assert Foo.bar(arg=10) == 20
476492
Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
477493
assert Foo.bar.spy_return == 20 # type:ignore[attr-defined]
494+
assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
478495
spy.assert_called_once_with(arg=10)
479496
assert spy.spy_return == 20
497+
assert spy.spy_return_list == [20]
480498

481499

482500
@skip_pypy
@@ -493,8 +511,10 @@ class Foo(Base):
493511
assert Foo.bar(arg=10) == 20
494512
Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
495513
assert Foo.bar.spy_return == 20 # type:ignore[attr-defined]
514+
assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
496515
spy.assert_called_once_with(arg=10)
497516
assert spy.spy_return == 20
517+
assert spy.spy_return_list == [20]
498518

499519

500520
def test_callable_like_spy(testdir: Any, mocker: MockerFixture) -> None:
@@ -515,6 +535,7 @@ def __call__(self, x):
515535
uut.call_like(10)
516536
spy.assert_called_once_with(10)
517537
assert spy.spy_return == 20
538+
assert spy.spy_return_list == [20]
518539

519540

520541
async def test_instance_async_method_spy(mocker: MockerFixture) -> None:

0 commit comments

Comments
 (0)