From 508a6381df780f03c9f40d3c353f1a235797617e Mon Sep 17 00:00:00 2001 From: tusenka Date: Tue, 27 May 2025 18:55:40 +0300 Subject: [PATCH 1/2] 13403: Disable assertion rewriting for external modules - squash commits --- changelog/13403.bugfix.rst | 1 + src/_pytest/assertion/__init__.py | 8 ++ src/_pytest/assertion/rewrite.py | 3 +- src/_pytest/pytester.py | 5 ++ testing/test_assertrewrite.py | 123 ++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 changelog/13403.bugfix.rst diff --git a/changelog/13403.bugfix.rst b/changelog/13403.bugfix.rst new file mode 100644 index 00000000000..132cbfe0010 --- /dev/null +++ b/changelog/13403.bugfix.rst @@ -0,0 +1 @@ +Disable assertion rewriting of external modules diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 532b96fe431..bb3a47ed28b 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Generator +import os import sys from typing import Any from typing import TYPE_CHECKING @@ -106,8 +107,15 @@ class AssertionState: def __init__(self, config: Config, mode) -> None: self.mode = mode self.trace = config.trace.root.get("assertion") + self.config=config self.hook: rewrite.AssertionRewritingHook | None = None + @property + def rootpath(self): + """Get current root path (current working dir) + """ + return str(self.config.invocation_params.dir) + def install_importhook(config: Config) -> rewrite.AssertionRewritingHook: """Try to install the rewrite hook, raise SystemError if it fails.""" diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index c4782c7c5a8..861ec77a70b 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -238,8 +238,9 @@ def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool: # modules not passed explicitly on the command line are only # rewritten if they match the naming convention for test files fn_path = PurePath(fn) + for pat in self.fnpats: - if fnmatch_ex(pat, fn_path): + if fnmatch_ex(pat, fn_path) and fn_path.is_relative_to(state.rootpath): state.trace(f"matched test file {fn!r}") return True diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 11127a88bb8..206456c47ba 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -749,6 +749,11 @@ def chdir(self) -> None: This is done automatically upon instantiation. """ self._monkeypatch.chdir(self.path) + self._monkeypatch.setattr(self._request.config,"invocation_params", Config.InvocationParams( + args= self._request.config.invocation_params.args, + plugins=self._request.config.invocation_params.plugins, + dir=Path(self._path), + )) def _makefile( self, diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index e2e448fe5e6..b0a02ba980a 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -12,6 +12,7 @@ import inspect import marshal import os +from os import mkdir from pathlib import Path import py_compile import re @@ -22,6 +23,10 @@ from unittest import mock import zipfile +from mock.mock import Mock + +from _pytest.monkeypatch import MonkeyPatch + import _pytest._code from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest.assertion import util @@ -35,6 +40,7 @@ from _pytest.assertion.rewrite import rewrite_asserts from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import make_numbered_dir from _pytest.pytester import Pytester import pytest @@ -370,6 +376,7 @@ def test_rewrites_plugin_as_a_package(self, pytester: Pytester) -> None: pytester.makeconftest('pytest_plugins = ["plugin"]') pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n") result = pytester.runpytest() + result.stdout.fnmatch_lines(["*assert 1 == 2*"]) def test_honors_pep_235(self, pytester: Pytester, monkeypatch) -> None: @@ -1294,6 +1301,33 @@ def test_meta_path(): ) assert pytester.runpytest().ret == 0 + def test_rootpath_base(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + """ + Base cases for get rootpath from AssertionState + """ + from _pytest.assertion import AssertionState + + config = pytester.parseconfig() + state = AssertionState(config, "rewrite") + assert state.rootpath == str(config.invocation_params.dir) + new_rootpath =str(pytester.path / "test") + if not os.path.exists(new_rootpath): + os.mkdir(new_rootpath) + monkeypatch.setattr(config,"invocation_params", Config.InvocationParams( + args= (), + plugins=(), + dir=Path(new_rootpath), + )) + state = AssertionState(config, "rewrite") + assert state.rootpath == new_rootpath + + @pytest.mark.skipif( + sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" + ) + @pytest.mark.skipif( + sys.platform.startswith("sunos5"), reason="cannot remove cwd on Solaris" + ) + def test_write_pyc(self, pytester: Pytester, tmp_path) -> None: from _pytest.assertion import AssertionState from _pytest.assertion.rewrite import _write_pyc @@ -1971,6 +2005,95 @@ def test_simple_failure(): assert hook.find_spec("file") is not None assert self.find_spec_calls == ["file"] + def test_assert_rewrites_only_rootpath( + self, pytester: Pytester, hook: AssertionRewritingHook, monkeypatch + ) -> None: + """ + If test files contained outside the rootpath, then skip them + """ + pytester.makepyfile( + **{ + "file.py": """\ + def test_simple_failure(): + assert 1 + 1 == 3 + """ + } + ) + with mock.patch.object(hook, "fnpats", ["*.py"]): + assert hook.find_spec("file") is not None + + rootpath = f"{os.getcwd()}/tests" + if not os.path.exists(rootpath): + mkdir(rootpath) + monkeypatch.setattr(pytester._request.config,"invocation_params", Config.InvocationParams( + args= (), + plugins=(), + dir=Path(rootpath), + )) + with mock.patch.object(hook, "fnpats", ["*.py"]): + assert hook.find_spec("file") is None + + def test_assert_rewrite_correct_for_conftfest( + self, pytester: Pytester, hook: AssertionRewritingHook, monkeypatch + ) -> None: + """ + Conftest is always rewritten regardless of the root dir + """ + pytester.makeconftest( + """ + import pytest + @pytest.fixture + def fix(): return 1 + """ + ) + + rootpath = f"{os.getcwd()}/tests" + if not os.path.exists(rootpath): + mkdir(rootpath) + monkeypatch.setattr( + pytester._request.config, + "invocation_params", + Config.InvocationParams( + args= (), + plugins=(), + dir=Path(rootpath), + ) + ) + with mock.patch.object(hook, "fnpats", ["*.py"]): + assert hook.find_spec("conftest") is not None + + def test_assert_rewrite_correct_for_plugins( + self, pytester: Pytester, hook: AssertionRewritingHook, monkeypatch + ) -> None: + """ + Plugins has always been rewritten regardless of the root dir + """ + pkgdir = pytester.mkpydir("plugin") + pkgdir.joinpath("__init__.py").write_text( + "import pytest\n" + "@pytest.fixture\n" + "def special_asserter():\n" + " def special_assert(x, y):\n" + " assert x == y\n" + " return special_assert\n", + encoding="utf-8", + ) + hook.mark_rewrite("plugin") + rootpath = f"{os.getcwd()}/tests" + if not os.path.exists(rootpath): + mkdir(rootpath) + monkeypatch.setattr( + pytester._request.config, + "invocation_params", + Config.InvocationParams( + args= (), + plugins=(), + dir=Path(rootpath), + ) + ) + with mock.patch.object(hook, "fnpats", ["*.py"]): + assert hook.find_spec("plugin") is not None + @pytest.mark.skipif( sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" ) From d08a463f4f192ef4ed8813a7ee0c4bc9e706be8b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 16:12:20 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/assertion/__init__.py | 5 +-- src/_pytest/pytester.py | 10 +++-- testing/test_assertrewrite.py | 63 ++++++++++++++++--------------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index bb3a47ed28b..04d4c96b1e3 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -107,13 +107,12 @@ class AssertionState: def __init__(self, config: Config, mode) -> None: self.mode = mode self.trace = config.trace.root.get("assertion") - self.config=config + self.config = config self.hook: rewrite.AssertionRewritingHook | None = None @property def rootpath(self): - """Get current root path (current working dir) - """ + """Get current root path (current working dir)""" return str(self.config.invocation_params.dir) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 206456c47ba..e1cea040e9f 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -749,11 +749,15 @@ def chdir(self) -> None: This is done automatically upon instantiation. """ self._monkeypatch.chdir(self.path) - self._monkeypatch.setattr(self._request.config,"invocation_params", Config.InvocationParams( - args= self._request.config.invocation_params.args, + self._monkeypatch.setattr( + self._request.config, + "invocation_params", + Config.InvocationParams( + args=self._request.config.invocation_params.args, plugins=self._request.config.invocation_params.plugins, dir=Path(self._path), - )) + ), + ) def _makefile( self, diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index b0a02ba980a..260e836d554 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -23,10 +23,6 @@ from unittest import mock import zipfile -from mock.mock import Mock - -from _pytest.monkeypatch import MonkeyPatch - import _pytest._code from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest.assertion import util @@ -1310,14 +1306,18 @@ def test_rootpath_base(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No config = pytester.parseconfig() state = AssertionState(config, "rewrite") assert state.rootpath == str(config.invocation_params.dir) - new_rootpath =str(pytester.path / "test") + new_rootpath = str(pytester.path / "test") if not os.path.exists(new_rootpath): os.mkdir(new_rootpath) - monkeypatch.setattr(config,"invocation_params", Config.InvocationParams( - args= (), - plugins=(), - dir=Path(new_rootpath), - )) + monkeypatch.setattr( + config, + "invocation_params", + Config.InvocationParams( + args=(), + plugins=(), + dir=Path(new_rootpath), + ), + ) state = AssertionState(config, "rewrite") assert state.rootpath == new_rootpath @@ -1327,7 +1327,6 @@ def test_rootpath_base(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No @pytest.mark.skipif( sys.platform.startswith("sunos5"), reason="cannot remove cwd on Solaris" ) - def test_write_pyc(self, pytester: Pytester, tmp_path) -> None: from _pytest.assertion import AssertionState from _pytest.assertion.rewrite import _write_pyc @@ -2025,11 +2024,15 @@ def test_simple_failure(): rootpath = f"{os.getcwd()}/tests" if not os.path.exists(rootpath): mkdir(rootpath) - monkeypatch.setattr(pytester._request.config,"invocation_params", Config.InvocationParams( - args= (), - plugins=(), - dir=Path(rootpath), - )) + monkeypatch.setattr( + pytester._request.config, + "invocation_params", + Config.InvocationParams( + args=(), + plugins=(), + dir=Path(rootpath), + ), + ) with mock.patch.object(hook, "fnpats", ["*.py"]): assert hook.find_spec("file") is None @@ -2051,13 +2054,13 @@ def fix(): return 1 if not os.path.exists(rootpath): mkdir(rootpath) monkeypatch.setattr( - pytester._request.config, - "invocation_params", - Config.InvocationParams( - args= (), - plugins=(), - dir=Path(rootpath), - ) + pytester._request.config, + "invocation_params", + Config.InvocationParams( + args=(), + plugins=(), + dir=Path(rootpath), + ), ) with mock.patch.object(hook, "fnpats", ["*.py"]): assert hook.find_spec("conftest") is not None @@ -2083,13 +2086,13 @@ def test_assert_rewrite_correct_for_plugins( if not os.path.exists(rootpath): mkdir(rootpath) monkeypatch.setattr( - pytester._request.config, - "invocation_params", - Config.InvocationParams( - args= (), - plugins=(), - dir=Path(rootpath), - ) + pytester._request.config, + "invocation_params", + Config.InvocationParams( + args=(), + plugins=(), + dir=Path(rootpath), + ), ) with mock.patch.object(hook, "fnpats", ["*.py"]): assert hook.find_spec("plugin") is not None