https://github.com/python/cpython/commit/564c58c718bc3de9cf3d9bc20cdc06317411c1cd
commit: 564c58c718bc3de9cf3d9bc20cdc06317411c1cd
branch: main
author: Jiucheng(Oliver) <[email protected]>
committer: gpshead <[email protected]>
date: 2026-06-30T22:31:43-07:00
summary:
gh-151626: Fix tests that fail when PYTHONPYCACHEPREFIX is set (GH-151952)
Fix tests in test_compileall, test_import, test_importlib, test_py_compile, and
test_inspect to neutralize PYTHONPYCACHEPREFIX
files:
A Misc/NEWS.d/next/Tests/2026-06-22-19-45-00.gh-issue-151626.K9pZ2x.rst
M Lib/test/test_compileall.py
M Lib/test/test_import/__init__.py
M Lib/test/test_importlib/test_util.py
M Lib/test/test_inspect/test_inspect.py
M Lib/test/test_py_compile.py
diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py
index b2150b621516bae..95dcb4ef9fdc202 100644
--- a/Lib/test/test_compileall.py
+++ b/Lib/test/test_compileall.py
@@ -554,6 +554,24 @@ def temporary_pycache_prefix(self):
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 @@ def test_legacy_paths(self):
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 @@ def test_symlink_loop(self):
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 @@ def test_include_on_stdin(self):
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')
diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py
index 9f3df8010d32339..9f6dec7d1c5802c 100644
--- a/Lib/test/test_import/__init__.py
+++ b/Lib/test/test_import/__init__.py
@@ -1669,6 +1669,11 @@ def _clean(self):
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 @@ def tearDown(self):
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):
diff --git a/Lib/test/test_importlib/test_util.py
b/Lib/test/test_importlib/test_util.py
index a926a7a4d408afc..8ec2dcd69e58b63 100644
--- a/Lib/test/test_importlib/test_util.py
+++ b/Lib/test/test_importlib/test_util.py
@@ -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):
diff --git a/Lib/test/test_inspect/test_inspect.py
b/Lib/test/test_inspect/test_inspect.py
index 495702badc3a7f1..5153e5eb9a4ff8b 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -7,6 +7,7 @@
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 @@ def test_details_option_with_package(self):
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 @@ def test_details_option_with_data_target(self):
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 @@ def test_details_option_with_aliased_target(self):
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]}",
"",
]
diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py
index 09113e983fcd94d..b4265e6a0b458db 100644
--- a/Lib/test/test_py_compile.py
+++ b/Lib/test/test_py_compile.py
@@ -186,21 +186,24 @@ def test_source_date_epoch(self):
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 @@ def test_with_files(self):
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
index 000000000000000..9858415018ae1ba
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2026-06-22-19-45-00.gh-issue-151626.K9pZ2x.rst
@@ -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.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]