Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-24792: Fix zipimporter masking the cause of import errors #22204

Merged
merged 9 commits into from
Dec 19, 2020
9 changes: 4 additions & 5 deletions Doc/library/zipimport.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ zipimporter Objects
.. method:: get_code(fullname)

Return the code object for the specified module. Raise
:exc:`ZipImportError` if the module couldn't be found.
:exc:`ZipImportError` if the module couldn't be imported.


.. method:: get_data(pathname)
Expand All @@ -137,7 +137,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
found.
imported.

.. versionadded:: 3.1

Expand All @@ -159,14 +159,13 @@ zipimporter Objects
.. method:: load_module(fullname)

Load the module specified by *fullname*. *fullname* must be the fully
qualified (dotted) module name. It returns the imported module, or raises
:exc:`ZipImportError` if it wasn't found.
qualified (dotted) module name. Returns the imported module on success,
raises :exc:`ZipImportError` on failure.

.. deprecated:: 3.10

Use :meth:`exec_module` instead.


.. attribute:: archive

The file name of the importer's associated ZIP file, without a possible
Expand Down
8 changes: 4 additions & 4 deletions Lib/test/test_zipimport.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,10 @@ def testBadMagic2(self):
files = {TESTMOD + pyc_ext: (NOW, badmagic_pyc)}
try:
self.doTest(".py", files, TESTMOD)
except ImportError:
pass
else:
self.fail("expected ImportError; import from bad pyc")
self.fail("This should not be reached")
except zipimport.ZipImportError as exc:
self.assertIsInstance(exc.__cause__, ImportError)
self.assertIn("magic number", exc.__cause__.msg)

def testBadMTime(self):
badtime_pyc = bytearray(test_pyc)
Expand Down
38 changes: 20 additions & 18 deletions Lib/zipimport.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def get_code(self, fullname):
"""get_code(fullname) -> code object.

Return the code object for the specified module. Raise ZipImportError
if the module couldn't be found.
if the module couldn't be imported.
"""
code, ispackage, modpath = _get_module_code(self, fullname)
return code
Expand Down Expand Up @@ -215,7 +215,8 @@ def get_data(self, pathname):
def get_filename(self, fullname):
"""get_filename(fullname) -> filename string.

Return the filename for the specified module.
Return the filename for the specified module or raise ZipImportError
if it couldn't be imported.
"""
# Deciding the filename requires working out where the code
# would come from if the module was actually loaded
Expand Down Expand Up @@ -267,7 +268,7 @@ def load_module(self, fullname):

Load the module specified by 'fullname'. 'fullname' must be the
fully qualified (dotted) module name. It returns the imported
module, or raises ZipImportError if it wasn't found.
module, or raises ZipImportError if it could not be imported.

Deprecated since Python 3.10. Use exec_module() instead.
"""
Expand Down Expand Up @@ -613,20 +614,15 @@ def _eq_mtime(t1, t2):


# Given the contents of a .py[co] file, unmarshal the data
# and return the code object. Return None if it the magic word doesn't
# match, or if the recorded .py[co] metadata does not match the source,
# (we do this instead of raising an exception as we fall back
# to .py if available and we don't want to mask other errors).
# and return the code object. Raises ImportError it the magic word doesn't
# match, or if the recorded .py[co] metadata does not match the source.
def _unmarshal_code(self, pathname, fullpath, fullname, data):
exc_details = {
'name': fullname,
'path': fullpath,
}

try:
flags = _bootstrap_external._classify_pyc(data, fullname, exc_details)
except ImportError:
return None
flags = _bootstrap_external._classify_pyc(data, fullname, exc_details)

hash_based = flags & 0b1 != 0
if hash_based:
Expand All @@ -640,11 +636,8 @@ def _unmarshal_code(self, pathname, fullpath, fullname, data):
source_bytes,
)

try:
_bootstrap_external._validate_hash_pyc(
data, source_hash, fullname, exc_details)
except ImportError:
return None
_bootstrap_external._validate_hash_pyc(
data, source_hash, fullname, exc_details)
else:
source_mtime, source_size = \
_get_mtime_and_size_of_source(self, fullpath)
Expand Down Expand Up @@ -730,6 +723,7 @@ def _get_pyc_source(self, path):
# 'fullname'.
def _get_module_code(self, fullname):
path = _get_module_path(self, fullname)
import_error = None
for suffix, isbytecode, ispackage in _zip_searchorder:
fullpath = path + suffix
_bootstrap._verbose_message('trying {}{}{}', self.archive, path_sep, fullpath, verbosity=2)
Expand All @@ -740,8 +734,12 @@ def _get_module_code(self, fullname):
else:
modpath = toc_entry[0]
data = _get_data(self.archive, toc_entry)
code = None
if isbytecode:
code = _unmarshal_code(self, modpath, fullpath, fullname, data)
try:
code = _unmarshal_code(self, modpath, fullpath, fullname, data)
except ImportError as exc:
import_error = exc
else:
code = _compile_source(modpath, data)
if code is None:
Expand All @@ -751,4 +749,8 @@ def _get_module_code(self, fullname):
modpath = toc_entry[0]
return code, ispackage, modpath
else:
raise ZipImportError(f"can't find module {fullname!r}", name=fullname)
if import_error:
msg = f"module load failed: {import_error}"
raise ZipImportError(msg, name=fullname) from import_error
else:
raise ZipImportError(f"can't find module {fullname!r}", name=fullname)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed bug where :mod:`zipimporter` sometimes reports an incorrect cause of import errors.
Loading