From ba1f5697de762c7cd8e1272cce1a844881c195b7 Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Wed, 16 Sep 2026 17:00:58 +0700 Subject: [PATCH] Pre-mark pytest_plugins for assertion rewrite Co-authored-by: Claude Sonnet 5 --- changelog/2353.bugfix.rst | 1 + src/_pytest/config/__init__.py | 19 ++++++++++++++++++- testing/test_assertrewrite.py | 22 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 changelog/2353.bugfix.rst diff --git a/changelog/2353.bugfix.rst b/changelog/2353.bugfix.rst new file mode 100644 index 00000000000..b70f1ddf5d1 --- /dev/null +++ b/changelog/2353.bugfix.rst @@ -0,0 +1 @@ +``pytest_plugins`` entries imported as a side effect of another plugin now also get assertion rewriting. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c7bd3e1afab..efed786738b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -909,6 +909,18 @@ def _import_plugin_specs( self, spec: types.ModuleType | str | Sequence[str] | None ) -> None: plugins = _get_plugin_specs_as_list(spec) + # Pre-mark plugins so side-effect imports are rewritten too (#2353). + # Skip blocked, registered or already imported ones. Their own import + # handles the warning. + self.rewrite_hook.mark_rewrite( + *( + _resolve_plugin_import_spec(plugin) + for plugin in plugins + if _resolve_plugin_import_spec(plugin) not in sys.modules + and not self.is_blocked(plugin) + and self.get_plugin(plugin) is None + ) + ) for import_spec in plugins: self.import_plugin(import_spec, consider_entry_points=True) @@ -928,7 +940,7 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No if self.is_blocked(modname) or self.get_plugin(modname) is not None: return - importspec = "_pytest." + modname if modname in builtin_plugins else modname + importspec = _resolve_plugin_import_spec(modname) self.rewrite_hook.mark_rewrite(importspec) if consider_entry_points: @@ -1006,6 +1018,11 @@ def _get_plugin_specs_as_list( ) +def _resolve_plugin_import_spec(modname: str) -> str: + """Resolve a plugin name to its import name (builtins live under ``_pytest``).""" + return "_pytest." + modname if modname in builtin_plugins else modname + + def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: """Given an iterable of file names in a source distribution, return the "names" that should be marked for assertion rewrite. diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index c9736f8fa48..f8281bd20bd 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1288,6 +1288,28 @@ def test_rewrite_warning_using_pytest_plugins(self, pytester: Pytester) -> None: result.stdout.fnmatch_lines(["*= 1 passed in *=*"]) result.stdout.no_fnmatch_line("*pytest-warning summary*") + def test_plugin_imported_as_side_effect_is_rewritten( + self, pytester: Pytester + ) -> None: + """A plugin imported as a side effect of another plugin must still + get assertion rewriting (#2353).""" + pytester.makepyfile( + **{ + "conftest.py": "pytest_plugins = ['plugin_2353_a', 'plugin_2353_b']", + "plugin_2353_a.py": "import plugin_2353_b", + "plugin_2353_b.py": "def check():\n x = 1\n assert x == 2\n", + "test_2353_side_effect.py": ( + "import plugin_2353_b\n\ndef test_rewritten():\n" + " plugin_2353_b.check()\n" + ), + } + ) + pytester.chdir() + result = pytester.runpytest_subprocess() + result.assert_outcomes(failed=1) + result.stdout.fnmatch_lines(["E *assert 1 == 2*"]) + result.stdout.no_fnmatch_line("*already imported*") + def test_rewrite_warning_using_pytest_plugins_env_var( self, pytester: Pytester, monkeypatch ) -> None: