From 411751e7aafd882a2612bf4673e7a29008b1da81 Mon Sep 17 00:00:00 2001 From: Frank Hoffmann <15r10nk-git@polarbit.de> Date: Tue, 8 Sep 2026 08:06:28 +0200 Subject: [PATCH] fix: bytecode cache invalidation for moved test files (#14552) --- changelog/14552.bugfix.rst | 1 + src/_pytest/assertion/rewrite.py | 7 +++++++ testing/test_assertrewrite.py | 25 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 changelog/14552.bugfix.rst diff --git a/changelog/14552.bugfix.rst b/changelog/14552.bugfix.rst new file mode 100644 index 00000000000..8e8f01d9d19 --- /dev/null +++ b/changelog/14552.bugfix.rst @@ -0,0 +1 @@ +Fixed assertion-rewrite cache invalidation for moved test files: a rewritten ``.pyc`` whose ``co_filename`` no longer matches the current source path is now treated as stale and rewritten. Previously, renaming or moving a test module/directory could leak the old path into ``inspect.currentframe().f_code.co_filename`` and related traceback/reporting paths. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 27953336c5c..1709c6c4b5e 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -379,6 +379,13 @@ def _read_pyc( if not isinstance(co, types.CodeType): trace(f"_read_pyc({source}): not a code object") return None + # A cached pyc can be moved together with the source file (for example + # by renaming a package or test directory). In that case the marshaled + # code object's ``co_filename`` still points to the old source path. + # Treat that as stale so the caller rewrites and recreates the cache. + if co.co_filename != str(source): + trace(f"_read_pyc({source}): stale filename {co.co_filename!r}") + return None return co diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index c9736f8fa48..98e7b54589e 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1161,6 +1161,31 @@ def test_foo(): glob.glob("__pycache__/*.pyc") ) + def test_moved_test_file_updates_code_filename( + self, pytester: Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Moving a test module must keep ``co_filename`` synchronized with ``__file__``.""" + monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) + + pytester.makepyfile( + **{ + "test1/test_a.py": """ + from inspect import currentframe + + def test_a(): + assert currentframe().f_code.co_filename == __file__ + """ + } + ) + + first = pytester.runpytest_subprocess("-s", "test1/test_a.py") + first.assert_outcomes(passed=1) + + pytester.path.joinpath("test1").rename(pytester.path.joinpath("test2")) + + second = pytester.runpytest_subprocess("-s", "test2/test_a.py") + second.assert_outcomes(passed=1) + @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo( self,