Skip to content

Commit 184eb19

Browse files
vokrackonicoddemus
andauthored
Set spy_return_iter only when explicitly requested (#537)
Fixes #529 --------- Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
1 parent 4fa0088 commit 184eb19

File tree

4 files changed

+40
-9
lines changed

4 files changed

+40
-9
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Releases
22
========
33

4+
UNRELEASED
5+
----------
6+
7+
*UNRELEASED*
8+
9+
* `#529 <https://github.com/pytest-dev/pytest-mock/issues/529>`_: Fixed ``itertools._tee object has no attribute error`` -- now ``duplicate_iterators=True`` must be passed to ``mocker.spy`` to duplicate iterators.
10+
411
3.15.0
512
------
613

docs/usage.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ also tracks function/method calls, return values and exceptions raised.
7878
The object returned by ``mocker.spy`` is a ``MagicMock`` object, so all standard checking functions
7979
are available (like ``assert_called_once_with`` or ``call_count`` in the examples above).
8080

81-
In addition, spy objects contain two extra attributes:
81+
In addition, spy objects contain four extra attributes:
8282

8383
* ``spy_return``: contains the last returned value of the spied function.
84-
* ``spy_return_iter``: contains a duplicate of the last returned value of the spied function if the value was an iterator. Uses `tee <https://docs.python.org/3/library/itertools.html#itertools.tee>`__) to duplicate the iterator.
84+
* ``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.
8585
* ``spy_return_list``: contains a list of all returned values of the spied function (new in ``3.13``).
8686
* ``spy_exception``: contain the last exception value raised by the spied function/method when
8787
it was last called, or ``None`` if no exception was raised.

src/pytest_mock/plugin.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,16 @@ def stop(self, mock: unittest.mock.MagicMock) -> None:
157157
"""
158158
self._mock_cache.remove(mock)
159159

160-
def spy(self, obj: object, name: str) -> MockType:
160+
def spy(
161+
self, obj: object, name: str, duplicate_iterators: bool = False
162+
) -> MockType:
161163
"""
162164
Create a spy of method. It will run method normally, but it is now
163165
possible to use `mock` call features with it, like call count.
164166
165167
:param obj: An object.
166168
:param name: A method in object.
169+
:param duplicate_iterators: Whether to keep a copy of the returned iterator in `spy_return_iter`.
167170
:return: Spy object.
168171
"""
169172
method = getattr(obj, name)
@@ -177,7 +180,7 @@ def wrapper(*args, **kwargs):
177180
spy_obj.spy_exception = e
178181
raise
179182
else:
180-
if isinstance(r, Iterator):
183+
if duplicate_iterators and isinstance(r, Iterator):
181184
r, duplicated_iterator = itertools.tee(r, 2)
182185
spy_obj.spy_return_iter = duplicated_iterator
183186
else:

tests/test_pytest_mock.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -540,13 +540,15 @@ def __call__(self, x):
540540

541541

542542
@pytest.mark.parametrize("iterator", [(i for i in range(3)), iter([0, 1, 2])])
543-
def test_spy_return_iter(mocker: MockerFixture, iterator: Iterator[int]) -> None:
543+
def test_spy_return_iter_duplicates_iterator_when_enabled(
544+
mocker: MockerFixture, iterator: Iterator[int]
545+
) -> None:
544546
class Foo:
545547
def bar(self) -> Iterator[int]:
546548
return iterator
547549

548550
foo = Foo()
549-
spy = mocker.spy(foo, "bar")
551+
spy = mocker.spy(foo, "bar", duplicate_iterators=True)
550552
result = list(foo.bar())
551553

552554
assert result == [0, 1, 2]
@@ -558,16 +560,35 @@ def bar(self) -> Iterator[int]:
558560
assert isinstance(return_value, Iterator)
559561

560562

563+
@pytest.mark.parametrize("iterator", [(i for i in range(3)), iter([0, 1, 2])])
564+
def test_spy_return_iter_is_not_set_when_disabled(
565+
mocker: MockerFixture, iterator: Iterator[int]
566+
) -> None:
567+
class Foo:
568+
def bar(self) -> Iterator[int]:
569+
return iterator
570+
571+
foo = Foo()
572+
spy = mocker.spy(foo, "bar", duplicate_iterators=False)
573+
result = list(foo.bar())
574+
575+
assert result == [0, 1, 2]
576+
assert spy.spy_return is not None
577+
assert spy.spy_return_iter is None
578+
[return_value] = spy.spy_return_list
579+
assert isinstance(return_value, Iterator)
580+
581+
561582
@pytest.mark.parametrize("iterable", [(0, 1, 2), [0, 1, 2], range(3)])
562-
def test_spy_return_iter_ignore_plain_iterable(
583+
def test_spy_return_iter_ignores_plain_iterable(
563584
mocker: MockerFixture, iterable: Iterable[int]
564585
) -> None:
565586
class Foo:
566587
def bar(self) -> Iterable[int]:
567588
return iterable
568589

569590
foo = Foo()
570-
spy = mocker.spy(foo, "bar")
591+
spy = mocker.spy(foo, "bar", duplicate_iterators=True)
571592
result = foo.bar()
572593

573594
assert result == iterable
@@ -587,7 +608,7 @@ def bar(self) -> Any:
587608
return self.iterables.pop(0)
588609

589610
foo = Foo()
590-
spy = mocker.spy(foo, "bar")
611+
spy = mocker.spy(foo, "bar", duplicate_iterators=True)
591612
result_iterator = list(foo.bar())
592613

593614
assert result_iterator == [0, 1, 2]

0 commit comments

Comments
 (0)