]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-151626: Fix tests that fail when PYTHONPYCACHEPREFIX is set (GH-151952)
authorJiucheng(Oliver) <git.jiucheng@gmail.com>
Wed, 1 Jul 2026 05:31:43 +0000 (01:31 -0400)
committerGitHub <noreply@github.com>
Wed, 1 Jul 2026 05:31:43 +0000 (22:31 -0700)
Fix tests in test_compileall, test_import, test_importlib, test_py_compile, and test_inspect to neutralize PYTHONPYCACHEPREFIX

Lib/test/test_compileall.py
Lib/test/test_import/__init__.py
Lib/test/test_importlib/test_util.py
Lib/test/test_inspect/test_inspect.py
Lib/test/test_py_compile.py
Misc/NEWS.d/next/Tests/2026-06-22-19-45-00.gh-issue-151626.K9pZ2x.rst [new file with mode: 0644]

index b2150b621516bae90eed4d62d9ce442d1de3fba7..95dcb4ef9fdc202bebd325bd815c45d595d52c98 100644 (file)
@@ -554,6 +554,24 @@ class CommandLineTestsBase:
         finally:
             sys.pycache_prefix = old_prefix
 
+    @contextlib.contextmanager
+    def no_pycache_prefix(self):
+        """Ignore any ambient pycache prefix for the duration of the test.
+
+        Some tests assume bytecode is written next to the source in a
+        __pycache__ directory.  When the test suite is run with
+        PYTHONPYCACHEPREFIX set, neutralize it both in this process (used by
+        cache_from_source) and in any spawned subprocesses.
+        """
+        old_prefix = sys.pycache_prefix
+        sys.pycache_prefix = None
+        try:
+            with os_helper.EnvironmentVarGuard() as env:
+                env.unset('PYTHONPYCACHEPREFIX')
+                yield
+        finally:
+            sys.pycache_prefix = old_prefix
+
     def _get_run_args(self, args):
         return [*support.optim_args_from_interpreter_flags(),
                 '-S', '-m', 'compileall',
@@ -650,15 +668,16 @@ class CommandLineTestsBase:
     def test_multiple_runs(self):
         # Bug 8527 reported that multiple calls produced empty
         # __pycache__/__pycache__ directories.
-        self.assertRunOK('-q', self.pkgdir)
-        # Verify the __pycache__ directory contents.
-        self.assertTrue(os.path.exists(self.pkgdir_cachedir))
-        cachecachedir = os.path.join(self.pkgdir_cachedir, '__pycache__')
-        self.assertFalse(os.path.exists(cachecachedir))
-        # Call compileall again.
-        self.assertRunOK('-q', self.pkgdir)
-        self.assertTrue(os.path.exists(self.pkgdir_cachedir))
-        self.assertFalse(os.path.exists(cachecachedir))
+        with self.no_pycache_prefix():
+            self.assertRunOK('-q', self.pkgdir)
+            # Verify the __pycache__ directory contents.
+            self.assertTrue(os.path.exists(self.pkgdir_cachedir))
+            cachecachedir = os.path.join(self.pkgdir_cachedir, '__pycache__')
+            self.assertFalse(os.path.exists(cachecachedir))
+            # Call compileall again.
+            self.assertRunOK('-q', self.pkgdir)
+            self.assertTrue(os.path.exists(self.pkgdir_cachedir))
+            self.assertFalse(os.path.exists(cachecachedir))
 
     @without_source_date_epoch  # timestamp invalidation test
     def test_force(self):
@@ -731,10 +750,13 @@ class CommandLineTestsBase:
         script_helper.make_pkg(pkg)
         os.symlink('.', os.path.join(pkg, 'evil'))
         os.symlink('.', os.path.join(pkg, 'evil2'))
-        self.assertRunOK('-q', self.pkgdir)
-        self.assertCompiled(os.path.join(
-            self.pkgdir, 'spam', 'evil', 'evil2', '__init__.py'
-        ))
+        # This relies on the __pycache__ layout (shared across the symlinked
+        # paths), so neutralize any ambient PYTHONPYCACHEPREFIX.
+        with self.no_pycache_prefix():
+            self.assertRunOK('-q', self.pkgdir)
+            self.assertCompiled(os.path.join(
+                self.pkgdir, 'spam', 'evil', 'evil2', '__init__.py'
+            ))
 
     def test_quiet(self):
         noisy = self.assertRunOK(self.pkgdir)
@@ -821,13 +843,16 @@ class CommandLineTestsBase:
         f2 = script_helper.make_script(self.pkgdir, 'f2', '')
         f3 = script_helper.make_script(self.pkgdir, 'f3', '')
         f4 = script_helper.make_script(self.pkgdir, 'f4', '')
-        p = script_helper.spawn_python(*(self._get_run_args(()) + ['-i', '-']))
-        p.stdin.write((f3+os.linesep).encode('ascii'))
-        script_helper.kill_python(p)
-        self.assertNotCompiled(f1)
-        self.assertNotCompiled(f2)
-        self.assertCompiled(f3)
-        self.assertNotCompiled(f4)
+        # spawn_python() runs with -E, ignoring PYTHONPYCACHEPREFIX, so make
+        # cache_from_source() in this process agree by neutralizing it too.
+        with self.no_pycache_prefix():
+            p = script_helper.spawn_python(*(self._get_run_args(()) + ['-i', '-']))
+            p.stdin.write((f3+os.linesep).encode('ascii'))
+            script_helper.kill_python(p)
+            self.assertNotCompiled(f1)
+            self.assertNotCompiled(f2)
+            self.assertCompiled(f3)
+            self.assertNotCompiled(f4)
 
     def test_compiles_as_much_as_possible(self):
         bingfn = script_helper.make_script(self.pkgdir, 'bing', 'syntax(error')
index 9f3df8010d3233956fd8affc404ec336f5664f71..9f6dec7d1c5802c53f210d188fb749f39a9356aa 100644 (file)
@@ -1669,6 +1669,11 @@ class PycacheTests(unittest.TestCase):
         unlink(self.source)
 
     def setUp(self):
+        # These tests assume bytecode is written next to the source in a
+        # local __pycache__ directory, so neutralize any pycache prefix (e.g.
+        # when the test suite is run with PYTHONPYCACHEPREFIX set).
+        self._orig_pycache_prefix = sys.pycache_prefix
+        sys.pycache_prefix = None
         self.source = TESTFN + '.py'
         self._clean()
         with open(self.source, 'w', encoding='utf-8') as fp:
@@ -1680,6 +1685,7 @@ class PycacheTests(unittest.TestCase):
         assert sys.path[0] == os.curdir, 'Unexpected sys.path[0]'
         del sys.path[0]
         self._clean()
+        sys.pycache_prefix = self._orig_pycache_prefix
 
     @skip_if_dont_write_bytecode
     def test_import_pyc_path(self):
index a926a7a4d408afc43da86cd1fda9795511bffa75..8ec2dcd69e58b636a0042a1798a70968e586bea9 100644 (file)
@@ -328,6 +328,17 @@ class PEP3147Tests:
 
     tag = sys.implementation.cache_tag
 
+    def setUp(self):
+        # Most of these tests assume the default (unset) pycache prefix, so
+        # clear it for the duration of the test (e.g. when the test suite is
+        # run with PYTHONPYCACHEPREFIX set).  Tests that need a specific prefix
+        # set their own via util.temporary_pycache_prefix().
+        self._orig_pycache_prefix = sys.pycache_prefix
+        sys.pycache_prefix = None
+
+    def tearDown(self):
+        sys.pycache_prefix = self._orig_pycache_prefix
+
     @unittest.skipIf(sys.implementation.cache_tag is None,
                      'requires sys.implementation.cache_tag not be None')
     def test_cache_from_source(self):
index 495702badc3a7f1a2d301690a8e5f415734fb901..5153e5eb9a4ff8b6fc39165c35502cd0a12b009f 100644 (file)
@@ -7,6 +7,7 @@ import datetime
 import functools
 import gc
 import importlib
+import importlib.util
 import inspect
 import io
 import linecache
@@ -6519,6 +6520,19 @@ class TestModuleCLI(unittest.TestCase):
     NO_SOURCE_ERROR = "No source code available for defining module"
     NO_SOURCE_TARGET_ERROR = "Failed to retrieve source code for given target"
 
+    @staticmethod
+    def _expected_cached(module):
+        # assert_python_ok() runs the subprocess in isolated mode (-I), which
+        # ignores PYTHONPYCACHEPREFIX, so compute the expected cached path the
+        # same way (i.e. without any pycache prefix) to stay independent of the
+        # environment the test suite is run in.  Modules without a cached path
+        # (e.g. frozen modules such as ntpath/importlib.machinery on Windows)
+        # report None, so preserve that.
+        if module.__spec__.cached is None:
+            return None
+        with support.swap_attr(sys, 'pycache_prefix', None):
+            return importlib.util.cache_from_source(module.__spec__.origin)
+
     def test_only_source(self):
         module = importlib.import_module('unittest')
         rc, out, err = assert_python_ok('-m', 'inspect',
@@ -6576,12 +6590,13 @@ class TestModuleCLI(unittest.TestCase):
         args = support.optim_args_from_interpreter_flags()
         rc, out, err = assert_python_ok(*args, '-m', 'inspect',
                                         module_name, '--details')
+        cached = self._expected_cached(module)
         # Full rendering check on the expected output
         expected_lines = [
             f"Target: {module.__name__}",  # No aliasing
             f"Origin: {module.__spec__.origin}",
             f"Source: {module.__file__}",
-            f"Cached: {module.__spec__.cached}",  # None is still displayed
+            f"Cached: {cached}",  # None is still displayed
             f"Loader: {_clean_object_ids(repr(module.__spec__.loader))}",
             f"Submodule search paths: {module.__path__}",
             "",
@@ -6619,13 +6634,14 @@ class TestModuleCLI(unittest.TestCase):
         args = support.optim_args_from_interpreter_flags()
         rc, out, err = assert_python_ok(*args, '-m', 'inspect',
                                         cli_target, '--details')
+        cached = self._expected_cached(module)
         # Full rendering check on the expected output
         # The error is only informational when reading source details
         expected_lines = [
             f"Target: {cli_target}",  # No aliasing
             f"Origin: {module.__spec__.origin}",
             f"Source: {module.__file__}",
-            f"Cached: {module.__spec__.cached}",  # None is still displayed
+            f"Cached: {cached}",  # None is still displayed
             self.NO_SOURCE_TARGET_ERROR,
             "",
         ]
@@ -6644,12 +6660,13 @@ class TestModuleCLI(unittest.TestCase):
         args = support.optim_args_from_interpreter_flags()
         rc, out, err = assert_python_ok(*args, '-m', 'inspect',
                                         cli_target, '--details')
+        cached = self._expected_cached(module)
         # Full rendering check on the expected output
         expected_lines = [
             f'Target: {defining_target} (looked up as "{cli_target}")',
             f"Origin: {module.__spec__.origin}",
             f"Source: {module.__file__}",
-            f"Cached: {module.__spec__.cached}",  # None is still displayed
+            f"Cached: {cached}",  # None is still displayed
             f"Line: {inspect.findsource(target)[1]}",
             "",
         ]
index 09113e983fcd94d8f586e0a22a72629c8dd2e411..b4265e6a0b458db42f62971a4d27ff0cfc3a5b2f 100644 (file)
@@ -186,21 +186,24 @@ class PyCompileTestsBase:
     def test_double_dot_no_clobber(self):
         # http://bugs.python.org/issue22966
         # py_compile foo.bar.py -> __pycache__/foo.cpython-34.pyc
-        weird_path = os.path.join(self.directory, 'foo.bar.py')
-        cache_path = importlib.util.cache_from_source(weird_path)
-        pyc_path = weird_path + 'c'
-        head, tail = os.path.split(cache_path)
-        penultimate_tail = os.path.basename(head)
-        self.assertEqual(
-            os.path.join(penultimate_tail, tail),
-            os.path.join(
-                '__pycache__',
-                'foo.bar.{}.pyc'.format(sys.implementation.cache_tag)))
-        with open(weird_path, 'w') as file:
-            file.write('x = 123\n')
-        py_compile.compile(weird_path)
-        self.assertTrue(os.path.exists(cache_path))
-        self.assertFalse(os.path.exists(pyc_path))
+        # This test asserts the default __pycache__ layout, so neutralize any
+        # pycache prefix (e.g. when run with PYTHONPYCACHEPREFIX set).
+        with support.swap_attr(sys, 'pycache_prefix', None):
+            weird_path = os.path.join(self.directory, 'foo.bar.py')
+            cache_path = importlib.util.cache_from_source(weird_path)
+            pyc_path = weird_path + 'c'
+            head, tail = os.path.split(cache_path)
+            penultimate_tail = os.path.basename(head)
+            self.assertEqual(
+                os.path.join(penultimate_tail, tail),
+                os.path.join(
+                    '__pycache__',
+                    'foo.bar.{}.pyc'.format(sys.implementation.cache_tag)))
+            with open(weird_path, 'w') as file:
+                file.write('x = 123\n')
+            py_compile.compile(weird_path)
+            self.assertTrue(os.path.exists(cache_path))
+            self.assertFalse(os.path.exists(pyc_path))
 
     @unittest.skipIf(sys.implementation.cache_tag is None,
                      'requires sys.implementation.cache_tag is not None')
@@ -307,7 +310,13 @@ class PyCompileCLITestCase(unittest.TestCase):
         self.assertEqual(rc, 0)
         self.assertEqual(stdout, b'')
         self.assertEqual(stderr, b'')
-        self.assertTrue(os.path.exists(self.cache_path))
+        # pycompilecmd() runs the interpreter in isolated mode (-I), which
+        # ignores PYTHONPYCACHEPREFIX, so the bytecode is written next to the
+        # source.  Compute the expected cache path the same way.
+        with support.swap_attr(sys, 'pycache_prefix', None):
+            cache_path = importlib.util.cache_from_source(
+                self.source_path, optimization='' if __debug__ else 1)
+        self.assertTrue(os.path.exists(cache_path))
 
     def test_bad_syntax(self):
         bad_syntax = os.path.join(os.path.dirname(__file__),
diff --git a/Misc/NEWS.d/next/Tests/2026-06-22-19-45-00.gh-issue-151626.K9pZ2x.rst b/Misc/NEWS.d/next/Tests/2026-06-22-19-45-00.gh-issue-151626.K9pZ2x.rst
new file mode 100644 (file)
index 0000000..9858415
--- /dev/null
@@ -0,0 +1,5 @@
+Fix several tests in ``test.test_inspect``, ``test.test_import``,
+``test.test_importlib``, ``test.test_py_compile`` and
+``test.test_compileall`` that failed when the test suite was run with
+:envvar:`PYTHONPYCACHEPREFIX` set.  These tests now neutralize the pycache
+prefix where they assume the default ``__pycache__`` bytecode layout.