From 0d5505decbc703149bbfbbba8d1fa9f62025e0a9 Mon Sep 17 00:00:00 2001 From: harjoth Date: Wed, 15 Jul 2026 22:15:13 -0700 Subject: [PATCH 1/4] gh-141276: Avoid compiling zipimport source in get_filename --- Lib/test/test_zipimport.py | 18 ++++++++++++++++++ Lib/zipimport.py | 11 +++++++---- ...6-07-15-21-45-33.gh-issue-141276.xH7qP2.rst | 2 ++ 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst diff --git a/Lib/test/test_zipimport.py b/Lib/test/test_zipimport.py index 76cd85709a63af8..4ed86cf48263bf1 100644 --- a/Lib/test/test_zipimport.py +++ b/Lib/test/test_zipimport.py @@ -9,6 +9,7 @@ import time import unittest import unittest.mock +import warnings from test import support from test.support import import_helper @@ -229,6 +230,23 @@ def testPy(self): files = {TESTMOD + ".py": test_src} self.doTest(".py", files, TESTMOD) + def test_syntax_warning(self): + files = {TESTMOD + ".py": "x = 1 is 1\n"} + self.makeZip(files) + zi = zipimport.zipimporter(TEMP_ZIP) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always", SyntaxWarning) + spec = zi.find_spec(TESTMOD) + self.assertIsNotNone(spec) + self.assertEqual(caught, []) + + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + self.assertEqual(len(caught), 1) + self.assertIsInstance(caught[0].message, SyntaxWarning) + def testPyc(self): files = {TESTMOD + pyc_ext: test_pyc} self.doTest(pyc_ext, files, TESTMOD) diff --git a/Lib/zipimport.py b/Lib/zipimport.py index 19279d1c2bea368..f7b6ada3981beaa 100644 --- a/Lib/zipimport.py +++ b/Lib/zipimport.py @@ -163,7 +163,8 @@ def get_filename(self, fullname): """ # Deciding the filename requires working out where the code # would come from if the module was actually loaded - code, ispackage, modpath = _get_module_code(self, fullname) + _, _, modpath = _get_module_code( + self, fullname, compile_source=False) return modpath @@ -793,9 +794,9 @@ def _get_pyc_source(self, path): return _get_data(self.archive, toc_entry) -# Get the code object associated with the module specified by -# 'fullname'. -def _get_module_code(self, fullname): +# Get the code object associated with the module specified by 'fullname'. +# If compile_source is false, return None for source code without compiling it. +def _get_module_code(self, fullname, *, compile_source=True): path = _get_module_path(self, fullname) import_error = None for suffix, isbytecode, ispackage in _zip_searchorder: @@ -815,6 +816,8 @@ def _get_module_code(self, fullname): except ImportError as exc: import_error = exc else: + if not compile_source: + return None, ispackage, modpath code = _compile_source(modpath, data, fullname) if code is None: # bad magic number or non-matching mtime diff --git a/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst b/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst new file mode 100644 index 000000000000000..6bbb7ce6d8a6cc8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst @@ -0,0 +1,2 @@ +Fix duplicate :exc:`SyntaxWarning` messages when importing source modules +from ZIP archives. From 05f92d2f28452cd092bfee8bbe41505a02bd1fae Mon Sep 17 00:00:00 2001 From: harjoth Date: Thu, 16 Jul 2026 12:54:40 -0700 Subject: [PATCH 2/4] gh-141276: Update zipimport warning expectations --- Lib/test/test_cmd_line_script.py | 2 +- Lib/test/test_zipimport_support.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index 73b1f671c58555f..2f8888d0f676cb3 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -827,7 +827,7 @@ def test_zipfile_run_filter_syntax_warnings_by_module(self): '-Werror:::test_pkg.__main__', os.path.join(zip_name, 'test_pkg') ) - self.assertEqual(err.count(b': SyntaxWarning: '), 12) + self.assertEqual(err.count(b': SyntaxWarning: '), 6) def tearDownModule(): diff --git a/Lib/test/test_zipimport_support.py b/Lib/test/test_zipimport_support.py index 2b28f46149b4ff3..dee5db00530ba34 100644 --- a/Lib/test/test_zipimport_support.py +++ b/Lib/test/test_zipimport_support.py @@ -253,7 +253,7 @@ def test_import_filter_syntax_warnings_by_module(self): warnings.filterwarnings('error', module='test_mod') import test_pkg.test_mod self.assertEqual(sorted(wm.lineno for wm in wlog), - sorted([4, 7, 10, 13, 14, 21]*2)) + [4, 7, 10, 13, 14, 21]) filename = test_pkg.test_mod.__file__ for wm in wlog: self.assertEqual(wm.filename, filename) From d202f7f9c78ab73bb3a38628bfe84860ba8ab437 Mon Sep 17 00:00:00 2001 From: harjoth Date: Thu, 8 Oct 2026 15:18:46 -0700 Subject: [PATCH 3/4] gh-141276: Return before reading source in _get_module_code Move the compile_source=False early return above the archive read, so get_filename() no longer decompresses source it immediately discards. Also update the NEWS entry with itamaro's suggested wording and add tests covering the deferred-SyntaxError behavior: find_spec()/ get_filename() succeed and get_filename() emits no warnings for a module with a syntax error, a valid .pyc still wins over a bad sibling .py, and a package's __init__.py error doesn't fall through to a same-named sibling module. --- Lib/test/test_zipimport.py | 58 +++++++++++++++++++ Lib/zipimport.py | 4 +- ...-07-15-21-45-33.gh-issue-141276.xH7qP2.rst | 5 +- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_zipimport.py b/Lib/test/test_zipimport.py index 4ed86cf48263bf1..021fae1bd298e03 100644 --- a/Lib/test/test_zipimport.py +++ b/Lib/test/test_zipimport.py @@ -247,6 +247,64 @@ def test_syntax_warning(self): self.assertEqual(len(caught), 1) self.assertIsInstance(caught[0].message, SyntaxWarning) + def test_syntax_error_defers_to_exec(self): + files = {TESTMOD + ".py": "x =\n"} + self.makeZip(files) + zi = zipimport.zipimporter(TEMP_ZIP) + expected_path = os.path.join(TEMP_ZIP, TESTMOD + ".py") + + spec = zi.find_spec(TESTMOD) + self.assertIsNotNone(spec) + self.assertEqual(spec.origin, expected_path) + self.assertEqual(zi.get_filename(TESTMOD), expected_path) + + mod = importlib.util.module_from_spec(spec) + with self.assertRaises(SyntaxError): + spec.loader.exec_module(mod) + + def test_get_filename_no_warnings(self): + files = {TESTMOD + ".py": "x = 1 is 1\n"} + self.makeZip(files) + zi = zipimport.zipimporter(TEMP_ZIP) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always", SyntaxWarning) + zi.get_filename(TESTMOD) + + self.assertEqual(caught, []) + + def test_syntax_error_in_source_with_valid_pyc(self): + bad_src = "x =\n" + files = {TESTMOD + ".py": bad_src, + TESTMOD + pyc_ext: make_pyc(test_co, NOW, len(bad_src))} + self.makeZip(files) + zi = zipimport.zipimporter(TEMP_ZIP) + expected_path = os.path.join(TEMP_ZIP, TESTMOD + pyc_ext) + + spec = zi.find_spec(TESTMOD) + self.assertIsNotNone(spec) + self.assertEqual(spec.origin, expected_path) + + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + self.assertEqual(mod.get_file(), expected_path) + + def test_syntax_error_in_package_init_no_fallthrough(self): + packdir = TESTPACK + os.sep + files = {packdir + "__init__.py": "x =\n", + TESTPACK + ".py": test_src} + self.makeZip(files) + zi = zipimport.zipimporter(TEMP_ZIP) + expected_path = os.path.join(TEMP_ZIP, TESTPACK, "__init__.py") + + spec = zi.find_spec(TESTPACK) + self.assertIsNotNone(spec) + self.assertEqual(spec.origin, expected_path) + + mod = importlib.util.module_from_spec(spec) + with self.assertRaises(SyntaxError): + spec.loader.exec_module(mod) + def testPyc(self): files = {TESTMOD + pyc_ext: test_pyc} self.doTest(pyc_ext, files, TESTMOD) diff --git a/Lib/zipimport.py b/Lib/zipimport.py index f7b6ada3981beaa..4b98b863fa146c7 100644 --- a/Lib/zipimport.py +++ b/Lib/zipimport.py @@ -808,6 +808,8 @@ def _get_module_code(self, fullname, *, compile_source=True): pass else: modpath = toc_entry[0] + if not isbytecode and not compile_source: + return None, ispackage, modpath data = _get_data(self.archive, toc_entry) code = None if isbytecode: @@ -816,8 +818,6 @@ def _get_module_code(self, fullname, *, compile_source=True): except ImportError as exc: import_error = exc else: - if not compile_source: - return None, ispackage, modpath code = _compile_source(modpath, data, fullname) if code is None: # bad magic number or non-matching mtime diff --git a/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst b/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst index 6bbb7ce6d8a6cc8..f46398389ef99a3 100644 --- a/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst +++ b/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst @@ -1,2 +1,3 @@ -Fix duplicate :exc:`SyntaxWarning` messages when importing source modules -from ZIP archives. +``zipimporter.get_filename()`` and ``find_spec()`` no longer compile source +modules; a :exc:`SyntaxError` in a zipped source module is now raised when the +module is executed, matching the behavior of filesystem-based loaders. From 8082336de24bd3152930a60606511bfd6ec379a8 Mon Sep 17 00:00:00 2001 From: harjoth Date: Fri, 9 Oct 2026 22:22:50 -0700 Subject: [PATCH 4/4] gh-141276: Address round-2 review feedback on zipimport Co-Authored-By: Claude Sonnet 5.5 --- Doc/library/zipimport.rst | 2 +- Lib/zipimport.py | 5 ++--- .../2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst | 9 ++++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Doc/library/zipimport.rst b/Doc/library/zipimport.rst index a4136733a550301..c4a0df1d0314d0c 100644 --- a/Doc/library/zipimport.rst +++ b/Doc/library/zipimport.rst @@ -124,7 +124,7 @@ zipimporter Objects Return the value ``__file__`` would be set to if the specified module was imported. Raise :exc:`ZipImportError` if the module couldn't be - imported. + found. .. versionadded:: 3.1 diff --git a/Lib/zipimport.py b/Lib/zipimport.py index 4b98b863fa146c7..241af9a0430684e 100644 --- a/Lib/zipimport.py +++ b/Lib/zipimport.py @@ -159,7 +159,7 @@ def get_filename(self, fullname): """get_filename(fullname) -> filename string. Return the filename for the specified module or raise ZipImportError - if it couldn't be imported. + if it couldn't be found. """ # Deciding the filename requires working out where the code # would come from if the module was actually loaded @@ -795,7 +795,7 @@ def _get_pyc_source(self, path): # Get the code object associated with the module specified by 'fullname'. -# If compile_source is false, return None for source code without compiling it. +# If compile_source is false, return None for source code without reading or compiling it. def _get_module_code(self, fullname, *, compile_source=True): path = _get_module_path(self, fullname) import_error = None @@ -823,7 +823,6 @@ def _get_module_code(self, fullname, *, compile_source=True): # bad magic number or non-matching mtime # in byte code, try next continue - modpath = toc_entry[0] return code, ispackage, modpath else: if import_error: diff --git a/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst b/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst index f46398389ef99a3..c19c7bbd31eb488 100644 --- a/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst +++ b/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst @@ -1,3 +1,6 @@ -``zipimporter.get_filename()`` and ``find_spec()`` no longer compile source -modules; a :exc:`SyntaxError` in a zipped source module is now raised when the -module is executed, matching the behavior of filesystem-based loaders. +Fix duplicate :exc:`SyntaxWarning` messages when importing source modules +from ZIP archives. :meth:`zipimport.zipimporter.get_filename`, and therefore +:meth:`~zipimport.zipimporter.find_spec`, no longer compile source modules. A +:exc:`SyntaxError` in a zipped source module is now raised when the module is +executed rather than when it is found, matching the behavior of +filesystem-based loaders.