From 1b9f37302761ec02375e52f0f5563cc868295b25 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 27 Jan 2026 23:39:52 +0000 Subject: [PATCH 01/29] gh-144278: Enable defining _PY_DISABLE_SYS_CACHE_TAG to disable sys.implementation.cache_tag. This has the effect of disabling automatic .pyc caching. --- .../next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst diff --git a/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst b/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst new file mode 100644 index 00000000000000..fa6b0c6e505cdb --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst @@ -0,0 +1,4 @@ +Enables patching in a ``_PY_DISABLE_SYS_CACHE_TAG`` preprocessor definition +when building to force :data:`sys.implementation.cache_tag` to ``None``. +This has the effect of completely disabling automatic creation and reading +of ``.pyc`` files. From 732c090b3f280bdf9b2c53185785c5dcd22049d4 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 27 Jan 2026 23:41:10 +0000 Subject: [PATCH 02/29] Helps if you commit the actual changes... --- Lib/py_compile.py | 4 +- Lib/test/support/import_helper.py | 21 ++- Lib/test/test_argparse.py | 3 +- Lib/test/test_capi/test_import.py | 5 +- Lib/test/test_cmd_line_script.py | 21 ++- Lib/test/test_compileall.py | 4 + Lib/test/test_import/__init__.py | 28 ++-- .../test_importlib/source/test_file_loader.py | 19 ++- Lib/test/test_importlib/source/test_finder.py | 2 + Lib/test/test_importlib/test_abc.py | 12 +- Lib/test/test_importlib/test_api.py | 4 - Lib/test/test_importlib/test_pkg_import.py | 9 +- Lib/test/test_importlib/test_spec.py | 17 ++- Lib/test/test_importlib/test_util.py | 30 +++++ Lib/test/test_importlib/util.py | 3 + Lib/test/test_inspect/test_inspect.py | 3 +- .../test_multiprocessing_main_handling.py | 12 +- Lib/test/test_py_compile.py | 68 +++++++--- Lib/test/test_pydoc/test_pydoc.py | 6 +- Lib/test/test_reprlib.py | 7 +- Lib/test/test_runpy.py | 27 ++-- Lib/test/test_zipimport.py | 1 - Lib/zipfile/__init__.py | 122 ++++++------------ Modules/_testlimitedcapi/import.c | 2 +- PC/pyconfig.h | 3 + Python/sysmodule.c | 11 +- 26 files changed, 270 insertions(+), 174 deletions(-) diff --git a/Lib/py_compile.py b/Lib/py_compile.py index 43d8ec90ffb6b1..694ea9304da9f9 100644 --- a/Lib/py_compile.py +++ b/Lib/py_compile.py @@ -194,8 +194,10 @@ def main(): else: filenames = args.filenames for filename in filenames: + cfilename = (None if sys.implementation.cache_tag + else f"{filename.rpartition('.')[0]}.pyc") try: - compile(filename, doraise=True) + compile(filename, cfilename, doraise=True) except PyCompileError as error: if args.quiet: parser.exit(1) diff --git a/Lib/test/support/import_helper.py b/Lib/test/support/import_helper.py index 4c7eac0b7eb674..f1642e51daf815 100644 --- a/Lib/test/support/import_helper.py +++ b/Lib/test/support/import_helper.py @@ -4,6 +4,7 @@ import importlib.machinery import importlib.util import os +import py_compile import shutil import sys import textwrap @@ -49,20 +50,30 @@ def forget(modname): # combinations of PEP 3147/488 and legacy pyc files. unlink(source + 'c') for opt in ('', 1, 2): - unlink(importlib.util.cache_from_source(source, optimization=opt)) + try: + unlink(importlib.util.cache_from_source(source, optimization=opt)) + except NotImplementedError: + pass -def make_legacy_pyc(source): +def make_legacy_pyc(source, allow_compile=False): """Move a PEP 3147/488 pyc file to its legacy pyc location. :param source: The file system path to the source file. The source file - does not need to exist, however the PEP 3147/488 pyc file must exist. + does not need to exist, however the PEP 3147/488 pyc file must exist or + allow_compile must be set. + :param allow_compile: If True, uses py_compile to create a .pyc if it does + not exist. This should be passed as True if cache_tag may be None. :return: The file system path to the legacy pyc file. """ - pyc_file = importlib.util.cache_from_source(source) assert source.endswith('.py') legacy_pyc = source + 'c' - shutil.move(pyc_file, legacy_pyc) + try: + pyc_file = importlib.util.cache_from_source(source) + except NotImplementedError: + py_compile.compile(source, legacy_pyc, doraise=True) + else: + shutil.move(pyc_file, legacy_pyc) return legacy_pyc diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 77170244675474..d2e9903f897bed 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7162,9 +7162,8 @@ def make_script(self, dirname, basename, *, compiled=False): script_name = script_helper.make_script(dirname, basename, self.source) if not compiled: return script_name - py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) return pyc_file def make_zip_script(self, script_name, name_in_zip=None): diff --git a/Lib/test/test_capi/test_import.py b/Lib/test/test_capi/test_import.py index 57e0316fda8a52..ea845df75ef3a7 100644 --- a/Lib/test/test_capi/test_import.py +++ b/Lib/test/test_capi/test_import.py @@ -289,7 +289,10 @@ def check_executecode_pathnames(self, execute_code_func, object=False): self.check_executecodemodule(execute_code_func, NULL, pathname) # Test NULL pathname and non-NULL cpathname - pyc_filename = importlib.util.cache_from_source(__file__) + try: + pyc_filename = importlib.util.cache_from_source(__file__) + except NotImplementedError: + return py_filename = importlib.util.source_from_cache(pyc_filename) origin = self.check_executecodemodule(execute_code_func, NULL, pyc_filename) if not object: diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index 8695df9eb0c294..73b1f671c58555 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -240,9 +240,8 @@ def test_script_abspath(self): def test_script_compiled(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, 'script') - py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) self._check_script(pyc_file, pyc_file, pyc_file, script_dir, None, importlib.machinery.SourcelessFileLoader) @@ -257,9 +256,8 @@ def test_directory(self): def test_directory_compiled(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__') - py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) self._check_script(script_dir, pyc_file, script_dir, script_dir, '', importlib.machinery.SourcelessFileLoader) @@ -279,8 +277,8 @@ def test_zipfile(self): def test_zipfile_compiled_timestamp(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__') - compiled_name = py_compile.compile( - script_name, doraise=True, + compiled_name = script_name + 'c' + py_compile.compile(script_name, compiled_name, doraise=True, invalidation_mode=py_compile.PycInvalidationMode.TIMESTAMP) zip_name, run_name = make_zip_script(script_dir, 'test_zip', compiled_name) self._check_script(zip_name, run_name, zip_name, zip_name, '', @@ -289,8 +287,8 @@ def test_zipfile_compiled_timestamp(self): def test_zipfile_compiled_checked_hash(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__') - compiled_name = py_compile.compile( - script_name, doraise=True, + compiled_name = script_name + 'c' + py_compile.compile(script_name, compiled_name, doraise=True, invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH) zip_name, run_name = make_zip_script(script_dir, 'test_zip', compiled_name) self._check_script(zip_name, run_name, zip_name, zip_name, '', @@ -299,8 +297,8 @@ def test_zipfile_compiled_checked_hash(self): def test_zipfile_compiled_unchecked_hash(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__') - compiled_name = py_compile.compile( - script_name, doraise=True, + compiled_name = script_name + 'c' + py_compile.compile(script_name, compiled_name, doraise=True, invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH) zip_name, run_name = make_zip_script(script_dir, 'test_zip', compiled_name) self._check_script(zip_name, run_name, zip_name, zip_name, '', @@ -353,9 +351,8 @@ def test_package_compiled(self): pkg_dir = os.path.join(script_dir, 'test_pkg') make_pkg(pkg_dir) script_name = _make_test_script(pkg_dir, '__main__') - compiled_name = py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) self._check_script(["-m", "test_pkg"], pyc_file, pyc_file, script_dir, 'test_pkg', importlib.machinery.SourcelessFileLoader, diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py index 8384c183dd92dd..b2150b621516ba 100644 --- a/Lib/test/test_compileall.py +++ b/Lib/test/test_compileall.py @@ -33,6 +33,10 @@ from test.support.os_helper import FakePath +if sys.implementation.cache_tag is None: + raise unittest.SkipTest('requires sys.implementation.cache_tag is not None') + + def get_pyc(script, opt): if not opt: # Replace None and 0 with '' diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 59c6dc4587c93d..e26c0f87a4de7f 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -76,7 +76,7 @@ skip_if_dont_write_bytecode = unittest.skipIf( - sys.dont_write_bytecode, + sys.dont_write_bytecode or sys.implementation.cache_tag is None, "test meaningful only when writing bytecode") @@ -504,7 +504,7 @@ def test_module_with_large_stack(self, module='longlist'): try: # Compile & remove .py file; we only need .pyc. # Bytecode must be relocated from the PEP 3147 bytecode-only location. - py_compile.compile(filename) + make_legacy_pyc(filename, allow_compile=True) finally: unlink(filename) @@ -514,7 +514,6 @@ def test_module_with_large_stack(self, module='longlist'): namespace = {} try: - make_legacy_pyc(filename) # This used to crash. exec('import ' + module, None, namespace) finally: @@ -1399,7 +1398,10 @@ def func(): """ dir_name = os.path.abspath(TESTFN) file_name = os.path.join(dir_name, module_name) + os.extsep + "py" - compiled_name = importlib.util.cache_from_source(file_name) + try: + compiled_name = importlib.util.cache_from_source(file_name) + except NotImplementedError: + compiled_name = None def setUp(self): self.sys_path = sys.path[:] @@ -1417,7 +1419,8 @@ def tearDown(self): else: unload(self.module_name) unlink(self.file_name) - unlink(self.compiled_name) + if self.compiled_name: + unlink(self.compiled_name) rmtree(self.dir_name) def import_module(self): @@ -1436,6 +1439,8 @@ def test_basics(self): self.assertEqual(mod.code_filename, self.file_name) self.assertEqual(mod.func_filename, self.file_name) + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag is not None') def test_incorrect_code_name(self): py_compile.compile(self.file_name, dfile="another_module.py") mod = self.import_module() @@ -1445,9 +1450,9 @@ def test_incorrect_code_name(self): def test_module_without_source(self): target = "another_module.py" - py_compile.compile(self.file_name, dfile=target) + pyc_file = self.file_name + 'c' + py_compile.compile(self.file_name, pyc_file, dfile=target) os.remove(self.file_name) - pyc_file = make_legacy_pyc(self.file_name) importlib.invalidate_caches() mod = self.import_module() self.assertEqual(mod.module_filename, pyc_file) @@ -1455,8 +1460,9 @@ def test_module_without_source(self): self.assertEqual(mod.func_filename, target) def test_foreign_code(self): - py_compile.compile(self.file_name) - with open(self.compiled_name, "rb") as f: + compiled_name = self.compiled_name or (self.file_name + 'c') + py_compile.compile(self.file_name, compiled_name) + with open(compiled_name, "rb") as f: header = f.read(16) code = marshal.load(f) constants = list(code.co_consts) @@ -1464,9 +1470,11 @@ def test_foreign_code(self): pos = constants.index(1000) constants[pos] = foreign_code code = code.replace(co_consts=tuple(constants)) - with open(self.compiled_name, "wb") as f: + with open(compiled_name, "wb") as f: f.write(header) marshal.dump(code, f) + if not self.compiled_name: + os.remove(self.file_name) mod = self.import_module() self.assertEqual(mod.constant.co_filename, foreign_code.co_filename) diff --git a/Lib/test/test_importlib/source/test_file_loader.py b/Lib/test/test_importlib/source/test_file_loader.py index 5d5d4722171a8e..e4bd850f3514ff 100644 --- a/Lib/test/test_importlib/source/test_file_loader.py +++ b/Lib/test/test_importlib/source/test_file_loader.py @@ -213,12 +213,21 @@ def manipulate_bytecode(self, del sys.modules['_temp'] except KeyError: pass - py_compile.compile(mapping[name], invalidation_mode=invalidation_mode) - if not del_source: - bytecode_path = self.util.cache_from_source(mapping[name]) + if sys.implementation.cache_tag is None: + if del_source: + bytecode_path = mapping[name] + 'c' + py_compile.compile(mapping[name], bytecode_path, + invalidation_mode=invalidation_mode) + os.unlink(mapping[name]) + else: + raise unittest.SkipTest('requires sys.implementation.cache_tag') else: - os.unlink(mapping[name]) - bytecode_path = make_legacy_pyc(mapping[name]) + py_compile.compile(mapping[name], invalidation_mode=invalidation_mode) + if not del_source: + bytecode_path = self.util.cache_from_source(mapping[name]) + else: + os.unlink(mapping[name]) + bytecode_path = make_legacy_pyc(mapping[name]) if manipulator: with open(bytecode_path, 'rb') as file: bc = file.read() diff --git a/Lib/test/test_importlib/source/test_finder.py b/Lib/test/test_importlib/source/test_finder.py index c33e90232b36e6..91865b997ad364 100644 --- a/Lib/test/test_importlib/source/test_finder.py +++ b/Lib/test/test_importlib/source/test_finder.py @@ -57,6 +57,8 @@ def run_test(self, test, create=None, *, compile_=None, unlink=None): """ if create is None: create = {test} + if (compile_ or unlink) and sys.implementation.cache_tag is None: + raise unittest.SkipTest('requires sys.implementation.cache_tag') with util.create_modules(*create) as mapping: if compile_: for name in compile_: diff --git a/Lib/test/test_importlib/test_abc.py b/Lib/test/test_importlib/test_abc.py index 7c146ea853b0d9..b24c576646842f 100644 --- a/Lib/test/test_importlib/test_abc.py +++ b/Lib/test/test_importlib/test_abc.py @@ -533,7 +533,10 @@ class SourceLoader(SourceOnlyLoader): def __init__(self, path, magic=None): super().__init__(path) - self.bytecode_path = self.util.cache_from_source(self.path) + try: + self.bytecode_path = self.util.cache_from_source(self.path) + except NotImplementedError: + self.bytecode_path = None self.source_size = len(self.source) if magic is None: magic = self.util.MAGIC_NUMBER @@ -579,7 +582,10 @@ def setUp(self, *, is_package=True, **kwargs): module_name = 'mod' self.path = os.path.join(self.package, '.'.join(['mod', 'py'])) self.name = '.'.join([self.package, module_name]) - self.cached = self.util.cache_from_source(self.path) + try: + self.cached = self.util.cache_from_source(self.path) + except NotImplementedError: + self.cached = None self.loader = self.loader_mock(self.path, **kwargs) def verify_module(self, module): @@ -656,6 +662,8 @@ def test_get_source_encoding(self): @unittest.skipIf(sys.dont_write_bytecode, "sys.dont_write_bytecode is true") +@unittest.skipIf(sys.implementation.cache_tag is None, + "sys.implementation.cache_tag is None") class SourceLoaderBytecodeTests(SourceLoaderTestHarness): """Test importlib.abc.SourceLoader's use of bytecode. diff --git a/Lib/test/test_importlib/test_api.py b/Lib/test/test_importlib/test_api.py index 4de0cf029a81e0..70d93d693ae593 100644 --- a/Lib/test/test_importlib/test_api.py +++ b/Lib/test/test_importlib/test_api.py @@ -231,7 +231,6 @@ def test_reload_location_changed(self): # Start as a plain module. self.init.invalidate_caches() path = os.path.join(cwd, name + '.py') - cached = self.util.cache_from_source(path) expected = {'__name__': name, '__package__': '', '__file__': path, @@ -251,7 +250,6 @@ def test_reload_location_changed(self): # Change to a package. self.init.invalidate_caches() init_path = os.path.join(cwd, name, '__init__.py') - cached = self.util.cache_from_source(init_path) expected = {'__name__': name, '__package__': name, '__file__': init_path, @@ -281,7 +279,6 @@ def test_reload_namespace_changed(self): # Start as a namespace package. self.init.invalidate_caches() bad_path = os.path.join(cwd, name, '__init.py') - cached = self.util.cache_from_source(bad_path) expected = {'__name__': name, '__package__': name, '__doc__': None, @@ -310,7 +307,6 @@ def test_reload_namespace_changed(self): # Change to a regular package. self.init.invalidate_caches() init_path = os.path.join(cwd, name, '__init__.py') - cached = self.util.cache_from_source(init_path) expected = {'__name__': name, '__package__': name, '__file__': init_path, diff --git a/Lib/test/test_importlib/test_pkg_import.py b/Lib/test/test_importlib/test_pkg_import.py index 5ffae6222bacb8..287684efc85a91 100644 --- a/Lib/test/test_importlib/test_pkg_import.py +++ b/Lib/test/test_importlib/test_pkg_import.py @@ -39,9 +39,12 @@ def tearDown(self): self.remove_modules() def rewrite_file(self, contents): - compiled_path = cache_from_source(self.module_path) - if os.path.exists(compiled_path): - os.remove(compiled_path) + try: + compiled_path = cache_from_source(self.module_path) + if os.path.exists(compiled_path): + os.remove(compiled_path) + except NotImplementedError: + pass with open(self.module_path, 'w', encoding='utf-8') as f: f.write(contents) diff --git a/Lib/test/test_importlib/test_spec.py b/Lib/test/test_importlib/test_spec.py index b48d0a101ca9e7..77b6228940c3b9 100644 --- a/Lib/test/test_importlib/test_spec.py +++ b/Lib/test/test_importlib/test_spec.py @@ -52,7 +52,10 @@ class ModuleSpecTests: def setUp(self): self.name = 'spam' self.path = 'spam.py' - self.cached = self.util.cache_from_source(self.path) + try: + self.cached = self.util.cache_from_source(self.path) + except NotImplementedError: + self.cached = None self.loader = TestLoader() self.spec = self.machinery.ModuleSpec(self.name, self.loader) self.loc_spec = self.machinery.ModuleSpec(self.name, self.loader, @@ -184,6 +187,8 @@ def test_cached_with_origin_not_location(self): self.assertIs(spec.cached, None) + @unittest.skipIf(sys.implementation.cache_tag is None, + "sys.implementation.cache_tag is None") def test_cached_source(self): expected = self.util.cache_from_source(self.path) @@ -224,7 +229,10 @@ def bootstrap(self): def setUp(self): self.name = 'spam' self.path = 'spam.py' - self.cached = self.util.cache_from_source(self.path) + try: + self.cached = self.util.cache_from_source(self.path) + except NotImplementedError: + self.cached = None self.loader = TestLoader() self.spec = self.machinery.ModuleSpec(self.name, self.loader) self.loc_spec = self.machinery.ModuleSpec(self.name, self.loader, @@ -349,7 +357,10 @@ class FactoryTests: def setUp(self): self.name = 'spam' self.path = os.path.abspath('spam.py') - self.cached = self.util.cache_from_source(self.path) + try: + self.cached = self.util.cache_from_source(self.path) + except NotImplementedError: + self.cached = None self.loader = TestLoader() self.fileloader = TestLoader(self.path) self.pkgloader = TestLoader(self.path, True) diff --git a/Lib/test/test_importlib/test_util.py b/Lib/test/test_importlib/test_util.py index 17a211f10fa0ac..a926a7a4d408af 100644 --- a/Lib/test/test_importlib/test_util.py +++ b/Lib/test/test_importlib/test_util.py @@ -345,6 +345,8 @@ def test_cache_from_source_no_cache_tag(self): with self.assertRaises(NotImplementedError): self.util.cache_from_source('whatever.py') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_cache_from_source_no_dot(self): # Directory with a dot, filename without dot. path = os.path.join('foo.bar', 'file') @@ -353,12 +355,16 @@ def test_cache_from_source_no_dot(self): self.assertEqual(self.util.cache_from_source(path, optimization=''), expect) + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_cache_from_source_cwd(self): path = 'foo.py' expect = os.path.join('__pycache__', 'foo.{}.pyc'.format(self.tag)) self.assertEqual(self.util.cache_from_source(path, optimization=''), expect) + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_cache_from_source_optimization_empty_string(self): # Setting 'optimization' to '' leads to no optimization tag (PEP 488). path = 'foo.py' @@ -366,6 +372,8 @@ def test_cache_from_source_optimization_empty_string(self): self.assertEqual(self.util.cache_from_source(path, optimization=''), expect) + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_cache_from_source_optimization_None(self): # Setting 'optimization' to None uses the interpreter's optimization. # (PEP 488) @@ -382,6 +390,8 @@ def test_cache_from_source_optimization_None(self): self.assertEqual(self.util.cache_from_source(path, optimization=None), expect) + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_cache_from_source_optimization_set(self): # The 'optimization' parameter accepts anything that has a string repr # that passes str.alnum(). @@ -399,6 +409,8 @@ def test_cache_from_source_optimization_set(self): with self.assertRaises(ValueError): self.util.cache_from_source(path, optimization='path/is/bad') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_cache_from_source_debug_override_optimization_both_set(self): # Can only set one of the optimization-related parameters. with warnings.catch_warnings(): @@ -408,6 +420,8 @@ def test_cache_from_source_debug_override_optimization_both_set(self): @unittest.skipUnless(os.sep == '\\' and os.altsep == '/', 'test meaningful only where os.altsep is defined') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_sep_altsep_and_sep_cache_from_source(self): # Windows path and PEP 3147 where sep is right of altsep. self.assertEqual( @@ -440,44 +454,60 @@ def test_source_from_cache_no_cache_tag(self): with self.assertRaises(NotImplementedError): self.util.source_from_cache(path) + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_source_from_cache_bad_path(self): # When the path to a pyc file is not in PEP 3147 format, a ValueError # is raised. self.assertRaises( ValueError, self.util.source_from_cache, '/foo/bar/bazqux.pyc') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_source_from_cache_no_slash(self): # No slashes at all in path -> ValueError self.assertRaises( ValueError, self.util.source_from_cache, 'foo.cpython-32.pyc') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_source_from_cache_too_few_dots(self): # Too few dots in final path component -> ValueError self.assertRaises( ValueError, self.util.source_from_cache, '__pycache__/foo.pyc') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_source_from_cache_too_many_dots(self): with self.assertRaises(ValueError): self.util.source_from_cache( '__pycache__/foo.cpython-32.opt-1.foo.pyc') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_source_from_cache_not_opt(self): # Non-`opt-` path component -> ValueError self.assertRaises( ValueError, self.util.source_from_cache, '__pycache__/foo.cpython-32.foo.pyc') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_source_from_cache_no__pycache__(self): # Another problem with the path -> ValueError self.assertRaises( ValueError, self.util.source_from_cache, '/foo/bar/foo.cpython-32.foo.pyc') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_source_from_cache_optimized_bytecode(self): # Optimized bytecode is not an issue. path = os.path.join('__pycache__', 'foo.{}.opt-1.pyc'.format(self.tag)) self.assertEqual(self.util.source_from_cache(path), 'foo.py') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag to not be None') def test_source_from_cache_missing_optimization(self): # An empty optimization level is a no-no. path = os.path.join('__pycache__', 'foo.{}.opt-.pyc'.format(self.tag)) diff --git a/Lib/test/test_importlib/util.py b/Lib/test/test_importlib/util.py index efbec667317d5f..6399f952f9e912 100644 --- a/Lib/test/test_importlib/util.py +++ b/Lib/test/test_importlib/util.py @@ -292,6 +292,9 @@ def writes_bytecode_files(fxn): tests that require it to be set to False.""" if sys.dont_write_bytecode: return unittest.skip("relies on writing bytecode")(fxn) + if sys.implementation.cache_tag is None: + return unittest.skip("requires sys.implementation.cache_tag to not be None")(fxn) + @functools.wraps(fxn) def wrapper(*args, **kwargs): original = sys.dont_write_bytecode diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 1999aa770ecc56..e4a3a7d9add2c2 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6527,7 +6527,8 @@ def test_details(self): self.assertIn(module.__name__, output) self.assertIn(module.__spec__.origin, output) self.assertIn(module.__file__, output) - self.assertIn(module.__spec__.cached, output) + if module.__spec__.cached: + self.assertIn(module.__spec__.cached, output) self.assertEqual(err, b'') diff --git a/Lib/test/test_multiprocessing_main_handling.py b/Lib/test/test_multiprocessing_main_handling.py index 6b30a89316703b..fbd44fb0d3d5eb 100644 --- a/Lib/test/test_multiprocessing_main_handling.py +++ b/Lib/test/test_multiprocessing_main_handling.py @@ -196,9 +196,8 @@ def test_ipython_workaround(self): def test_script_compiled(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, 'script') - py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) self._check_script(pyc_file) def test_directory(self): @@ -213,9 +212,8 @@ def test_directory_compiled(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__', source=source) - py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) self._check_script(script_dir) def test_zipfile(self): @@ -231,7 +229,8 @@ def test_zipfile_compiled(self): with os_helper.temp_dir() as script_dir: script_name = _make_test_script(script_dir, '__main__', source=source) - compiled_name = py_compile.compile(script_name, doraise=True) + compiled_name = script_name + 'c' + py_compile.compile(script_name, compiled_name, doraise=True) zip_name, run_name = make_zip_script(script_dir, 'test_zip', compiled_name) self._check_script(zip_name) @@ -273,9 +272,8 @@ def test_package_compiled(self): make_pkg(pkg_dir) script_name = _make_test_script(pkg_dir, '__main__', source=source) - compiled_name = py_compile.compile(script_name, doraise=True) + pyc_file = import_helper.make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - pyc_file = import_helper.make_legacy_pyc(script_name) launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg') self._check_script(launch_name) diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py index 64387296e84621..66de61930968e4 100644 --- a/Lib/test/test_py_compile.py +++ b/Lib/test/test_py_compile.py @@ -56,7 +56,10 @@ def setUp(self): self.directory = tempfile.mkdtemp(dir=os.getcwd()) self.source_path = os.path.join(self.directory, '_test.py') self.pyc_path = self.source_path + 'c' - self.cache_path = importlib.util.cache_from_source(self.source_path) + try: + self.cache_path = importlib.util.cache_from_source(self.source_path) + except NotImplementedError: + self.cache_path = None self.cwd_drive = os.path.splitdrive(os.getcwd())[0] # In these tests we compute relative paths. When using Windows, the # current working directory path and the 'self.source_path' might be @@ -73,10 +76,31 @@ def tearDown(self): if self.cwd_drive: os.chdir(self.cwd_drive) + def assert_cache_path_exists(self, should_exist=True): + if self.cache_path: + if should_exist: + self.assertTrue(os.path.exists(self.cache_path)) + else: + self.assertFalse(os.path.exists(self.cache_path)) + return + cache_dir = os.path.join(self.directory, '__pycache__') + if not os.path.isdir(cache_dir): + if should_exist: + self.fail('no __pycache__ directory exists') + return + for f in os.listdir(cache_dir): + if f.startswith('_test.') and f.endswith('.pyc'): + if should_exist: + return + self.fail(f'__pycache__/{f} was created') + else: + if should_exist: + self.fail('no __pycache__/_test.*.pyc file exists') + def test_absolute_path(self): py_compile.compile(self.source_path, self.pyc_path) self.assertTrue(os.path.exists(self.pyc_path)) - self.assertFalse(os.path.exists(self.cache_path)) + self.assert_cache_path_exists(False) def test_do_not_overwrite_symlinks(self): # In the face of a cfile argument being a symlink, bail out. @@ -98,22 +122,24 @@ def test_do_not_overwrite_nonregular_files(self): with self.assertRaises(FileExistsError): py_compile.compile(self.source_path, os.devnull) + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag is not None') def test_cache_path(self): py_compile.compile(self.source_path) - self.assertTrue(os.path.exists(self.cache_path)) + self.assert_cache_path_exists(True) def test_cwd(self): with os_helper.change_cwd(self.directory): py_compile.compile(os.path.basename(self.source_path), os.path.basename(self.pyc_path)) self.assertTrue(os.path.exists(self.pyc_path)) - self.assertFalse(os.path.exists(self.cache_path)) + self.assert_cache_path_exists(False) def test_relative_path(self): py_compile.compile(os.path.relpath(self.source_path), os.path.relpath(self.pyc_path)) self.assertTrue(os.path.exists(self.pyc_path)) - self.assertFalse(os.path.exists(self.cache_path)) + self.assert_cache_path_exists(False) @os_helper.skip_if_dac_override @unittest.skipIf(os.name == 'nt', @@ -136,14 +162,14 @@ def test_bad_coding(self): 'tokenizedata', 'bad_coding2.py') with support.captured_stderr(): - self.assertIsNone(py_compile.compile(bad_coding, doraise=False)) - self.assertFalse(os.path.exists( - importlib.util.cache_from_source(bad_coding))) + self.assertIsNone(py_compile.compile(bad_coding, self.pyc_path, + doraise=False)) + self.assertFalse(os.path.exists(self.pyc_path)) def test_source_date_epoch(self): py_compile.compile(self.source_path, self.pyc_path) self.assertTrue(os.path.exists(self.pyc_path)) - self.assertFalse(os.path.exists(self.cache_path)) + self.assert_cache_path_exists(False) with open(self.pyc_path, 'rb') as fp: flags = importlib._bootstrap_external._classify_pyc( fp.read(), 'test', {}) @@ -155,6 +181,8 @@ def test_source_date_epoch(self): self.assertEqual(flags, expected_flags) @unittest.skipIf(sys.flags.optimize > 0, 'test does not work with -O') + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag is not None') def test_double_dot_no_clobber(self): # http://bugs.python.org/issue22966 # py_compile foo.bar.py -> __pycache__/foo.cpython-34.pyc @@ -174,6 +202,8 @@ def test_double_dot_no_clobber(self): 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') def test_optimization_path(self): # Specifying optimized bytecode should lead to a path reflecting that. self.assertIn('opt-2', py_compile.compile(self.source_path, optimize=2)) @@ -181,17 +211,19 @@ def test_optimization_path(self): def test_invalidation_mode(self): py_compile.compile( self.source_path, + self.pyc_path, invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH, ) - with open(self.cache_path, 'rb') as fp: + with open(self.pyc_path, 'rb') as fp: flags = importlib._bootstrap_external._classify_pyc( fp.read(), 'test', {}) self.assertEqual(flags, 0b11) py_compile.compile( self.source_path, + self.pyc_path, invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH, ) - with open(self.cache_path, 'rb') as fp: + with open(self.pyc_path, 'rb') as fp: flags = importlib._bootstrap_external._classify_pyc( fp.read(), 'test', {}) self.assertEqual(flags, 0b1) @@ -201,11 +233,11 @@ def test_quiet(self): 'tokenizedata', 'bad_coding2.py') with support.captured_stderr() as stderr: - self.assertIsNone(py_compile.compile(bad_coding, doraise=False, quiet=2)) - self.assertIsNone(py_compile.compile(bad_coding, doraise=True, quiet=2)) + self.assertIsNone(py_compile.compile(bad_coding, self.pyc_path, doraise=False, quiet=2)) + self.assertIsNone(py_compile.compile(bad_coding, self.pyc_path, doraise=True, quiet=2)) self.assertEqual(stderr.getvalue(), '') with self.assertRaises(py_compile.PyCompileError): - py_compile.compile(bad_coding, doraise=True, quiet=1) + py_compile.compile(bad_coding, self.pyc_path, doraise=True, quiet=1) class PyCompileTestsWithSourceEpoch(PyCompileTestsBase, @@ -227,8 +259,12 @@ class PyCompileCLITestCase(unittest.TestCase): def setUp(self): self.directory = tempfile.mkdtemp() self.source_path = os.path.join(self.directory, '_test.py') - self.cache_path = importlib.util.cache_from_source(self.source_path, - optimization='' if __debug__ else 1) + try: + self.cache_path = importlib.util.cache_from_source(self.source_path, + optimization='' if __debug__ else 1) + except NotImplementedError: + # py_compile.main() assumes legacy pyc path if there is no cache_tag + self.cache_path = self.source_path + 'c' with open(self.source_path, 'w') as file: file.write('x = 123\n') diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 0e113006cfa156..2e190d1b81be8e 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -1006,6 +1006,8 @@ def test_synopsis_sourceless(self): os = import_helper.import_fresh_module('os') expected = os.__doc__.splitlines()[0] filename = os.__spec__.cached + if not filename: + raise unittest.SkipTest('requires .pyc files') synopsis = pydoc.synopsis(filename) self.assertEqual(synopsis, expected) @@ -1013,10 +1015,10 @@ def test_synopsis_sourceless(self): def test_synopsis_sourceless_empty_doc(self): with os_helper.temp_cwd() as test_dir: init_path = os.path.join(test_dir, 'foomod42.py') - cached_path = importlib.util.cache_from_source(init_path) + cached_path = init_path + 'c' with open(init_path, 'w') as fobj: fobj.write("foo = 1") - py_compile.compile(init_path) + py_compile.compile(init_path, cached_path) synopsis = pydoc.synopsis(init_path, {}) self.assertIsNone(synopsis) synopsis_cached = pydoc.synopsis(cached_path, {}) diff --git a/Lib/test/test_reprlib.py b/Lib/test/test_reprlib.py index 22a55b57c076eb..5a95a05c6496a2 100644 --- a/Lib/test/test_reprlib.py +++ b/Lib/test/test_reprlib.py @@ -695,8 +695,11 @@ def _check_path_limitations(self, module_name): source_path_len += 2 * (len(self.longname) + 1) # a path separator + `module_name` + ".py" source_path_len += len(module_name) + 1 + len(".py") - cached_path_len = (source_path_len + - len(importlib.util.cache_from_source("x.py")) - len("x.py")) + try: + cached_path_len = (source_path_len + + len(importlib.util.cache_from_source("x.py")) - len("x.py")) + except NotImplementedError: + cached_path_len = source_path_len if os.name == 'nt' and cached_path_len >= 258: # Under Windows, the max path len is 260 including C's terminating # NUL character. diff --git a/Lib/test/test_runpy.py b/Lib/test/test_runpy.py index 254a009a69718b..9f3bc8973eb8ac 100644 --- a/Lib/test/test_runpy.py +++ b/Lib/test/test_runpy.py @@ -320,14 +320,16 @@ def create_ns(init_globals): self.check_code_execution(create_ns, expected_ns) importlib.invalidate_caches() __import__(mod_name) - os.remove(mod_fname) if not sys.dont_write_bytecode: - make_legacy_pyc(mod_fname) + make_legacy_pyc(mod_fname, allow_compile=True) unload(mod_name) # In case loader caches paths + os.remove(mod_fname) importlib.invalidate_caches() if verbose > 1: print("Running from compiled:", mod_name) self._fix_ns_for_legacy_pyc(expected_ns, alter_sys) self.check_code_execution(create_ns, expected_ns) + else: + os.remove(mod_fname) finally: self._del_pkg(pkg_dir) if verbose > 1: print("Module executed successfully") @@ -360,14 +362,16 @@ def create_ns(init_globals): self.check_code_execution(create_ns, expected_ns) importlib.invalidate_caches() __import__(mod_name) - os.remove(mod_fname) if not sys.dont_write_bytecode: - make_legacy_pyc(mod_fname) + make_legacy_pyc(mod_fname, allow_compile=True) unload(mod_name) # In case loader caches paths + os.remove(mod_fname) if verbose > 1: print("Running from compiled:", pkg_name) importlib.invalidate_caches() self._fix_ns_for_legacy_pyc(expected_ns, alter_sys) self.check_code_execution(create_ns, expected_ns) + else: + os.remove(mod_fname) finally: self._del_pkg(pkg_dir) if verbose > 1: print("Package executed successfully") @@ -420,7 +424,7 @@ def _check_relative_imports(self, depth, run_name=None): importlib.invalidate_caches() __import__(mod_name) os.remove(mod_fname) - if not sys.dont_write_bytecode: + if not sys.dont_write_bytecode and sys.implementation.cache_tag: make_legacy_pyc(mod_fname) unload(mod_name) # In case the loader caches paths if verbose > 1: print("Running from compiled:", mod_name) @@ -676,6 +680,8 @@ def test_basic_script_no_suffix(self): self._check_script(script_name, "", script_name, script_name, expect_spec=False) + @unittest.skipIf(sys.implementation.cache_tag is None, + 'requires sys.implementation.cache_tag') def test_script_compiled(self): with temp_dir() as script_dir: mod_name = 'script' @@ -696,12 +702,10 @@ def test_directory_compiled(self): with temp_dir() as script_dir: mod_name = '__main__' script_name = self._make_test_script(script_dir, mod_name) - compiled_name = py_compile.compile(script_name, doraise=True) + legacy_pyc = make_legacy_pyc(script_name, allow_compile=True) os.remove(script_name) - if not sys.dont_write_bytecode: - legacy_pyc = make_legacy_pyc(script_name) - self._check_script(script_dir, "", legacy_pyc, - script_dir, mod_name=mod_name) + self._check_script(script_dir, "", legacy_pyc, + script_dir, mod_name=mod_name) def test_directory_error(self): with temp_dir() as script_dir: @@ -722,7 +726,8 @@ def test_zipfile_compiled(self): with temp_dir() as script_dir: mod_name = '__main__' script_name = self._make_test_script(script_dir, mod_name) - compiled_name = py_compile.compile(script_name, doraise=True) + compiled_name = script_name + 'c' + py_compile.compile(script_name, compiled_name, doraise=True) zip_name, fname = make_zip_script(script_dir, 'test_zip', compiled_name) self._check_script(zip_name, "", fname, zip_name, diff --git a/Lib/test/test_zipimport.py b/Lib/test/test_zipimport.py index dce3e1d9d38e7a..76cd85709a63af 100644 --- a/Lib/test/test_zipimport.py +++ b/Lib/test/test_zipimport.py @@ -60,7 +60,6 @@ def module_path_to_dotted_name(path): TEMP_ZIP = os.path.abspath("junk95142.zip") TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "zipimport_data") -pyc_file = importlib.util.cache_from_source(TESTMOD + '.py') pyc_ext = '.pyc' diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index ac2332e58468a2..8234bf52d39c5f 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -2223,10 +2223,10 @@ def writepy(self, pathname, basename="", filterfunc=None): basename = name if self.debug: print("Adding package in", pathname, "as", basename) - fname, arcname = self._get_codename(initname[0:-3], basename) + arcname, bytecode = self._get_code(initname[0:-3], basename) if self.debug: print("Adding", arcname) - self.write(fname, arcname) + self.writestr(arcname, bytecode) dirlist = sorted(os.listdir(pathname)) dirlist.remove("__init__.py") # Add all *.py files and package subdirectories @@ -2243,11 +2243,10 @@ def writepy(self, pathname, basename="", filterfunc=None): if self.debug: print('file %r skipped by filterfunc' % path) continue - fname, arcname = self._get_codename(path[0:-3], - basename) + arcname, bytecode = self._get_code(path[0:-3], basename) if self.debug: print("Adding", arcname) - self.write(fname, arcname) + self.writestr(arcname, bytecode) else: # This is NOT a package directory, add its files at top level if self.debug: @@ -2260,101 +2259,58 @@ def writepy(self, pathname, basename="", filterfunc=None): if self.debug: print('file %r skipped by filterfunc' % path) continue - fname, arcname = self._get_codename(path[0:-3], - basename) + arcname, bytecode = self._get_code(path[0:-3], basename) if self.debug: print("Adding", arcname) - self.write(fname, arcname) + self.writestr(arcname, bytecode) else: if pathname[-3:] != ".py": raise RuntimeError( 'Files added with writepy() must end with ".py"') - fname, arcname = self._get_codename(pathname[0:-3], basename) + arcname, bytecode = self._get_code(pathname[0:-3], basename) if self.debug: print("Adding file", arcname) - self.write(fname, arcname) + self.writestr(arcname, bytecode) - def _get_codename(self, pathname, basename): - """Return (filename, archivename) for the path. + def _get_code(self, pathname, basename): + """Return (arcname, bytecode) for the path. - Given a module name path, return the correct file path and - archive name, compiling if necessary. For example, given - /python/lib/string, return (/python/lib/string.pyc, string). + Given a module name path, return the bytecode and archive + name. For example, given /python/lib/string, return + ('string', b''). """ - def _compile(file, optimize=-1): - import py_compile - if self.debug: - print("Compiling", file) - try: - py_compile.compile(file, doraise=True, optimize=optimize) - except py_compile.PyCompileError as err: - print(err.msg) - return False - return True + import importlib._bootstrap_external + import importlib.machinery file_py = pathname + ".py" file_pyc = pathname + ".pyc" - pycache_opt0 = importlib.util.cache_from_source(file_py, optimization='') - pycache_opt1 = importlib.util.cache_from_source(file_py, optimization=1) - pycache_opt2 = importlib.util.cache_from_source(file_py, optimization=2) - if self._optimize == -1: - # legacy mode: use whatever file is present - if (os.path.isfile(file_pyc) and - os.stat(file_pyc).st_mtime >= os.stat(file_py).st_mtime): - # Use .pyc file. - arcname = fname = file_pyc - elif (os.path.isfile(pycache_opt0) and - os.stat(pycache_opt0).st_mtime >= os.stat(file_py).st_mtime): - # Use the __pycache__/*.pyc file, but write it to the legacy pyc - # file name in the archive. - fname = pycache_opt0 - arcname = file_pyc - elif (os.path.isfile(pycache_opt1) and - os.stat(pycache_opt1).st_mtime >= os.stat(file_py).st_mtime): - # Use the __pycache__/*.pyc file, but write it to the legacy pyc - # file name in the archive. - fname = pycache_opt1 - arcname = file_pyc - elif (os.path.isfile(pycache_opt2) and - os.stat(pycache_opt2).st_mtime >= os.stat(file_py).st_mtime): - # Use the __pycache__/*.pyc file, but write it to the legacy pyc - # file name in the archive. - fname = pycache_opt2 - arcname = file_pyc - else: - # Compile py into PEP 3147 pyc file. - if _compile(file_py): - if sys.flags.optimize == 0: - fname = pycache_opt0 - elif sys.flags.optimize == 1: - fname = pycache_opt1 - else: - fname = pycache_opt2 - arcname = file_pyc - else: - fname = arcname = file_py + archivename = os.path.split(file_pyc)[1] + + loader = importlib.machinery.SourceFileLoader('', file_py) + source_bytes = loader.get_data(file_py) + try: + if self.debug: + print("Compiling", file_py) + code = loader.source_to_code(source_bytes, archivename) + except Exception as err: + # Historically, this function prints messages here rather than raising + # (see test_zipfile.test_write_filtered_python_package) + from py_compile import PyCompileError + print(PyCompileError(type(err), err, file_py).msg) + + archivename = os.path.split(file_py)[1] + bytecode = source_bytes else: - # new mode: use given optimization level - if self._optimize == 0: - fname = pycache_opt0 - arcname = file_pyc - else: - arcname = file_pyc - if self._optimize == 1: - fname = pycache_opt1 - elif self._optimize == 2: - fname = pycache_opt2 - else: - msg = "invalid value for 'optimize': {!r}".format(self._optimize) - raise ValueError(msg) - if not (os.path.isfile(fname) and - os.stat(fname).st_mtime >= os.stat(file_py).st_mtime): - if not _compile(file_py, optimize=self._optimize): - fname = arcname = file_py - archivename = os.path.split(arcname)[1] + # Historically this function has used timestamp comparisons, so we + # keep using it until someone makes that specific improvement. + source_stats = loader.path_stats(file_py) + bytecode = importlib._bootstrap_external._code_to_timestamp_pyc( + code, source_stats['mtime'], source_stats['size']) + if basename: archivename = "%s/%s" % (basename, archivename) - return (fname, archivename) + + return archivename, bytecode def main(args=None): diff --git a/Modules/_testlimitedcapi/import.c b/Modules/_testlimitedcapi/import.c index f85daee57d712e..f572212ba88b76 100644 --- a/Modules/_testlimitedcapi/import.c +++ b/Modules/_testlimitedcapi/import.c @@ -22,7 +22,7 @@ static PyObject * pyimport_getmagictag(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { const char *tag = PyImport_GetMagicTag(); - return PyUnicode_FromString(tag); + return tag ? PyUnicode_FromString(tag) : Py_NewRef(Py_None); } diff --git a/PC/pyconfig.h b/PC/pyconfig.h index a126fca6f5aafb..c9c1015e7a9bc9 100644 --- a/PC/pyconfig.h +++ b/PC/pyconfig.h @@ -765,4 +765,7 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ // Truncate the thread name to 32766 characters. #define _PYTHREAD_NAME_MAXLEN 32766 +// DO NOT MERGE +#define _PY_DISABLE_SYS_CACHE_TAG + #endif /* !Py_CONFIG_H */ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 94eb3164ecad58..111e3ae2650752 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -3572,7 +3572,11 @@ make_version_info(PyThreadState *tstate) const char *_PySys_ImplName = NAME; #define MAJOR Py_STRINGIFY(PY_MAJOR_VERSION) #define MINOR Py_STRINGIFY(PY_MINOR_VERSION) +#ifdef _PY_DISABLE_SYS_CACHE_TAG +#define TAG NULL +#else #define TAG NAME "-" MAJOR MINOR +#endif const char *_PySys_ImplCacheTag = TAG; #undef NAME #undef MAJOR @@ -3599,9 +3603,12 @@ make_impl_info(PyObject *version_info) if (res < 0) goto error; - value = PyUnicode_FromString(_PySys_ImplCacheTag); - if (value == NULL) + value = _PySys_ImplCacheTag + ? PyUnicode_FromString(_PySys_ImplCacheTag) + : Py_NewRef(Py_None); + if (value == NULL) { goto error; + } res = PyDict_SetItemString(impl_info, "cache_tag", value); Py_DECREF(value); if (res < 0) From d831daa387592411b27b327a79180d34ef9c659f Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 27 Jan 2026 23:42:20 +0000 Subject: [PATCH 03/29] Respect allow_compile flag --- Lib/test/support/import_helper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/support/import_helper.py b/Lib/test/support/import_helper.py index f1642e51daf815..a320ba70f72f81 100644 --- a/Lib/test/support/import_helper.py +++ b/Lib/test/support/import_helper.py @@ -71,6 +71,8 @@ def make_legacy_pyc(source, allow_compile=False): try: pyc_file = importlib.util.cache_from_source(source) except NotImplementedError: + if not allow_compile: + raise py_compile.compile(source, legacy_pyc, doraise=True) else: shutil.move(pyc_file, legacy_pyc) From 1fd6a3097fc4c10b6f3188dff8ab25022c23afa6 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 27 Jan 2026 23:44:08 +0000 Subject: [PATCH 04/29] Remove unused import --- Lib/test/test_argparse.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index d2e9903f897bed..78f02f70b9f0fc 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7,7 +7,6 @@ import io import operator import os -import py_compile import shutil import stat import sys From 601e5375ba15b24f2b6ab8c9a493ba4d422ceec4 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 27 Jan 2026 23:55:41 +0000 Subject: [PATCH 05/29] Fix NEWS reference --- .../next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst b/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst index fa6b0c6e505cdb..687a469f170cc8 100644 --- a/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst +++ b/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst @@ -1,4 +1,4 @@ Enables patching in a ``_PY_DISABLE_SYS_CACHE_TAG`` preprocessor definition -when building to force :data:`sys.implementation.cache_tag` to ``None``. +when building to force :attr:`sys.implementation.cache_tag` to ``None``. This has the effect of completely disabling automatic creation and reading of ``.pyc`` files. From df97ccdb7fbff3e101bbe7192de5c81379265154 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 28 Jan 2026 13:49:27 +0000 Subject: [PATCH 06/29] Make TAG definable instaed --- Lib/test/support/import_helper.py | 5 ++--- PC/pyconfig.h | 3 --- Python/sysmodule.c | 7 ++++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Lib/test/support/import_helper.py b/Lib/test/support/import_helper.py index a320ba70f72f81..093de6a82d8ca7 100644 --- a/Lib/test/support/import_helper.py +++ b/Lib/test/support/import_helper.py @@ -70,12 +70,11 @@ def make_legacy_pyc(source, allow_compile=False): legacy_pyc = source + 'c' try: pyc_file = importlib.util.cache_from_source(source) - except NotImplementedError: + shutil.move(pyc_file, legacy_pyc) + except (FileNotFoundError, NotImplementedError): if not allow_compile: raise py_compile.compile(source, legacy_pyc, doraise=True) - else: - shutil.move(pyc_file, legacy_pyc) return legacy_pyc diff --git a/PC/pyconfig.h b/PC/pyconfig.h index c9c1015e7a9bc9..a126fca6f5aafb 100644 --- a/PC/pyconfig.h +++ b/PC/pyconfig.h @@ -765,7 +765,4 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ // Truncate the thread name to 32766 characters. #define _PYTHREAD_NAME_MAXLEN 32766 -// DO NOT MERGE -#define _PY_DISABLE_SYS_CACHE_TAG - #endif /* !Py_CONFIG_H */ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 111e3ae2650752..87872e61da1423 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1,3 +1,6 @@ +// DO NOT MERGE +#define TAG NULL + /* System module */ @@ -3572,9 +3575,7 @@ make_version_info(PyThreadState *tstate) const char *_PySys_ImplName = NAME; #define MAJOR Py_STRINGIFY(PY_MAJOR_VERSION) #define MINOR Py_STRINGIFY(PY_MINOR_VERSION) -#ifdef _PY_DISABLE_SYS_CACHE_TAG -#define TAG NULL -#else +#ifndef TAG #define TAG NAME "-" MAJOR MINOR #endif const char *_PySys_ImplCacheTag = TAG; From cbd1e15c69781d85053c203a9abbb89e8af20eea Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 28 Jan 2026 15:18:21 +0000 Subject: [PATCH 07/29] Suppress pip compile without cache tag --- Lib/ensurepip/__init__.py | 2 ++ .../Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index f9f905f46ff09a..db8f0ac630edc0 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -177,6 +177,8 @@ def _bootstrap(*, root=None, upgrade=False, user=False, args += ["--user"] if verbosity: args += ["-" + "v" * verbosity] + if sys.implementation.cache_tag is None: + args += ["--no-compile"] return _run_pip([*args, "pip"], [os.fsdecode(tmp_wheel_path)]) diff --git a/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst b/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst index 687a469f170cc8..63a69d3115bcf4 100644 --- a/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst +++ b/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst @@ -1,4 +1,4 @@ -Enables patching in a ``_PY_DISABLE_SYS_CACHE_TAG`` preprocessor definition -when building to force :attr:`sys.implementation.cache_tag` to ``None``. -This has the effect of completely disabling automatic creation and reading -of ``.pyc`` files. +Enables patching the ``TAG`` preprocessor definition to override +:attr:`sys.implementation.cache_tag` at build time. Setting it to ``NULL`` +has the effect of completely disabling automatic creation and use of +``.pyc`` files. From 0429aa297c7d28abac3be8e5ea41439991e41a31 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 28 Jan 2026 15:27:57 +0000 Subject: [PATCH 08/29] Test ensurepip for new flag --- Lib/test/test_ensurepip.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_ensurepip.py b/Lib/test/test_ensurepip.py index f6743d57ca28dd..c62b340f6a340f 100644 --- a/Lib/test/test_ensurepip.py +++ b/Lib/test/test_ensurepip.py @@ -12,6 +12,12 @@ import ensurepip._uninstall +if sys.implementation.cache_tag is None: + COMPILE_OPT = ["--no-compile"] +else: + COMPILE_OPT = [] + + class TestPackages(unittest.TestCase): def touch(self, directory, filename): fullname = os.path.join(directory, filename) @@ -85,7 +91,7 @@ def test_basic_bootstrapping(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "pip", + unittest.mock.ANY, *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -99,7 +105,7 @@ def test_bootstrapping_with_root(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "--root", "/foo/bar/", + unittest.mock.ANY, "--root", "/foo/bar/", *COMPILE_OPT, "pip", ], unittest.mock.ANY, @@ -111,7 +117,7 @@ def test_bootstrapping_with_user(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "--user", "pip", + unittest.mock.ANY, "--user", *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -122,7 +128,7 @@ def test_bootstrapping_with_upgrade(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "--upgrade", "pip", + unittest.mock.ANY, "--upgrade", *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -133,7 +139,7 @@ def test_bootstrapping_with_verbosity_1(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "-v", "pip", + unittest.mock.ANY, "-v", *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -144,7 +150,7 @@ def test_bootstrapping_with_verbosity_2(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "-vv", "pip", + unittest.mock.ANY, "-vv", *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -155,7 +161,7 @@ def test_bootstrapping_with_verbosity_3(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "-vvv", "pip", + unittest.mock.ANY, "-vvv", *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) @@ -312,7 +318,7 @@ def test_basic_bootstrapping(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "pip", + unittest.mock.ANY, *COMPILE_OPT, "pip", ], unittest.mock.ANY, ) From b8e633569e5163f71cc11eb54d7a87c1a947c767 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 28 Jan 2026 15:41:29 +0000 Subject: [PATCH 09/29] Fix NEWS and suppress pip compilation for testing --- .github/workflows/build.yml | 1 + .../Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7f7aa5172e082..11bf6def9ff9e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,7 @@ concurrency: env: FORCE_COLOR: 1 + PIP_NO_COMPILE: 1 jobs: build-context: diff --git a/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst b/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst index 63a69d3115bcf4..3965a5036a8e57 100644 --- a/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst +++ b/Misc/NEWS.d/next/Build/2026-01-27-23-39-26.gh-issue-144278.tejFwL.rst @@ -1,4 +1,4 @@ Enables patching the ``TAG`` preprocessor definition to override -:attr:`sys.implementation.cache_tag` at build time. Setting it to ``NULL`` -has the effect of completely disabling automatic creation and use of -``.pyc`` files. +:data:`sys.implementation.cache_tag ` at build time. +Setting it to ``NULL`` has the effect of completely disabling automatic +creation and use of ``.pyc`` files. From 142f0098f98e702ec3a620cf16bb20beb3b5bc50 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 28 Jan 2026 16:26:13 +0000 Subject: [PATCH 10/29] Set env on each step --- .github/workflows/build.yml | 5 ++++- .github/workflows/reusable-cifuzz.yml | 3 +++ .github/workflows/reusable-docs.yml | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 11bf6def9ff9e5..f585e897e96df9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,6 @@ concurrency: env: FORCE_COLOR: 1 - PIP_NO_COMPILE: 1 jobs: build-context: @@ -431,6 +430,8 @@ jobs: env: OPENSSL_VER: 3.0.18 PYTHONSTRICTEXTENSIONBUILD: 1 + # DO NOT MERGE + PIP_NO_COMPILE: 1 steps: - uses: actions/checkout@v6 with: @@ -543,6 +544,8 @@ jobs: OPENSSL_VER: 3.0.18 PYTHONSTRICTEXTENSIONBUILD: 1 ASAN_OPTIONS: detect_leaks=0:allocator_may_return_null=1:handle_segv=0 + # DO NOT MERGE + PIP_NO_COMPILE: 1 steps: - uses: actions/checkout@v6 with: diff --git a/.github/workflows/reusable-cifuzz.yml b/.github/workflows/reusable-cifuzz.yml index 1986f5fb2cc640..a28e549839262c 100644 --- a/.github/workflows/reusable-cifuzz.yml +++ b/.github/workflows/reusable-cifuzz.yml @@ -18,6 +18,9 @@ jobs: name: ${{ inputs.oss-fuzz-project-name }} (${{ inputs.sanitizer }}) runs-on: ubuntu-latest timeout-minutes: 60 + env: + # DO NOT MERGE + PIP_NO_COMPILE: 1 steps: - name: Build fuzzers (${{ inputs.sanitizer }}) id: build diff --git a/.github/workflows/reusable-docs.yml b/.github/workflows/reusable-docs.yml index fc68c040fca059..5c5623ef02726d 100644 --- a/.github/workflows/reusable-docs.yml +++ b/.github/workflows/reusable-docs.yml @@ -81,6 +81,9 @@ jobs: name: 'Doctest' runs-on: ubuntu-24.04 timeout-minutes: 60 + env: + # DO NOT MERGE + PIP_NO_COMPILE: 1 steps: - uses: actions/checkout@v6 with: From 2b0f29cd38470c35f1014fdbdf9a1661157d9501 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 28 Jan 2026 16:59:09 +0000 Subject: [PATCH 11/29] Make compileall silently ignore missing cache_tag --- .github/workflows/build.yml | 4 ---- .github/workflows/reusable-cifuzz.yml | 3 --- .github/workflows/reusable-docs.yml | 3 --- Lib/compileall.py | 9 +++++++++ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f585e897e96df9..e7f7aa5172e082 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -430,8 +430,6 @@ jobs: env: OPENSSL_VER: 3.0.18 PYTHONSTRICTEXTENSIONBUILD: 1 - # DO NOT MERGE - PIP_NO_COMPILE: 1 steps: - uses: actions/checkout@v6 with: @@ -544,8 +542,6 @@ jobs: OPENSSL_VER: 3.0.18 PYTHONSTRICTEXTENSIONBUILD: 1 ASAN_OPTIONS: detect_leaks=0:allocator_may_return_null=1:handle_segv=0 - # DO NOT MERGE - PIP_NO_COMPILE: 1 steps: - uses: actions/checkout@v6 with: diff --git a/.github/workflows/reusable-cifuzz.yml b/.github/workflows/reusable-cifuzz.yml index a28e549839262c..1986f5fb2cc640 100644 --- a/.github/workflows/reusable-cifuzz.yml +++ b/.github/workflows/reusable-cifuzz.yml @@ -18,9 +18,6 @@ jobs: name: ${{ inputs.oss-fuzz-project-name }} (${{ inputs.sanitizer }}) runs-on: ubuntu-latest timeout-minutes: 60 - env: - # DO NOT MERGE - PIP_NO_COMPILE: 1 steps: - name: Build fuzzers (${{ inputs.sanitizer }}) id: build diff --git a/.github/workflows/reusable-docs.yml b/.github/workflows/reusable-docs.yml index 5c5623ef02726d..fc68c040fca059 100644 --- a/.github/workflows/reusable-docs.yml +++ b/.github/workflows/reusable-docs.yml @@ -81,9 +81,6 @@ jobs: name: 'Doctest' runs-on: ubuntu-24.04 timeout-minutes: 60 - env: - # DO NOT MERGE - PIP_NO_COMPILE: 1 steps: - uses: actions/checkout@v6 with: diff --git a/Lib/compileall.py b/Lib/compileall.py index 9519a5ac16f024..0d8edb3c107712 100644 --- a/Lib/compileall.py +++ b/Lib/compileall.py @@ -165,6 +165,15 @@ def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0, stripdir = os.fspath(stripdir) if stripdir is not None else None name = os.path.basename(fullname) + # Without a cache_tag, we can only create legacy .pyc files. None of our + # callers seem to expect this, so the best we can do is silently succeed + # without creating anything. + if not legacy and sys.implementation.cache_tag is None: + if not quiet: + print("No cache tag is available to generate .pyc path for {!r}" + .format(fullname)) + return True + dfile = None if ddir is not None: From 29502a7f7321f0bd4701c7709a7e024c313cb2de Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 28 Jan 2026 17:59:26 +0000 Subject: [PATCH 12/29] Fail instead --- Lib/compileall.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/compileall.py b/Lib/compileall.py index 0d8edb3c107712..39eadcb032eb81 100644 --- a/Lib/compileall.py +++ b/Lib/compileall.py @@ -166,13 +166,12 @@ def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0, name = os.path.basename(fullname) # Without a cache_tag, we can only create legacy .pyc files. None of our - # callers seem to expect this, so the best we can do is silently succeed - # without creating anything. + # callers seem to expect this, so the best we can do is fail without raising if not legacy and sys.implementation.cache_tag is None: if not quiet: print("No cache tag is available to generate .pyc path for {!r}" .format(fullname)) - return True + return False dfile = None From bd2731a12eeceb8c6777163a53879435d6fc1138 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 28 Jan 2026 22:34:31 +0000 Subject: [PATCH 13/29] Test without null tag --- Python/sysmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 87872e61da1423..53ec6378ce89b2 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1,5 +1,5 @@ // DO NOT MERGE -#define TAG NULL +//#define TAG NULL /* System module */ From c51bcca2b2f05019143b52a79b6f6e7ec51f43d6 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 28 Jan 2026 22:35:19 +0000 Subject: [PATCH 14/29] Avoid unnecessary .format --- Lib/compileall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/compileall.py b/Lib/compileall.py index 39eadcb032eb81..c452aed135838f 100644 --- a/Lib/compileall.py +++ b/Lib/compileall.py @@ -169,8 +169,8 @@ def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0, # callers seem to expect this, so the best we can do is fail without raising if not legacy and sys.implementation.cache_tag is None: if not quiet: - print("No cache tag is available to generate .pyc path for {!r}" - .format(fullname)) + print("No cache tag is available to generate .pyc path for", + repr(fullname)) return False dfile = None From 11546af67995623e75003522fb649601f882b445 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 28 Jan 2026 22:56:45 +0000 Subject: [PATCH 15/29] Make TAG NULL again --- Python/sysmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 53ec6378ce89b2..87872e61da1423 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1,5 +1,5 @@ // DO NOT MERGE -//#define TAG NULL +#define TAG NULL /* System module */ From 20edcd4239783c6c4e3ca0330130edaf056c5e44 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 29 Jan 2026 00:11:30 +0000 Subject: [PATCH 16/29] Potential fast path --- Lib/importlib/_bootstrap_external.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index b576ceb1ce9f6e..1c05b40f4e8e35 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -820,10 +820,13 @@ def get_code(self, fullname): source_hash = None hash_based = False check_source = True - try: - bytecode_path = cache_from_source(source_path) - except NotImplementedError: - bytecode_path = None + bytecode_path = None + # TESTING: Potential fast path + if sys.implementation.cache_tag is not None: + try: + bytecode_path = cache_from_source(source_path) + except NotImplementedError: + pass else: try: st = self.path_stats(source_path) From 4ab201b23036548365afc64c0028fa510b6c337b Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 29 Jan 2026 00:12:06 +0000 Subject: [PATCH 17/29] Fix fast path --- Lib/importlib/_bootstrap_external.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 1c05b40f4e8e35..576de6596bda96 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -827,7 +827,7 @@ def get_code(self, fullname): bytecode_path = cache_from_source(source_path) except NotImplementedError: pass - else: + if bytecode_path: try: st = self.path_stats(source_path) except OSError: From d60a6be2f364d8d782251b1a0ac63122563e290a Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 29 Jan 2026 17:34:31 +0000 Subject: [PATCH 18/29] Check running tests without existing .pyc files --- .github/workflows/reusable-windows.yml | 3 +++ Python/sysmodule.c | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-windows.yml b/.github/workflows/reusable-windows.yml index 82ea819867ef6d..e7449251be6ac4 100644 --- a/.github/workflows/reusable-windows.yml +++ b/.github/workflows/reusable-windows.yml @@ -41,6 +41,9 @@ jobs: shell: bash - name: Display build info run: .\\python.bat -m test.pythoninfo + - name: DO NOT MERGE cleanup pyc + run: dir Lib/*.pyc -r | del + shell: powershell - name: Tests run: >- .\\PCbuild\\rt.bat diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 87872e61da1423..53ec6378ce89b2 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1,5 +1,5 @@ // DO NOT MERGE -#define TAG NULL +//#define TAG NULL /* System module */ From 97d34e261255df4cc7abf14f01093a4add05f406 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 29 Jan 2026 20:10:41 +0000 Subject: [PATCH 19/29] Try interning name and path eagerly --- .github/workflows/reusable-windows.yml | 3 --- Lib/importlib/_bootstrap_external.py | 17 +++++++---------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/reusable-windows.yml b/.github/workflows/reusable-windows.yml index e7449251be6ac4..82ea819867ef6d 100644 --- a/.github/workflows/reusable-windows.yml +++ b/.github/workflows/reusable-windows.yml @@ -41,9 +41,6 @@ jobs: shell: bash - name: Display build info run: .\\python.bat -m test.pythoninfo - - name: DO NOT MERGE cleanup pyc - run: dir Lib/*.pyc -r | del - shell: powershell - name: Tests run: >- .\\PCbuild\\rt.bat diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 576de6596bda96..20f93ce9956b36 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -820,14 +820,11 @@ def get_code(self, fullname): source_hash = None hash_based = False check_source = True - bytecode_path = None - # TESTING: Potential fast path - if sys.implementation.cache_tag is not None: - try: - bytecode_path = cache_from_source(source_path) - except NotImplementedError: - pass - if bytecode_path: + try: + bytecode_path = cache_from_source(source_path) + except NotImplementedError: + bytecode_path = None + else: try: st = self.path_stats(source_path) except OSError: @@ -904,8 +901,8 @@ class FileLoader: def __init__(self, fullname, path): """Cache the module name and the path to the file found by the finder.""" - self.name = fullname - self.path = path + self.name = sys.intern(fullname) + self.path = sys.intern(path) def __eq__(self, other): return (self.__class__ == other.__class__ and From 2f8fa9ebaea903ef70264db0549d9e48bb51bf93 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 29 Jan 2026 20:20:03 +0000 Subject: [PATCH 20/29] Don't intern name tuple --- Lib/importlib/_bootstrap_external.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 20f93ce9956b36..42aa58d3709ac8 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -901,7 +901,7 @@ class FileLoader: def __init__(self, fullname, path): """Cache the module name and the path to the file found by the finder.""" - self.name = sys.intern(fullname) + self.name = fullname self.path = sys.intern(path) def __eq__(self, other): From 003e6c13e96fe7b875a20377eb45c6e77b8b0df7 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 29 Jan 2026 20:38:09 +0000 Subject: [PATCH 21/29] Fix flag and only intern strings --- Lib/importlib/_bootstrap_external.py | 5 ++++- Python/sysmodule.c | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 42aa58d3709ac8..851e4fec9a21a5 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -902,7 +902,10 @@ def __init__(self, fullname, path): """Cache the module name and the path to the file found by the finder.""" self.name = fullname - self.path = sys.intern(path) + if isinstance(path, str): + self.path = sys.intern(path) + else: + sys.path = path def __eq__(self, other): return (self.__class__ == other.__class__ and diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 53ec6378ce89b2..87872e61da1423 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1,5 +1,5 @@ // DO NOT MERGE -//#define TAG NULL +#define TAG NULL /* System module */ From 29666424eae36cd0f8f838d469ac30395b30238b Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 29 Jan 2026 20:41:28 +0000 Subject: [PATCH 22/29] Spelling --- Lib/importlib/_bootstrap_external.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 851e4fec9a21a5..c9fa9630ccfe63 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -905,7 +905,7 @@ def __init__(self, fullname, path): if isinstance(path, str): self.path = sys.intern(path) else: - sys.path = path + self.path = path def __eq__(self, other): return (self.__class__ == other.__class__ and From 2d5912e51e6fa9a3fba4e753bebea45680b702ba Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 30 Jan 2026 01:09:40 +0000 Subject: [PATCH 23/29] Unconditionally skip .pyc creation to see if that's the cause --- Lib/importlib/_bootstrap_external.py | 7 +++---- Python/sysmodule.c | 9 ++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index c9fa9630ccfe63..b95c9e09bd9bc2 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -821,6 +821,8 @@ def get_code(self, fullname): hash_based = False check_source = True try: + # DO NOT MERGE - just testing unconditionally skipping this path + raise NotImplementedError() bytecode_path = cache_from_source(source_path) except NotImplementedError: bytecode_path = None @@ -902,10 +904,7 @@ def __init__(self, fullname, path): """Cache the module name and the path to the file found by the finder.""" self.name = fullname - if isinstance(path, str): - self.path = sys.intern(path) - else: - self.path = path + self.path = path def __eq__(self, other): return (self.__class__ == other.__class__ and diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 87872e61da1423..5c819b8d6e0075 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1,5 +1,5 @@ // DO NOT MERGE -#define TAG NULL +//#define TAG NULL /* System module */ @@ -3604,6 +3604,13 @@ make_impl_info(PyObject *version_info) if (res < 0) goto error; +// DO NOT MERGE +#ifdef MS_WINDOWS + if (GetEnvironmentVariableW(L"PYNOCACHETAG", NULL, 0)) { + _PySys_ImplCacheTag = NULL; + } +#endif + value = _PySys_ImplCacheTag ? PyUnicode_FromString(_PySys_ImplCacheTag) : Py_NewRef(Py_None); From fcca3f9232421bce010e84ea33b63a8bed091ce7 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 30 Jan 2026 10:51:32 +0000 Subject: [PATCH 24/29] Test skipping pyc at lower level --- Lib/importlib/_bootstrap_external.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index b95c9e09bd9bc2..1ea606bc27e91a 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -821,13 +821,13 @@ def get_code(self, fullname): hash_based = False check_source = True try: - # DO NOT MERGE - just testing unconditionally skipping this path - raise NotImplementedError() bytecode_path = cache_from_source(source_path) except NotImplementedError: bytecode_path = None else: try: + # DO NOT MERGE - just testing unconditionally skipping this path + raise OSError() st = self.path_stats(source_path) except OSError: pass @@ -889,6 +889,8 @@ def get_code(self, fullname): data = _code_to_timestamp_pyc(code_object, source_mtime, len(source_bytes)) try: + # DO NOT MERGE - just testing unconditionally skipping this path + raise NotImplementedError() self._cache_bytecode(source_path, bytecode_path, data) except NotImplementedError: pass From 6a588e88dec406629aaf67682ecaf0164ec160d3 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 30 Jan 2026 11:01:20 +0000 Subject: [PATCH 25/29] Just skip pyc writing --- Lib/importlib/_bootstrap_external.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 1ea606bc27e91a..75490acd857d1e 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -826,8 +826,6 @@ def get_code(self, fullname): bytecode_path = None else: try: - # DO NOT MERGE - just testing unconditionally skipping this path - raise OSError() st = self.path_stats(source_path) except OSError: pass @@ -889,8 +887,6 @@ def get_code(self, fullname): data = _code_to_timestamp_pyc(code_object, source_mtime, len(source_bytes)) try: - # DO NOT MERGE - just testing unconditionally skipping this path - raise NotImplementedError() self._cache_bytecode(source_path, bytecode_path, data) except NotImplementedError: pass @@ -951,6 +947,8 @@ def _cache_bytecode(self, source_path, bytecode_path, data): def set_data(self, path, data, *, _mode=0o666): """Write bytes data to a file.""" + # DO NOT MERGE - just testing unconditionally skipping this path + return parent, filename = _path_split(path) path_parts = [] # Figure out what directories are missing. From efdedaa7b8bab621706f5ea4923d1b6c062d5a32 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 30 Jan 2026 11:13:24 +0000 Subject: [PATCH 26/29] Revert tests --- Lib/importlib/_bootstrap_external.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 75490acd857d1e..b576ceb1ce9f6e 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -947,8 +947,6 @@ def _cache_bytecode(self, source_path, bytecode_path, data): def set_data(self, path, data, *, _mode=0o666): """Write bytes data to a file.""" - # DO NOT MERGE - just testing unconditionally skipping this path - return parent, filename = _path_split(path) path_parts = [] # Figure out what directories are missing. From c277edbcc308b80d63d13e16962ad68bb2d745ed Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 30 Jan 2026 11:31:45 +0000 Subject: [PATCH 27/29] Disable bytecode writing --- Python/initconfig.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Python/initconfig.c b/Python/initconfig.c index 9cdc10c4e78071..a9977738a0c46b 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -475,7 +475,7 @@ int Py_NoSiteFlag = 0; /* Suppress 'import site' */ int Py_BytesWarningFlag = 0; /* Warn on str(bytes) and str(buffer) */ int Py_FrozenFlag = 0; /* Needed by getpath.c */ int Py_IgnoreEnvironmentFlag = 0; /* e.g. PYTHONPATH, PYTHONHOME */ -int Py_DontWriteBytecodeFlag = 0; /* Suppress writing bytecode files (*.pyc) */ +int Py_DontWriteBytecodeFlag = 1; /* Suppress writing bytecode files (*.pyc) */ int Py_NoUserSiteDirectory = 0; /* for -s and site.py */ int Py_UnbufferedStdioFlag = 0; /* Unbuffered binary std{in,out,err} */ int Py_HashRandomizationFlag = 0; /* for -R and PYTHONHASHSEED */ @@ -1080,7 +1080,7 @@ config_init_defaults(PyConfig *config) config->interactive = 0; config->optimization_level = 0; config->parser_debug= 0; - config->write_bytecode = 1; + config->write_bytecode = 0; config->verbose = 0; config->quiet = 0; config->user_site_directory = 1; From 16a75d8fade207e9bbde70170b5c15d6384fbf8d Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 30 Jan 2026 11:53:45 +0000 Subject: [PATCH 28/29] Revert initconfig.c --- Python/initconfig.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Python/initconfig.c b/Python/initconfig.c index a9977738a0c46b..9cdc10c4e78071 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -475,7 +475,7 @@ int Py_NoSiteFlag = 0; /* Suppress 'import site' */ int Py_BytesWarningFlag = 0; /* Warn on str(bytes) and str(buffer) */ int Py_FrozenFlag = 0; /* Needed by getpath.c */ int Py_IgnoreEnvironmentFlag = 0; /* e.g. PYTHONPATH, PYTHONHOME */ -int Py_DontWriteBytecodeFlag = 1; /* Suppress writing bytecode files (*.pyc) */ +int Py_DontWriteBytecodeFlag = 0; /* Suppress writing bytecode files (*.pyc) */ int Py_NoUserSiteDirectory = 0; /* for -s and site.py */ int Py_UnbufferedStdioFlag = 0; /* Unbuffered binary std{in,out,err} */ int Py_HashRandomizationFlag = 0; /* for -R and PYTHONHASHSEED */ @@ -1080,7 +1080,7 @@ config_init_defaults(PyConfig *config) config->interactive = 0; config->optimization_level = 0; config->parser_debug= 0; - config->write_bytecode = 0; + config->write_bytecode = 1; config->verbose = 0; config->quiet = 0; config->user_site_directory = 1; From c3779af8fc00722ce0043ddb5f36ab4f3d8961eb Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 30 Jan 2026 15:02:14 +0000 Subject: [PATCH 29/29] Remove do not merge code --- Python/sysmodule.c | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 5c819b8d6e0075..a374589145109b 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1,6 +1,3 @@ -// DO NOT MERGE -//#define TAG NULL - /* System module */ @@ -3604,13 +3601,6 @@ make_impl_info(PyObject *version_info) if (res < 0) goto error; -// DO NOT MERGE -#ifdef MS_WINDOWS - if (GetEnvironmentVariableW(L"PYNOCACHETAG", NULL, 0)) { - _PySys_ImplCacheTag = NULL; - } -#endif - value = _PySys_ImplCacheTag ? PyUnicode_FromString(_PySys_ImplCacheTag) : Py_NewRef(Py_None);