From 65b40f421db7ff67270123168de3fde5f84b65f8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 20:53:48 +0000 Subject: [PATCH 01/28] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.9.1 → 23.10.1](https://github.com/psf/black/compare/23.9.1...23.10.1) - [github.com/pre-commit/mirrors-mypy: v1.6.0 → v1.6.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.0...v1.6.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9fd5499..f481c14 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ exclude: '^($|.*\.bin)' repos: - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black args: [--safe, --quiet] @@ -24,7 +24,7 @@ repos: - id: reorder-python-imports args: ['--application-directories=.:src'] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.0 # NOTE: keep this in sync with tox.ini + rev: v1.6.1 # NOTE: keep this in sync with tox.ini hooks: - id: mypy files: ^(src|tests) From 1ff80b2294f2cfabae97a56f8838952fa507a6b7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:01:04 +0000 Subject: [PATCH 02/28] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0) - [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f481c14..37d26ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ exclude: '^($|.*\.bin)' repos: - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black args: [--safe, --quiet] @@ -24,7 +24,7 @@ repos: - id: reorder-python-imports args: ['--application-directories=.:src'] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 # NOTE: keep this in sync with tox.ini + rev: v1.7.0 # NOTE: keep this in sync with tox.ini hooks: - id: mypy files: ^(src|tests) From 4031b63f6f71a08eeafc3b828ca7513dbf8db2af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 20:21:26 +0000 Subject: [PATCH 03/28] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.7.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 37d26ce..9575feb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: - id: reorder-python-imports args: ['--application-directories=.:src'] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.0 # NOTE: keep this in sync with tox.ini + rev: v1.7.1 # NOTE: keep this in sync with tox.ini hooks: - id: mypy files: ^(src|tests) From 5922e0c5b90d7c0c51c949b0c647c081468cbcae Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Nov 2023 14:17:06 -0300 Subject: [PATCH 04/28] Remove obsolete comment --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9575feb..fc8f1c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: - id: reorder-python-imports args: ['--application-directories=.:src'] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 # NOTE: keep this in sync with tox.ini + rev: v1.7.1 hooks: - id: mypy files: ^(src|tests) From 7f9c13147aad9d6fa5c4f59dbd5b37324ffce417 Mon Sep 17 00:00:00 2001 From: Lauren Campbell Date: Wed, 6 Dec 2023 18:30:32 -0500 Subject: [PATCH 05/28] autospec for spy: remove if else which was in place for python < 3.7 (#399) The if/else for the spy feature was there to get around a bug in python that was fixed in 3.7. Now that we only support >= 3.8 this check is no longer needed. This should not really affect users, but if we learn it does, we can add a CHANGELOG later (as discussed in #399). --- src/pytest_mock/plugin.py | 10 ++-------- tests/test_pytest_mock.py | 37 +++++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index 673f98b..a917747 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -137,14 +137,6 @@ def spy(self, obj: object, name: str) -> MockType: :return: Spy object. """ method = getattr(obj, name) - if inspect.isclass(obj) and isinstance( - inspect.getattr_static(obj, name), (classmethod, staticmethod) - ): - # Can't use autospec classmethod or staticmethod objects before 3.7 - # see: https://bugs.python.org/issue23078 - autospec = False - else: - autospec = inspect.ismethod(method) or inspect.isfunction(method) def wrapper(*args, **kwargs): spy_obj.spy_return = None @@ -175,6 +167,8 @@ async def async_wrapper(*args, **kwargs): else: wrapped = functools.update_wrapper(wrapper, method) + autospec = inspect.ismethod(method) or inspect.isfunction(method) + spy_obj = self.patch.object(obj, name, side_effect=wrapped, autospec=autospec) spy_obj.spy_return = None spy_obj.spy_exception = None diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index fdeed0c..3ee00da 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -314,17 +314,37 @@ def bar(self, arg): assert str(spy.spy_exception) == f"Error with {v}" -def test_instance_method_spy_autospec_true(mocker: MockerFixture) -> None: +def test_instance_class_static_method_spy_autospec_true(mocker: MockerFixture) -> None: class Foo: def bar(self, arg): return arg * 2 + @classmethod + def baz(cls, arg): + return arg * 2 + + @staticmethod + def qux(arg): + return arg * 2 + foo = Foo() - spy = mocker.spy(foo, "bar") + instance_method_spy = mocker.spy(foo, "bar") with pytest.raises( AttributeError, match="'function' object has no attribute 'fake_assert_method'" ): - spy.fake_assert_method(arg=5) + instance_method_spy.fake_assert_method(arg=5) + + class_method_spy = mocker.spy(Foo, "baz") + with pytest.raises( + AttributeError, match="Mock object has no attribute 'fake_assert_method'" + ): + class_method_spy.fake_assert_method(arg=5) + + static_method_spy = mocker.spy(Foo, "qux") + with pytest.raises( + AttributeError, match="Mock object has no attribute 'fake_assert_method'" + ): + static_method_spy.fake_assert_method(arg=5) def test_spy_reset(mocker: MockerFixture) -> None: @@ -404,17 +424,6 @@ def bar(cls, arg): assert spy.spy_return == 20 -@skip_pypy -def test_class_method_spy_autospec_false(mocker: MockerFixture) -> None: - class Foo: - @classmethod - def bar(cls, arg): - return arg * 2 - - spy = mocker.spy(Foo, "bar") - spy.fake_assert_method() - - @skip_pypy def test_class_method_subclass_spy(mocker: MockerFixture) -> None: class Base: From 8480bb6d0500f933be039cfec65e04157e6ecffe Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 19 Dec 2023 08:24:23 -0300 Subject: [PATCH 06/28] Fix tests for Python 3.11 and 3.12 Fixes #401. --- tests/test_pytest_mock.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index 3ee00da..7acb361 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -246,9 +246,8 @@ def __test_failure_message(self, mocker: MockerFixture, **kwargs: Any) -> None: msg = "Expected call: {0}()\nNot called" expected_message = msg.format(expected_name) stub = mocker.stub(**kwargs) - with pytest.raises(AssertionError) as exc_info: + with pytest.raises(AssertionError, match=re.escape(expected_message)) as exc_info: stub.assert_called_with() - assert str(exc_info.value) == expected_message def test_failure_message_with_no_name(self, mocker: MagicMock) -> None: self.__test_failure_message(mocker) From c596504e062be06475b03122c9c0cc732ae87840 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:24:38 +0000 Subject: [PATCH 07/28] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_pytest_mock.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index 7acb361..c185f2a 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -246,7 +246,9 @@ def __test_failure_message(self, mocker: MockerFixture, **kwargs: Any) -> None: msg = "Expected call: {0}()\nNot called" expected_message = msg.format(expected_name) stub = mocker.stub(**kwargs) - with pytest.raises(AssertionError, match=re.escape(expected_message)) as exc_info: + with pytest.raises( + AssertionError, match=re.escape(expected_message) + ) as exc_info: stub.assert_called_with() def test_failure_message_with_no_name(self, mocker: MagicMock) -> None: From 6da5b0506d6378a8dbe5ae314d5134e6868aeabd Mon Sep 17 00:00:00 2001 From: danigm Date: Wed, 20 Dec 2023 16:02:13 +0100 Subject: [PATCH 08/28] Update expected message to match python 3.11.7 (#404) https://github.com/python/cpython/issues/111019 Fixes #401. Closes #403. --- tests/test_pytest_mock.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index c185f2a..01534a4 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -25,6 +25,8 @@ # Python 3.8 changed the output formatting (bpo-35500), which has been ported to mock 3.0 NEW_FORMATTING = sys.version_info >= (3, 8) +# Python 3.11.7 changed the output formatting, https://github.com/python/cpython/issues/111019 +NEWEST_FORMATTING = sys.version_info >= (3, 11, 7) if sys.version_info[:2] >= (3, 8): from unittest.mock import AsyncMock @@ -240,7 +242,9 @@ def test_repr_with_name(self, mocker: MockerFixture) -> None: def __test_failure_message(self, mocker: MockerFixture, **kwargs: Any) -> None: expected_name = kwargs.get("name") or "mock" - if NEW_FORMATTING: + if NEWEST_FORMATTING: + msg = "expected call not found.\nExpected: {0}()\n Actual: not called." + elif NEW_FORMATTING: msg = "expected call not found.\nExpected: {0}()\nActual: not called." else: msg = "Expected call: {0}()\nNot called" From ba34960598e203a5ca2ddb8a235d7d1033949842 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 29 Jan 2024 18:09:53 -0300 Subject: [PATCH 09/28] Use ruff in place of black and reorder-python-imports Unfortunately black and reorder-python-imports are no longer compatible between each other: https://github.com/asottile/reorder-python-imports/issues/367 https://github.com/asottile/reorder-python-imports/issues/366 https://github.com/psf/black/issues/4175 Take this opportunity to try out ruff. --- .pre-commit-config.yaml | 14 +++++--------- pyproject.toml | 8 ++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc8f1c7..3b56a69 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,5 @@ exclude: '^($|.*\.bin)' repos: - - repo: https://github.com/psf/black - rev: 23.11.0 - hooks: - - id: black - args: [--safe, --quiet] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: @@ -18,11 +13,12 @@ repos: files: ^(CHANGELOG.rst|README.rst|HOWTORELEASE.rst|changelog/.*)$ language: python additional_dependencies: [pygments, restructuredtext_lint] - - repo: https://github.com/asottile/reorder-python-imports - rev: v3.12.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.14 hooks: - - id: reorder-python-imports - args: ['--application-directories=.:src'] + - id: ruff + args: [ --fix ] + - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.7.1 hooks: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f9ef96d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = [ + "setuptools", + "setuptools-scm[toml]", +] +build-backend = "setuptools.build_meta" + +[tool.ruff] From b8522e73a85441cf4c02c39038a88ac0bab57504 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 29 Jan 2024 18:19:51 -0300 Subject: [PATCH 10/28] Run pre-commit hooks in all files --- src/pytest_mock/plugin.py | 32 +++++++++++++++----------------- tests/test_pytest_mock.py | 13 ++++++++----- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index a917747..d1912b8 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -52,9 +52,7 @@ class MockerFixture: def __init__(self, config: Any) -> None: 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_and_mocks, mock_module - ) # type: MockerFixture._Patcher + self.patch = self._Patcher(self._patches_and_mocks, mock_module) # type: MockerFixture._Patcher # aliases for convenience self.Mock = mock_module.Mock self.MagicMock = mock_module.MagicMock @@ -250,7 +248,7 @@ def object( spec_set: Optional[object] = None, autospec: Optional[object] = None, new_callable: object = None, - **kwargs: Any + **kwargs: Any, ) -> MockType: """API to mock.patch.object""" if new is self.DEFAULT: @@ -266,7 +264,7 @@ def object( spec_set=spec_set, autospec=autospec, new_callable=new_callable, - **kwargs + **kwargs, ) def context_manager( @@ -279,7 +277,7 @@ def context_manager( spec_set: Optional[builtins.object] = None, autospec: Optional[builtins.object] = None, new_callable: builtins.object = None, - **kwargs: Any + **kwargs: Any, ) -> MockType: """This is equivalent to mock.patch.object except that the returned mock does not issue a warning when used as a context manager.""" @@ -296,7 +294,7 @@ def context_manager( spec_set=spec_set, autospec=autospec, new_callable=new_callable, - **kwargs + **kwargs, ) def multiple( @@ -307,7 +305,7 @@ def multiple( spec_set: Optional[builtins.object] = None, autospec: Optional[builtins.object] = None, new_callable: Optional[builtins.object] = None, - **kwargs: Any + **kwargs: Any, ) -> Dict[str, MockType]: """API to mock.patch.multiple""" return self._start_patch( @@ -319,7 +317,7 @@ def multiple( spec_set=spec_set, autospec=autospec, new_callable=new_callable, - **kwargs + **kwargs, ) def dict( @@ -327,7 +325,7 @@ def dict( in_dict: Union[Mapping[Any, Any], str], values: Union[Mapping[Any, Any], Iterable[Tuple[Any, Any]]] = (), clear: bool = False, - **kwargs: Any + **kwargs: Any, ) -> Any: """API to mock.patch.dict""" return self._start_patch( @@ -336,7 +334,7 @@ def dict( in_dict, values=values, clear=clear, - **kwargs + **kwargs, ) @overload @@ -349,7 +347,7 @@ def __call__( spec_set: Optional[builtins.object] = ..., autospec: Optional[builtins.object] = ..., new_callable: None = ..., - **kwargs: Any + **kwargs: Any, ) -> MockType: ... @@ -363,7 +361,7 @@ def __call__( spec_set: Optional[builtins.object] = ..., autospec: Optional[builtins.object] = ..., new_callable: None = ..., - **kwargs: Any + **kwargs: Any, ) -> _T: ... @@ -377,7 +375,7 @@ def __call__( spec_set: Optional[builtins.object], autospec: Optional[builtins.object], new_callable: Callable[[], _T], - **kwargs: Any + **kwargs: Any, ) -> _T: ... @@ -392,7 +390,7 @@ def __call__( autospec: Optional[builtins.object] = ..., *, new_callable: Callable[[], _T], - **kwargs: Any + **kwargs: Any, ) -> _T: ... @@ -405,7 +403,7 @@ def __call__( spec_set: Optional[builtins.object] = None, autospec: Optional[builtins.object] = None, new_callable: Optional[Callable[[], Any]] = None, - **kwargs: Any + **kwargs: Any, ) -> Any: """API to mock.patch""" if new is self.DEFAULT: @@ -420,7 +418,7 @@ def __call__( spec_set=spec_set, autospec=autospec, new_callable=new_callable, - **kwargs + **kwargs, ) diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index 01534a4..8bcdcb7 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -250,9 +250,7 @@ def __test_failure_message(self, mocker: MockerFixture, **kwargs: Any) -> None: msg = "Expected call: {0}()\nNot called" expected_message = msg.format(expected_name) stub = mocker.stub(**kwargs) - with pytest.raises( - AssertionError, match=re.escape(expected_message) - ) as exc_info: + with pytest.raises(AssertionError, match=re.escape(expected_message)): stub.assert_called_with() def test_failure_message_with_no_name(self, mocker: MagicMock) -> None: @@ -561,7 +559,12 @@ def assert_argument_introspection(left: Any, right: Any) -> Generator[None, None # NOTE: we assert with either verbose or not, depending on how our own # test was run by examining sys.argv verbose = any(a.startswith("-v") for a in sys.argv) - expected = "\n ".join(util._compare_eq_iterable(left, right, verbose)) + if int(pytest.version_tuple[0]) < 8: + 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) + ) assert expected in str(e) else: raise AssertionError("DID NOT RAISE") @@ -1042,7 +1045,7 @@ def my_func(): with dummy_module.MyContext() as v: return v - m = mocker.patch.object(dummy_module, "MyContext") + mocker.patch.object(dummy_module, "MyContext") assert isinstance(my_func(), mocker.MagicMock) From c23528a7cdb9a845783c254398992c665eec72c5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 29 Jan 2024 18:21:12 -0300 Subject: [PATCH 11/28] Run pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b56a69..c021ee7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: [ --fix ] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.8.0 hooks: - id: mypy files: ^(src|tests) From 875f5aa560679b6b035475a82867595b3b98dc5e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 29 Jan 2024 18:42:37 -0300 Subject: [PATCH 12/28] Require pytest >=6.2.5 Also add a tox environment testing in that specific version to ensure compatibility with 6.2.5. --- .github/workflows/test.yml | 3 +++ CHANGELOG.rst | 5 +++++ setup.py | 2 +- tests/test_pytest_mock.py | 10 +++++----- tox.ini | 3 ++- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7eef5be..7bc8f62 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,9 @@ jobs: os: [ubuntu-latest, windows-latest] tox_env: ["py"] include: + - python: "3.10" + os: ubuntu-latest + tox_env: "pytest6" - python: "3.12" os: ubuntu-latest tox_env: "norewrite" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 052f803..c576196 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Releases ======== +UNRELEASED +---------- + +* ``pytest-mock`` now requires ``pytest>=6.2.5``. + 3.12.0 (2023-10-19) ------------------- diff --git a/setup.py b/setup.py index 748f804..5e89ed3 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ "pytest_mock": ["py.typed"], }, python_requires=">=3.8", - install_requires=["pytest>=5.0"], + install_requires=["pytest>=6.2.5"], use_scm_version={"write_to": "src/pytest_mock/_version.py"}, setup_requires=["setuptools_scm"], url="https://github.com/pytest-dev/pytest-mock/", diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index 8bcdcb7..c7df725 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -559,7 +559,7 @@ def assert_argument_introspection(left: Any, right: Any) -> Generator[None, None # NOTE: we assert with either verbose or not, depending on how our own # test was run by examining sys.argv verbose = any(a.startswith("-v") for a in sys.argv) - if int(pytest.version_tuple[0]) < 8: + if int(pytest.__version__.split(".")[0]) < 8: expected = "\n ".join(util._compare_eq_iterable(left, right, verbose)) # type:ignore[arg-type] else: expected = "\n ".join( @@ -888,12 +888,12 @@ def test(mocker): "*Args:", "*assert ('fo',) == ('',)", "*At index 0 diff: 'fo' != ''*", - "*Use -v to get more diff*", + "*Use -v to*", "*Kwargs:*", "*assert {} == {'bar': 4}*", "*Right contains* more item*", "*{'bar': 4}*", - "*Use -v to get more diff*", + "*Use -v to*", ] result.stdout.fnmatch_lines(expected_lines) @@ -929,12 +929,12 @@ async def test(mocker): "*Args:", "*assert ('fo',) == ('',)", "*At index 0 diff: 'fo' != ''*", - "*Use -v to get more diff*", + "*Use -v to*", "*Kwargs:*", "*assert {} == {'bar': 4}*", "*Right contains* more item*", "*{'bar': 4}*", - "*Use -v to get more diff*", + "*Use -v to*", ] result.stdout.fnmatch_lines(expected_lines) diff --git a/tox.ini b/tox.ini index a2b3e21..8082b5a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,13 @@ [tox] minversion = 3.5.3 -envlist = py{38,39,310,311,312}, norewrite +envlist = py{38,39,310,311,312}, norewrite, pytest6 [testenv] deps = coverage mock pytest-asyncio + pytest6: pytest==6.2.5 commands = coverage run --append --source={envsitepackagesdir}/pytest_mock -m pytest tests --color=yes From 1662868e4f2be94c9bddb79a8f9b1fb8b64f6ea5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 31 Jan 2024 10:23:43 -0300 Subject: [PATCH 13/28] Fix ruff isort configuration In addition, remove other hooks which are not needed now as they are handled by ruff. --- .pre-commit-config.yaml | 7 +------ pyproject.toml | 7 ++++++- src/pytest_mock/__init__.py | 4 ++-- src/pytest_mock/plugin.py | 4 ++-- tests/test_pytest_mock.py | 1 - 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c021ee7..99b4f8d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,5 @@ exclude: '^($|.*\.bin)' repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - repo: local hooks: - id: rst @@ -17,7 +12,7 @@ repos: rev: v0.1.14 hooks: - id: ruff - args: [ --fix ] + args: ["--fix"] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.8.0 diff --git a/pyproject.toml b/pyproject.toml index f9ef96d..8e6c7b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,4 +5,9 @@ requires = [ ] build-backend = "setuptools.build_meta" -[tool.ruff] +[tool.ruff.lint] +extend-select = ["I001"] + +[tool.ruff.lint.isort] +force-single-line = true +known-third-party = ["src"] diff --git a/src/pytest_mock/__init__.py b/src/pytest_mock/__init__.py index 0afa47c..127e5c8 100644 --- a/src/pytest_mock/__init__.py +++ b/src/pytest_mock/__init__.py @@ -1,11 +1,11 @@ +from pytest_mock.plugin import MockerFixture +from pytest_mock.plugin import PytestMockWarning from pytest_mock.plugin import class_mocker from pytest_mock.plugin import mocker -from pytest_mock.plugin import MockerFixture from pytest_mock.plugin import module_mocker from pytest_mock.plugin import package_mocker from pytest_mock.plugin import pytest_addoption from pytest_mock.plugin import pytest_configure -from pytest_mock.plugin import PytestMockWarning from pytest_mock.plugin import session_mocker MockFixture = MockerFixture # backward-compatibility only (#204) diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index d1912b8..cd7d762 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -7,18 +7,18 @@ import warnings from typing import Any from typing import Callable -from typing import cast from typing import Dict from typing import Generator from typing import Iterable from typing import List from typing import Mapping from typing import Optional -from typing import overload from typing import Tuple from typing import Type from typing import TypeVar from typing import Union +from typing import cast +from typing import overload import pytest diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index c7df725..d56d444 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -12,7 +12,6 @@ from unittest.mock import MagicMock import pytest - from pytest_mock import MockerFixture from pytest_mock import PytestMockWarning From 4ae94d7f006fc123b9f3381adc9a7668dbeaaf00 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:29:39 +0000 Subject: [PATCH 14/28] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.14 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.14...v0.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99b4f8d..e4b2402 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.14 + rev: v0.2.0 hooks: - id: ruff args: ["--fix"] From b7fb4ac37e4453670b3cadfab1ae808516a28672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20K=C3=B6ppe?= Date: Mon, 12 Feb 2024 06:03:43 -0800 Subject: [PATCH 15/28] Migrate to pyproject.toml Co-authored-by: Bruno Oliveira --- CHANGELOG.rst | 3 +++ pyproject.toml | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 47 -------------------------------------------- 3 files changed, 56 insertions(+), 47 deletions(-) delete mode 100644 setup.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c576196..cccef3f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ UNRELEASED ---------- * ``pytest-mock`` now requires ``pytest>=6.2.5``. +* `#410 `_: pytest-mock's ``setup.py`` file is removed. + If you relied on this file, e.g. to install pytest using ``setup.py install``, + please see `Why you shouldn't invoke setup.py directly `_ for alternatives. 3.12.0 (2023-10-19) ------------------- diff --git a/pyproject.toml b/pyproject.toml index 8e6c7b0..9865b37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,59 @@ requires = [ ] build-backend = "setuptools.build_meta" +[project] +name = "pytest-mock" +description = "Thin-wrapper around the mock package for easier use with pytest" +authors = [ + {name = "Bruno Oliveira", email = "nicoddemus@gmail.com"}, +] +dependencies = [ + "pytest>=6.2.5", +] +dynamic = ["version"] +requires-python = ">=3.8" +readme = "README.rst" +license = {text = "MIT"} +keywords = ["pytest", "mock"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Testing", +] + +[project.urls] +Homepage = "https://github.com/pytest-dev/pytest-mock/" +Documentation = "https://pytest-mock.readthedocs.io/en/latest/" +Changelog = "https://pytest-mock.readthedocs.io/en/latest/changelog.html" +Source = "https://github.com/pytest-dev/pytest-mock/" +Tracker = "https://github.com/pytest-dev/pytest-mock/issues" + +[project.optional-dependencies] +dev = [ + "pre-commit", + "pytest-asyncio", + "tox", +] + +[project.entry-points.pytest11] +pytest_mock = "pytest_mock" + +[tool.setuptools.package-data] +pytest_mock = ["py.typed"] + +[tool.setuptools_scm] +write_to = "src/pytest_mock/_version.py" + [tool.ruff.lint] extend-select = ["I001"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 5e89ed3..0000000 --- a/setup.py +++ /dev/null @@ -1,47 +0,0 @@ -from setuptools import find_packages -from setuptools import setup - -setup( - name="pytest-mock", - entry_points={"pytest11": ["pytest_mock = pytest_mock"]}, - packages=find_packages(where="src"), - package_dir={"": "src"}, - platforms="any", - package_data={ - "pytest_mock": ["py.typed"], - }, - python_requires=">=3.8", - install_requires=["pytest>=6.2.5"], - use_scm_version={"write_to": "src/pytest_mock/_version.py"}, - setup_requires=["setuptools_scm"], - url="https://github.com/pytest-dev/pytest-mock/", - license="MIT", - author="Bruno Oliveira", - author_email="nicoddemus@gmail.com", - description="Thin-wrapper around the mock package for easier use with pytest", - long_description=open("README.rst", encoding="utf-8").read(), - long_description_content_type="text/x-rst", - keywords="pytest mock", - extras_require={"dev": ["pre-commit", "tox", "pytest-asyncio"]}, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Framework :: Pytest", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Software Development :: Testing", - ], - project_urls={ - "Documentation": "https://pytest-mock.readthedocs.io/en/latest/", - "Changelog": "https://pytest-mock.readthedocs.io/en/latest/changelog.html", - "Source": "https://github.com/pytest-dev/pytest-mock/", - "Tracker": "https://github.com/pytest-dev/pytest-mock/issues", - }, -) From 6f88d760aac0d9446b06d08679e992dc6146973e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 13:28:09 -0300 Subject: [PATCH 16/28] [pre-commit.ci] pre-commit autoupdate (#411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4b2402..20eaaef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 + rev: v0.2.1 hooks: - id: ruff args: ["--fix"] From 882d2522777a2cad7f2f67aab64e5bbdde6e06db Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 16 Feb 2024 06:49:14 -0300 Subject: [PATCH 17/28] Create SECURITY.md --- SECURITY.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a5dd8a4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,8 @@ +## Security contact information + +This project is not used in production, so there should be no security concerns. + +However as part of the Tidelift offering, the policy to report a security vulnerability is to use the +[Tidelift security contact](https://tidelift.com/security). + +Tidelift will coordinate the fix and disclosure. From e023728d398bcabd24a81d1b0249aabb0e2a460f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 21:33:34 -0300 Subject: [PATCH 18/28] [pre-commit.ci] pre-commit autoupdate (#412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.1 → v0.2.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.1...v0.2.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 20eaaef..200c769 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.1 + rev: v0.2.2 hooks: - id: ruff args: ["--fix"] From fdc4c7ffd6546c86c848bab37bf69a17bdcc2e09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 06:56:19 -0300 Subject: [PATCH 19/28] [pre-commit.ci] pre-commit autoupdate (#413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.2 → v0.3.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.2...v0.3.2) - [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- scripts/gen-release-notes.py | 1 + src/pytest_mock/plugin.py | 12 ++++-------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 200c769..c256638 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.2.2 + rev: v0.3.2 hooks: - id: ruff args: ["--fix"] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy files: ^(src|tests) diff --git a/scripts/gen-release-notes.py b/scripts/gen-release-notes.py index a41eaf2..9e6c4bb 100644 --- a/scripts/gen-release-notes.py +++ b/scripts/gen-release-notes.py @@ -6,6 +6,7 @@ 3. Writes to ``scripts/latest-release-notes.md``, which can be used with https://github.com/softprops/action-gh-release. """ + from pathlib import Path import pypandoc diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index cd7d762..b76952d 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -348,8 +348,7 @@ def __call__( autospec: Optional[builtins.object] = ..., new_callable: None = ..., **kwargs: Any, - ) -> MockType: - ... + ) -> MockType: ... @overload def __call__( @@ -362,8 +361,7 @@ def __call__( autospec: Optional[builtins.object] = ..., new_callable: None = ..., **kwargs: Any, - ) -> _T: - ... + ) -> _T: ... @overload def __call__( @@ -376,8 +374,7 @@ def __call__( autospec: Optional[builtins.object], new_callable: Callable[[], _T], **kwargs: Any, - ) -> _T: - ... + ) -> _T: ... @overload def __call__( @@ -391,8 +388,7 @@ def __call__( *, new_callable: Callable[[], _T], **kwargs: Any, - ) -> _T: - ... + ) -> _T: ... def __call__( self, From dc28a0ec7b66372fbc6e0cf1bbe443ce7ca465cd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:07:42 -0300 Subject: [PATCH 20/28] [pre-commit.ci] pre-commit autoupdate (#416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.2 → v0.3.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.2...v0.3.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c256638..9b4c9dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: language: python additional_dependencies: [pygments, restructuredtext_lint] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.2 + rev: v0.3.3 hooks: - id: ruff args: ["--fix"] From 6d5d6dc2274cea96b6919a4a2a6dc7a3394ef11d Mon Sep 17 00:00:00 2001 From: frank-lenormand <131158759+frank-lenormand@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:54:35 +0100 Subject: [PATCH 21/28] Implement `spy_return_list` (#417) Fix #378 Co-authored-by: Bruno Oliveira --- CHANGELOG.rst | 1 + docs/usage.rst | 3 +- src/pytest_mock/plugin.py | 91 +++++++++++++++++++++++++++++---------- tests/test_pytest_mock.py | 21 +++++++++ 4 files changed, 93 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cccef3f..1c0b69c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Releases UNRELEASED ---------- +* `#417 `_: ``spy`` now has ``spy_return_list``, which is a list containing all the values returned by the spied function. * ``pytest-mock`` now requires ``pytest>=6.2.5``. * `#410 `_: pytest-mock's ``setup.py`` file is removed. If you relied on this file, e.g. to install pytest using ``setup.py install``, diff --git a/docs/usage.rst b/docs/usage.rst index 1b29d45..c576dd0 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -79,7 +79,8 @@ are available (like ``assert_called_once_with`` or ``call_count`` in the example In addition, spy objects contain two extra attributes: -* ``spy_return``: contains the returned value of the spied function. +* ``spy_return``: contains the last returned value of the spied function. +* ``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/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index b76952d..354752b 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -5,11 +5,14 @@ import sys import unittest.mock import warnings +from dataclasses import dataclass +from dataclasses import field from typing import Any from typing import Callable from typing import Dict from typing import Generator from typing import Iterable +from typing import Iterator from typing import List from typing import Mapping from typing import Optional @@ -43,6 +46,45 @@ class PytestMockWarning(UserWarning): """Base class for all warnings emitted by pytest-mock.""" +@dataclass +class MockCacheItem: + mock: MockType + patch: Optional[Any] = None + + +@dataclass +class MockCache: + cache: List[MockCacheItem] = field(default_factory=list) + + def find(self, mock: MockType) -> MockCacheItem: + the_mock = next( + (mock_item for mock_item in self.cache if mock_item.mock == mock), None + ) + if the_mock is None: + raise ValueError("This mock object is not registered") + return the_mock + + def add(self, mock: MockType, **kwargs: Any) -> MockCacheItem: + try: + return self.find(mock) + except ValueError: + self.cache.append(MockCacheItem(mock=mock, **kwargs)) + return self.cache[-1] + + def remove(self, mock: MockType) -> None: + mock_item = self.find(mock) + self.cache.remove(mock_item) + + def clear(self) -> None: + self.cache.clear() + + def __iter__(self) -> Iterator[MockCacheItem]: + return iter(self.cache) + + def __reversed__(self) -> Iterator[MockCacheItem]: + return reversed(self.cache) + + class MockerFixture: """ Fixture that provides the same interface to functions in the mock module, @@ -50,9 +92,9 @@ class MockerFixture: """ def __init__(self, config: Any) -> None: - self._patches_and_mocks: List[Tuple[Any, unittest.mock.MagicMock]] = [] + self._mock_cache: MockCache = MockCache() self.mock_module = mock_module = get_mock_module(config) - self.patch = self._Patcher(self._patches_and_mocks, mock_module) # type: MockerFixture._Patcher + self.patch = self._Patcher(self._mock_cache, mock_module) # type: MockerFixture._Patcher # aliases for convenience self.Mock = mock_module.Mock self.MagicMock = mock_module.MagicMock @@ -75,7 +117,7 @@ def create_autospec( m: MockType = self.mock_module.create_autospec( spec, spec_set, instance, **kwargs ) - self._patches_and_mocks.append((None, m)) + self._mock_cache.add(m) return m def resetall( @@ -93,37 +135,39 @@ def resetall( else: supports_reset_mock_with_args = (self.Mock,) - for p, m in self._patches_and_mocks: + for mock_item in self._mock_cache: # See issue #237. - if not hasattr(m, "reset_mock"): + if not hasattr(mock_item.mock, "reset_mock"): continue - if isinstance(m, supports_reset_mock_with_args): - m.reset_mock(return_value=return_value, side_effect=side_effect) + # NOTE: The mock may be a dictionary + if hasattr(mock_item.mock, "spy_return_list"): + mock_item.mock.spy_return_list = [] + if isinstance(mock_item.mock, supports_reset_mock_with_args): + mock_item.mock.reset_mock( + return_value=return_value, side_effect=side_effect + ) else: - m.reset_mock() + mock_item.mock.reset_mock() def stopall(self) -> None: """ Stop all patchers started by this fixture. Can be safely called multiple times. """ - for p, m in reversed(self._patches_and_mocks): - if p is not None: - p.stop() - self._patches_and_mocks.clear() + for mock_item in reversed(self._mock_cache): + if mock_item.patch is not None: + mock_item.patch.stop() + self._mock_cache.clear() def stop(self, mock: unittest.mock.MagicMock) -> None: """ Stops a previous patch or spy call by passing the ``MagicMock`` object returned by it. """ - 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") + mock_item = self._mock_cache.find(mock) + if mock_item.patch: + mock_item.patch.stop() + self._mock_cache.remove(mock) def spy(self, obj: object, name: str) -> MockType: """ @@ -146,6 +190,7 @@ def wrapper(*args, **kwargs): raise else: spy_obj.spy_return = r + spy_obj.spy_return_list.append(r) return r async def async_wrapper(*args, **kwargs): @@ -158,6 +203,7 @@ async def async_wrapper(*args, **kwargs): raise else: spy_obj.spy_return = r + spy_obj.spy_return_list.append(r) return r if asyncio.iscoroutinefunction(method): @@ -169,6 +215,7 @@ async def async_wrapper(*args, **kwargs): spy_obj = self.patch.object(obj, name, side_effect=wrapped, autospec=autospec) spy_obj.spy_return = None + spy_obj.spy_return_list = [] spy_obj.spy_exception = None return spy_obj @@ -206,8 +253,8 @@ class _Patcher: DEFAULT = object() - def __init__(self, patches_and_mocks, mock_module): - self.__patches_and_mocks = patches_and_mocks + def __init__(self, mock_cache, mock_module): + self.__mock_cache = mock_cache self.mock_module = mock_module def _start_patch( @@ -219,7 +266,7 @@ def _start_patch( """ p = mock_func(*args, **kwargs) mocked: MockType = p.start() - self.__patches_and_mocks.append((p, mocked)) + self.__mock_cache.add(mock=mocked, patch=p) if hasattr(mocked, "reset_mock"): # check if `mocked` is actually a mock object, as depending on autospec or target # parameters `mocked` can be anything diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index d56d444..1d43d9e 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -279,8 +279,13 @@ def bar(self, arg): assert other.bar(arg=10) == 20 foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined] assert foo.bar.spy_return == 20 # type:ignore[attr-defined] + assert foo.bar.spy_return_list == [20] # type:ignore[attr-defined] spy.assert_called_once_with(arg=10) assert spy.spy_return == 20 + assert foo.bar(arg=11) == 22 + assert foo.bar(arg=12) == 24 + assert spy.spy_return == 24 + assert spy.spy_return_list == [20, 22, 24] # Ref: https://docs.python.org/3/library/exceptions.html#exception-hierarchy @@ -358,10 +363,12 @@ def bar(self, x): spy = mocker.spy(Foo, "bar") assert spy.spy_return is None + assert spy.spy_return_list == [] assert spy.spy_exception is None Foo().bar(10) assert spy.spy_return == 30 + assert spy.spy_return_list == [30] assert spy.spy_exception is None # Testing spy can still be reset (#237). @@ -370,10 +377,12 @@ def bar(self, x): with pytest.raises(ValueError): Foo().bar(0) assert spy.spy_return is None + assert spy.spy_return_list == [] assert str(spy.spy_exception) == "invalid x" Foo().bar(15) assert spy.spy_return == 45 + assert spy.spy_return_list == [45] assert spy.spy_exception is None @@ -409,6 +418,7 @@ class Foo(Base): calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)] assert spy.call_args_list == calls assert spy.spy_return == 20 + assert spy.spy_return_list == [20, 20] @skip_pypy @@ -422,8 +432,10 @@ def bar(cls, arg): assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined] assert Foo.bar.spy_return == 20 # type:ignore[attr-defined] + assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined] spy.assert_called_once_with(arg=10) assert spy.spy_return == 20 + assert spy.spy_return_list == [20] @skip_pypy @@ -440,8 +452,10 @@ class Foo(Base): assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined] assert Foo.bar.spy_return == 20 # type:ignore[attr-defined] + assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined] spy.assert_called_once_with(arg=10) assert spy.spy_return == 20 + assert spy.spy_return_list == [20] @skip_pypy @@ -460,8 +474,10 @@ def bar(cls, arg): assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined] assert Foo.bar.spy_return == 20 # type:ignore[attr-defined] + assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined] spy.assert_called_once_with(arg=10) assert spy.spy_return == 20 + assert spy.spy_return_list == [20] @skip_pypy @@ -475,8 +491,10 @@ def bar(arg): assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined] assert Foo.bar.spy_return == 20 # type:ignore[attr-defined] + assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined] spy.assert_called_once_with(arg=10) assert spy.spy_return == 20 + assert spy.spy_return_list == [20] @skip_pypy @@ -493,8 +511,10 @@ class Foo(Base): assert Foo.bar(arg=10) == 20 Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined] assert Foo.bar.spy_return == 20 # type:ignore[attr-defined] + assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined] spy.assert_called_once_with(arg=10) assert spy.spy_return == 20 + assert spy.spy_return_list == [20] def test_callable_like_spy(testdir: Any, mocker: MockerFixture) -> None: @@ -515,6 +535,7 @@ def __call__(self, x): uut.call_like(10) spy.assert_called_once_with(10) assert spy.spy_return == 20 + assert spy.spy_return_list == [20] async def test_instance_async_method_spy(mocker: MockerFixture) -> None: From 5b9d2858f507e04cc97a3c206f55d7d5505ae71c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 21 Mar 2024 15:55:40 -0300 Subject: [PATCH 22/28] Release 3.13.0 --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1c0b69c..d977131 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,8 @@ Releases ======== -UNRELEASED ----------- +3.13.0 (2024-03-21) +------------------- * `#417 `_: ``spy`` now has ``spy_return_list``, which is a list containing all the values returned by the spied function. * ``pytest-mock`` now requires ``pytest>=6.2.5``. From ef9461b8b5bdcdd416841b986cf4e1d336c84266 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 21 Mar 2024 15:58:35 -0300 Subject: [PATCH 23/28] Add instructions on how to start deploy from command-line --- RELEASING.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASING.rst b/RELEASING.rst index 8db6e7c..2c734f1 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -3,5 +3,10 @@ Here are the steps on how to make a new release. 1. Create a ``release-VERSION`` branch from ``upstream/main``. 2. Update ``CHANGELOG.rst``. 3. Push the branch to ``upstream``. -4. Once all tests pass, start the ``deploy`` workflow manually. +4. Once all tests pass, start the ``deploy`` workflow manually or via: + + ``` + gh workflow run deploy.yml --repo pytest-dev/pytest-mock --ref release-VERSION -f version=VERSION + ``` + 5. Merge the PR. From 366966bff1e3ca2e1455d704dd59991da5593877 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 21 Mar 2024 15:39:04 -0400 Subject: [PATCH 24/28] Export `MockType`/`AsyncMockType` for type annotations (#415) Fix #414 --------- Co-authored-by: Bruno Oliveira --- CHANGELOG.rst | 6 ++++++ src/pytest_mock/__init__.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d977131..c16eed8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Releases ======== +UNRELEASED +---------- + +* `#415 `_: ``MockType`` and ``AsyncMockType`` can be imported from ``pytest_mock`` for type annotation purposes. + + 3.13.0 (2024-03-21) ------------------- diff --git a/src/pytest_mock/__init__.py b/src/pytest_mock/__init__.py index 127e5c8..75fd27a 100644 --- a/src/pytest_mock/__init__.py +++ b/src/pytest_mock/__init__.py @@ -1,4 +1,6 @@ +from pytest_mock.plugin import AsyncMockType from pytest_mock.plugin import MockerFixture +from pytest_mock.plugin import MockType from pytest_mock.plugin import PytestMockWarning from pytest_mock.plugin import class_mocker from pytest_mock.plugin import mocker @@ -11,8 +13,10 @@ MockFixture = MockerFixture # backward-compatibility only (#204) __all__ = [ + "AsyncMockType", "MockerFixture", "MockFixture", + "MockType", "PytestMockWarning", "pytest_addoption", "pytest_configure", From 6bd8712a14a1a11d348354318fdbad3fd9bbdb78 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 21 Mar 2024 16:45:05 -0300 Subject: [PATCH 25/28] Drop pre-Python 3.8 support code pytest-mock only supports Python 3.8+ since version 3.12. --- src/pytest_mock/plugin.py | 23 +++++++--------------- tests/test_pytest_mock.py | 40 ++++++++------------------------------- 2 files changed, 15 insertions(+), 48 deletions(-) diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index 354752b..0d1e188 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -2,7 +2,6 @@ import builtins import functools import inspect -import sys import unittest.mock import warnings from dataclasses import dataclass @@ -30,16 +29,12 @@ _T = TypeVar("_T") -if sys.version_info >= (3, 8): - AsyncMockType = unittest.mock.AsyncMock - MockType = Union[ - unittest.mock.MagicMock, - unittest.mock.AsyncMock, - unittest.mock.NonCallableMagicMock, - ] -else: - AsyncMockType = Any - MockType = Union[unittest.mock.MagicMock, unittest.mock.NonCallableMagicMock] +AsyncMockType = unittest.mock.AsyncMock +MockType = Union[ + unittest.mock.MagicMock, + unittest.mock.AsyncMock, + unittest.mock.NonCallableMagicMock, +] class PytestMockWarning(UserWarning): @@ -271,17 +266,13 @@ def _start_patch( # 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: - if sys.version_info >= (3, 8): - depth = 5 - else: - depth = 4 mocked.__enter__.side_effect = lambda: warnings.warn( "Mocks returned by pytest-mock do not need to be used as context managers. " "The mocker fixture automatically undoes mocking at the end of a test. " "This warning can be ignored if it was triggered by mocking a context manager. " "https://pytest-mock.readthedocs.io/en/latest/remarks.html#usage-as-context-manager", PytestMockWarning, - stacklevel=depth, + stacklevel=5, ) return mocked diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index 1d43d9e..61b75d8 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -9,6 +9,7 @@ from typing import Generator from typing import Tuple from typing import Type +from unittest.mock import AsyncMock from unittest.mock import MagicMock import pytest @@ -22,14 +23,9 @@ platform.python_implementation() == "PyPy", reason="could not make it work on pypy" ) -# Python 3.8 changed the output formatting (bpo-35500), which has been ported to mock 3.0 -NEW_FORMATTING = sys.version_info >= (3, 8) # Python 3.11.7 changed the output formatting, https://github.com/python/cpython/issues/111019 NEWEST_FORMATTING = sys.version_info >= (3, 11, 7) -if sys.version_info[:2] >= (3, 8): - from unittest.mock import AsyncMock - @pytest.fixture def needs_assert_rewrite(pytestconfig): @@ -173,12 +169,7 @@ def test_mock_patch_dict_resetall(mocker: MockerFixture) -> None: "NonCallableMock", "PropertyMock", "sentinel", - pytest.param( - "seal", - marks=pytest.mark.skipif( - sys.version_info < (3, 7), reason="seal is present on 3.7 and above" - ), - ), + "seal", ], ) def test_mocker_aliases(name: str, pytestconfig: Any) -> None: @@ -243,10 +234,8 @@ def __test_failure_message(self, mocker: MockerFixture, **kwargs: Any) -> None: expected_name = kwargs.get("name") or "mock" if NEWEST_FORMATTING: msg = "expected call not found.\nExpected: {0}()\n Actual: not called." - elif NEW_FORMATTING: - msg = "expected call not found.\nExpected: {0}()\nActual: not called." else: - msg = "Expected call: {0}()\nNot called" + msg = "expected call not found.\nExpected: {0}()\nActual: not called." expected_message = msg.format(expected_name) stub = mocker.stub(**kwargs) with pytest.raises(AssertionError, match=re.escape(expected_message)): @@ -259,10 +248,6 @@ def test_failure_message_with_no_name(self, mocker: MagicMock) -> None: def test_failure_message_with_name(self, mocker: MagicMock, name: str) -> None: self.__test_failure_message(mocker, name=name) - @pytest.mark.skipif( - sys.version_info[:2] < (3, 8), - reason="This Python version doesn't have `AsyncMock`.", - ) def test_async_stub_type(self, mocker: MockerFixture) -> None: assert isinstance(mocker.async_stub(), AsyncMock) @@ -892,17 +877,11 @@ def test(mocker): """ ) result = testdir.runpytest("-s") - if NEW_FORMATTING: - expected_lines = [ - "*AssertionError: expected call not found.", - "*Expected: mock('', bar=4)", - "*Actual: mock('fo')", - ] - else: - expected_lines = [ - "*AssertionError: Expected call: mock('', bar=4)*", - "*Actual call: mock('fo')*", - ] + expected_lines = [ + "*AssertionError: expected call not found.", + "*Expected: mock('', bar=4)", + "*Actual: mock('fo')", + ] expected_lines += [ "*pytest introspection follows:*", "*Args:", @@ -918,9 +897,6 @@ def test(mocker): result.stdout.fnmatch_lines(expected_lines) -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="AsyncMock is present on 3.8 and above" -) @pytest.mark.usefixtures("needs_assert_rewrite") def test_detailed_introspection_async(testdir: Any) -> None: """Check that the "mock_use_standalone" is being used.""" From 4faf92ae233afadac3831ab570531e540dc87830 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 21 Mar 2024 18:52:51 -0300 Subject: [PATCH 26/28] Fix regression with mocker.patch not being undone correctly The problem was introduced due to a bug in #417 which escaped review: we should always add a mock object to the cache, even if a similar one already exists. Fix #420 --- CHANGELOG.rst | 2 ++ src/pytest_mock/plugin.py | 7 ++----- tests/test_pytest_mock.py | 25 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c16eed8..eba2dc4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,8 @@ UNRELEASED * `#415 `_: ``MockType`` and ``AsyncMockType`` can be imported from ``pytest_mock`` for type annotation purposes. +* `#420 `_: Fixed a regression which would cause ``mocker.patch.object`` to not being properly cleared between tests. + 3.13.0 (2024-03-21) ------------------- diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index 0d1e188..05f3530 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -60,11 +60,8 @@ def find(self, mock: MockType) -> MockCacheItem: return the_mock def add(self, mock: MockType, **kwargs: Any) -> MockCacheItem: - try: - return self.find(mock) - except ValueError: - self.cache.append(MockCacheItem(mock=mock, **kwargs)) - return self.cache[-1] + self.cache.append(MockCacheItem(mock=mock, **kwargs)) + return self.cache[-1] def remove(self, mock: MockType) -> None: mock_item = self.find(mock) diff --git a/tests/test_pytest_mock.py b/tests/test_pytest_mock.py index 61b75d8..fc16490 100644 --- a/tests/test_pytest_mock.py +++ b/tests/test_pytest_mock.py @@ -1264,3 +1264,28 @@ def foo(self): mocker.stop(spy) assert un_spy.foo() == 42 assert spy.call_count == 1 + + +def test_stop_multiple_patches(mocker: MockerFixture) -> None: + """Regression for #420.""" + + class Class1: + @staticmethod + def get(): + return 1 + + class Class2: + @staticmethod + def get(): + return 2 + + def handle_get(): + return 3 + + mocker.patch.object(Class1, "get", handle_get) + mocker.patch.object(Class2, "get", handle_get) + + mocker.stopall() + + assert Class1.get() == 1 + assert Class2.get() == 2 From 5257e3c0df0a18bca4028daa9e6d2d91870ff576 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 21 Mar 2024 19:06:27 -0300 Subject: [PATCH 27/28] Refactor MockCache to have a narrow interface It should also be responsible for stopping the patchers, instead of acting merely as storage. Follow up the previous commit. --- src/pytest_mock/plugin.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/pytest_mock/plugin.py b/src/pytest_mock/plugin.py index 05f3530..1e0a0b2 100644 --- a/src/pytest_mock/plugin.py +++ b/src/pytest_mock/plugin.py @@ -49,33 +49,37 @@ class MockCacheItem: @dataclass class MockCache: + """ + Cache MagicMock and Patcher instances so we can undo them later. + """ + cache: List[MockCacheItem] = field(default_factory=list) - def find(self, mock: MockType) -> MockCacheItem: - the_mock = next( - (mock_item for mock_item in self.cache if mock_item.mock == mock), None - ) - if the_mock is None: - raise ValueError("This mock object is not registered") - return the_mock + def _find(self, mock: MockType) -> MockCacheItem: + for mock_item in self.cache: + if mock_item.mock is mock: + return mock_item + raise ValueError("This mock object is not registered") def add(self, mock: MockType, **kwargs: Any) -> MockCacheItem: self.cache.append(MockCacheItem(mock=mock, **kwargs)) return self.cache[-1] def remove(self, mock: MockType) -> None: - mock_item = self.find(mock) + mock_item = self._find(mock) + if mock_item.patch: + mock_item.patch.stop() self.cache.remove(mock_item) def clear(self) -> None: + for mock_item in reversed(self.cache): + if mock_item.patch is not None: + mock_item.patch.stop() self.cache.clear() def __iter__(self) -> Iterator[MockCacheItem]: return iter(self.cache) - def __reversed__(self) -> Iterator[MockCacheItem]: - return reversed(self.cache) - class MockerFixture: """ @@ -146,9 +150,6 @@ def stopall(self) -> None: Stop all patchers started by this fixture. Can be safely called multiple times. """ - for mock_item in reversed(self._mock_cache): - if mock_item.patch is not None: - mock_item.patch.stop() self._mock_cache.clear() def stop(self, mock: unittest.mock.MagicMock) -> None: @@ -156,9 +157,6 @@ def stop(self, mock: unittest.mock.MagicMock) -> None: Stops a previous patch or spy call by passing the ``MagicMock`` object returned by it. """ - mock_item = self._mock_cache.find(mock) - if mock_item.patch: - mock_item.patch.stop() self._mock_cache.remove(mock) def spy(self, obj: object, name: str) -> MockType: From 8733134b6194395e9cd3c745adcc9a9c09b0279e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 21 Mar 2024 16:46:04 -0300 Subject: [PATCH 28/28] Update CHANGELOG for 3.14.0 --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eba2dc4..005dd03 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,8 @@ Releases ======== -UNRELEASED ----------- +3.14.0 (2024-03-21) +------------------- * `#415 `_: ``MockType`` and ``AsyncMockType`` can be imported from ``pytest_mock`` for type annotation purposes.