Bug report
Bug description:
traceback.TracebackException raises a new ModuleNotFoundError when formatting an existing ModuleNotFoundError for a submodule import where the parent package entry in sys.modules is None.
Setting sys.modules['pkg'] = None is a documented mechanism to block imports of a package. It is used in the wild via unittest.mock.patch.dict("sys.modules", {"pkg": None}) in test suites. When such a blocked sub-module import fails and the resulting ModuleNotFoundError is formatted via traceback.format_exception(), logging.exception(), or similar, the new _find_incompatible_extension_module() function (added in GH-145006) calls importlib.resources.files(parent), which internally calls importlib.import_module(parent). This hits the None in sys.modules and raises a second ModuleNotFoundError inside the traceback machinery itself.
Reproducer
import sys, traceback
sys.modules['pkg'] = None
try:
from pkg.mod import name
except ModuleNotFoundError as e:
traceback.format_exception(e)
Works on 3.14, raises ModuleNotFoundError on 3.15.0b2:
Traceback (most recent call last):
File ".../rep.py", line 5, in <module>
from pkg.mod import name
ModuleNotFoundError: No module named 'pkg.mod'; 'pkg' is not a package
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../rep.py", line 7, in <module>
traceback.format_exception(e)
File "/usr/lib64/python3.15/traceback.py", line 200, in format_exception
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
File "/usr/lib64/python3.15/traceback.py", line 1207, in __init__
elif abi_tag := _find_incompatible_extension_module(module_name):
File "/usr/lib64/python3.15/traceback.py", line 2075, in _find_incompatible_extension_module
traversable = importlib.resources.files(parent)
File "/usr/lib64/python3.15/importlib/resources/_common.py", line 22, in files
return from_package(resolve(anchor))
File "/usr/lib64/python3.15/functools.py", line 1003, in wrapper
return dispatch(args[0].__class__)(*args, **kw)
File "/usr/lib64/python3.15/importlib/resources/_common.py", line 48, in _
return importlib.import_module(cand)
File "/usr/lib64/python3.15/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1381, in _gcd_import
File "<frozen importlib._bootstrap>", line 1364, in _find_and_load
ModuleNotFoundError: import of pkg halted; None in sys.modules
logging.exception() is also affected:
import sys, logging
sys.modules['pkg'] = None
try:
from pkg.mod import name
except ModuleNotFoundError:
logging.exception('fail') # raises instead of logging the exception
Real-world failure
https://github.com/dask/distributed tests
The CondaInstall plugin catches ModuleNotFoundError when conda is not installed, logs the error, and raises RuntimeError. When asyncio's exception handler tries to format the chained ModuleNotFoundError, it raises another ModuleNotFoundError, bringing down the scheduler.
To reproduce, we run pytest -k test_conda_install_fails_when_conda_not_found ... during Fedora's RPM build of distributed.
Traceback (most recent call last):
File "/usr/lib64/python3.15/asyncio/base_events.py", line 1920, in call_exception_handler
self.default_exception_handler(context)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
File "/usr/lib64/python3.15/asyncio/base_events.py", line 1892, in default_exception_handler
logger.error('\n'.join(log_lines), exc_info=exc_info)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib64/python3.15/logging/__init__.py", line 1552, in error
self._log(ERROR, msg, args, **kwargs)
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib64/python3.15/logging/__init__.py", line 1668, in _log
self.handle(record)
~~~~~~~~~~~^^^^^^^^
File "/usr/lib64/python3.15/logging/__init__.py", line 1684, in handle
self.callHandlers(record)
~~~~~~~~~~~~~~~~~^^^^^^^^
File "/usr/lib64/python3.15/logging/__init__.py", line 1740, in callHandlers
hdlr.handle(record)
~~~~~~~~~~~^^^^^^^^
File "/usr/lib64/python3.15/logging/__init__.py", line 1030, in handle
self.emit(record)
~~~~~~~~~^^^^^^^^
File "/usr/lib/python3.15/site-packages/_pytest/logging.py", line 384, in emit
super().emit(record)
~~~~~~~~~~~~^^^^^^^^
File "/usr/lib64/python3.15/logging/__init__.py", line 1162, in emit
self.handleError(record)
~~~~~~~~~~~~~~~~^^^^^^^^
File "/usr/lib64/python3.15/logging/__init__.py", line 1154, in emit
msg = self.format(record)
File "/usr/lib64/python3.15/logging/__init__.py", line 1002, in format
return fmt.format(record)
~~~~~~~~~~^^^^^^^^
File "/usr/lib/python3.15/site-packages/_pytest/logging.py", line 137, in format
return super().format(record)
~~~~~~~~~~~~~~^^^^^^^^
File "/usr/lib64/python3.15/logging/__init__.py", line 720, in format
record.exc_text = self.formatException(record.exc_info)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "/usr/lib64/python3.15/logging/__init__.py", line 670, in formatException
traceback.print_exception(ei[0], ei[1], tb, limit=None, file=sio)
~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib64/python3.15/traceback.py", line 174, in print_exception
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
File "/usr/lib64/python3.15/traceback.py", line 1260, in __init__
cause = TracebackException(
type(e.__cause__),
...<6 lines>...
max_group_depth=max_group_depth,
_seen=_seen)
File "/usr/lib64/python3.15/traceback.py", line 1207, in __init__
elif abi_tag := _find_incompatible_extension_module(module_name):
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
File "/usr/lib64/python3.15/traceback.py", line 2075, in _find_incompatible_extension_module
traversable = importlib.resources.files(parent)
File "/usr/lib64/python3.15/importlib/resources/_common.py", line 22, in files
return from_package(resolve(anchor))
~~~~~~~^^^^^^^^
File "/usr/lib64/python3.15/functools.py", line 1003, in wrapper
return dispatch(args[0].__class__)(*args, **kw)
~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
File "/usr/lib64/python3.15/importlib/resources/_common.py", line 48, in _
return importlib.import_module(cand)
~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "/usr/lib64/python3.15/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1381, in _gcd_import
File "<frozen importlib._bootstrap>", line 1364, in _find_and_load
ModuleNotFoundError: import of conda halted; None in sys.modules
Where is this happening
|
parent, _, child = module_name.rpartition('.') |
|
if parent: |
|
traversable = importlib.resources.files(parent) |
The importlib.resources.files() call resolves the parent package via importlib.import_module(), which fails if sys.modules[parent] is None.
Suggested fix
Wrap the importlib.resources.files() call to handle ImportError:
if parent:
try:
traversable = importlib.resources.files(parent)
except ImportError:
return
(I will send this as a PR shortly, assuming there is no negative response here.)
Related
Disclaimer
I used LLM to debug the failure in distributed to find the issues and create the reproducer. However, this was reviewed by a human (yours truly).
CPython versions tested on:
3.15
Operating systems tested on:
Linux
Bug report
Bug description:
traceback.TracebackExceptionraises a newModuleNotFoundErrorwhen formatting an existingModuleNotFoundErrorfor a submodule import where the parent package entry insys.modulesisNone.Setting
sys.modules['pkg'] = Noneis a documented mechanism to block imports of a package. It is used in the wild viaunittest.mock.patch.dict("sys.modules", {"pkg": None})in test suites. When such a blocked sub-module import fails and the resultingModuleNotFoundErroris formatted viatraceback.format_exception(),logging.exception(), or similar, the new_find_incompatible_extension_module()function (added in GH-145006) callsimportlib.resources.files(parent), which internally callsimportlib.import_module(parent). This hits theNoneinsys.modulesand raises a secondModuleNotFoundErrorinside the traceback machinery itself.Reproducer
Works on 3.14, raises
ModuleNotFoundErroron 3.15.0b2:logging.exception()is also affected:Real-world failure
https://github.com/dask/distributed tests
The
CondaInstallplugin catchesModuleNotFoundErrorwhen conda is not installed, logs the error, and raisesRuntimeError. When asyncio's exception handler tries to format the chainedModuleNotFoundError, it raises anotherModuleNotFoundError, bringing down the scheduler.To reproduce, we run
pytest -k test_conda_install_fails_when_conda_not_found ...during Fedora's RPM build of distributed.Where is this happening
cpython/Lib/traceback.py
Lines 2105 to 2107 in 65afcdd
The
importlib.resources.files()call resolves the parent package viaimportlib.import_module(), which fails ifsys.modules[parent]isNone.Suggested fix
Wrap the
importlib.resources.files()call to handleImportError:(I will send this as a PR shortly, assuming there is no negative response here.)
Related
Disclaimer
I used LLM to debug the failure in distributed to find the issues and create the reproducer. However, this was reviewed by a human (yours truly).
CPython versions tested on:
3.15
Operating systems tested on:
Linux