https://github.com/python/cpython/commit/230917f5072c5c6b9373374a84d52935d7afa3b5
commit: 230917f5072c5c6b9373374a84d52935d7afa3b5
branch: main
author: Harjoth Khara <[email protected]>
committer: itamaro <[email protected]>
date: 2026-10-10T12:56:51-07:00
summary:

gh-141276: Avoid compiling zipimport source in get_filename (#153799)

`zipimporter.get_filename()` compiled source modules just to work out the
module's file name. `find_spec()` calls it through `spec_from_loader()`, so
importing a source module from a ZIP archive compiled it twice and emitted
every `SyntaxWarning` twice.

`get_filename()` now returns before reading or compiling the source.
As a result, a `SyntaxError` in a zipped source module is now raised by
`exec_module()` rather than by `find_spec()` or `get_filename()`, and
the source isn't compiled twice during import, which matches
the filesystem-based loaders.

files:
A Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst
M Doc/library/zipimport.rst
M Lib/test/test_cmd_line_script.py
M Lib/test/test_zipimport.py
M Lib/test/test_zipimport_support.py
M Lib/zipimport.py

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/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py
index 3802ed62ce7c26b..4b09eb76cbb3792 100644
--- a/Lib/test/test_cmd_line_script.py
+++ b/Lib/test/test_cmd_line_script.py
@@ -846,7 +846,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.py b/Lib/test/test_zipimport.py
index 76cd85709a63af8..021fae1bd298e03 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,81 @@ 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 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/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)
diff --git a/Lib/zipimport.py b/Lib/zipimport.py
index 19279d1c2bea368..241af9a0430684e 100644
--- a/Lib/zipimport.py
+++ b/Lib/zipimport.py
@@ -159,11 +159,12 @@ 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
-        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 reading or 
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:
@@ -807,6 +808,8 @@ def _get_module_code(self, fullname):
             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:
@@ -820,7 +823,6 @@ def _get_module_code(self, fullname):
                 # 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
new file mode 100644
index 000000000000000..c19c7bbd31eb488
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-15-21-45-33.gh-issue-141276.xH7qP2.rst
@@ -0,0 +1,6 @@
+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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to