diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3cfe123..b4757e4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -58,7 +58,7 @@ jobs: git push origin v${{ github.event.inputs.version }} - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5682d4..46a5d5b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: path: dist - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} allow-prereleases: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f416f4b..05bdbb1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,13 +9,13 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.11 + rev: v0.13.0 hooks: - id: ruff args: ["--fix"] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.17.1 + rev: v1.18.1 hooks: - id: mypy files: ^(src|tests) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0cde5ad..987407c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Releases ======== +3.15.1 +------ + +*2025-09-16* + +* `#529 `_: Fixed ``itertools._tee object has no attribute error`` -- now ``duplicate_iterators=True`` must be passed to ``mocker.spy`` to duplicate iterators. + 3.15.0 ------ diff --git a/docs/usage.rst b/docs/usage.rst index 587fcb3..80d7c66 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -78,10 +78,10 @@ also tracks function/method calls, return values and exceptions raised. 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 two extra attributes: +In addition, spy objects contain 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. Uses `tee `__) to duplicate the iterator. +* ``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 `__) to duplicate the iterator. * ``spy_return_list``: contains a list of all returned values of the spied function (new in ``3.13``). * ``spy_exception``: contain the last exception value raised by the spied function/method when it was last called, or ``None`` if no exception was raised. diff --git a/scripts/gen-release-notes.py b/scripts/gen-release-notes.py index 9e6c4bb..635d81f 100644 --- a/scripts/gen-release-notes.py +++ b/scripts/gen-release-notes.py @@ -18,7 +18,7 @@ capture = False for line in rst_text.splitlines(): # Only start capturing after the latest release section. - if line.startswith("-------"): + if line.startswith("----"): capture = not capture if not capture: # We only need to capture the latest release, so stop. diff --git a/src/pytest_mock/_util.py b/src/pytest_mock/_util.py index ad830ca..d3a732a 100644 --- a/src/pytest_mock/_util.py +++ b/src/pytest_mock/_util.py @@ -15,7 +15,7 @@ def get_mock_module(config): config.getini("mock_use_standalone_module") ) if use_standalone_module: - from unittest import mock + import mock _mock_module = mock else: diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index f4dbfc3..ef99612 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -157,13 +157,16 @@ def stop(self, mock: unittest.mock.MagicMock) -> None: """ self._mock_cache.remove(mock) - def spy(self, obj: object, name: str) -> MockType: + def spy( + self, obj: object, name: str, duplicate_iterators: bool = False + ) -> MockType: """ 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. :param obj: An object. :param name: A method in object. + :param duplicate_iterators: Whether to keep a copy of the returned iterator in `spy_return_iter`. :return: Spy object. """ method = getattr(obj, name) @@ -177,7 +180,7 @@ def wrapper(*args, **kwargs): spy_obj.spy_exception = e raise else: - if isinstance(r, Iterator): + if duplicate_iterators and isinstance(r, Iterator): r, duplicated_iterator = itertools.tee(r, 2) spy_obj.spy_return_iter = duplicated_iterator else: diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index e9b06c8..79d59e1 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -540,13 +540,15 @@ def __call__(self, x): @pytest.mark.parametrize("iterator", [(i for i in range(3)), iter([0, 1, 2])]) -def test_spy_return_iter(mocker: MockerFixture, iterator: Iterator[int]) -> None: +def test_spy_return_iter_duplicates_iterator_when_enabled( + mocker: MockerFixture, iterator: Iterator[int] +) -> None: class Foo: def bar(self) -> Iterator[int]: return iterator foo = Foo() - spy = mocker.spy(foo, "bar") + spy = mocker.spy(foo, "bar", duplicate_iterators=True) result = list(foo.bar()) assert result == [0, 1, 2] @@ -558,8 +560,27 @@ def bar(self) -> Iterator[int]: assert isinstance(return_value, Iterator) +@pytest.mark.parametrize("iterator", [(i for i in range(3)), iter([0, 1, 2])]) +def test_spy_return_iter_is_not_set_when_disabled( + mocker: MockerFixture, iterator: Iterator[int] +) -> None: + class Foo: + def bar(self) -> Iterator[int]: + return iterator + + foo = Foo() + spy = mocker.spy(foo, "bar", duplicate_iterators=False) + result = list(foo.bar()) + + assert result == [0, 1, 2] + assert spy.spy_return is not None + assert spy.spy_return_iter is None + [return_value] = spy.spy_return_list + assert isinstance(return_value, Iterator) + + @pytest.mark.parametrize("iterable", [(0, 1, 2), [0, 1, 2], range(3)]) -def test_spy_return_iter_ignore_plain_iterable( +def test_spy_return_iter_ignores_plain_iterable( mocker: MockerFixture, iterable: Iterable[int] ) -> None: class Foo: @@ -567,7 +588,7 @@ def bar(self) -> Iterable[int]: return iterable foo = Foo() - spy = mocker.spy(foo, "bar") + spy = mocker.spy(foo, "bar", duplicate_iterators=True) result = foo.bar() assert result == iterable @@ -587,7 +608,7 @@ def bar(self) -> Any: return self.iterables.pop(0) foo = Foo() - spy = mocker.spy(foo, "bar") + spy = mocker.spy(foo, "bar", duplicate_iterators=True) result_iterator = list(foo.bar()) assert result_iterator == [0, 1, 2] @@ -643,7 +664,7 @@ def assert_argument_introspection(left: Any, right: Any) -> Generator[None, None expected = "\n ".join(util._compare_eq_iterable(left, right, verbose)) # type:ignore[arg-type] else: expected = "\n ".join( - util._compare_eq_iterable(left, right, lambda t, *_: t, verbose) + util._compare_eq_iterable(left, right, lambda t, *_, **__: t, verbose) # type:ignore[arg-type] ) assert expected in str(e) else: diff --git a/tox.ini b/tox.ini index 92bcba7..20273f7 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,8 @@ envlist = py{39,310,311,312,313,314}, norewrite, pytest6 [testenv] deps = coverage + # Used for standalone mock support. + mock pytest-asyncio pytest6: pytest==6.2.5 commands =