python / python/cpython

_find_incompatible_extension_module raises ModuleNotFoundError when sys.modules contains None for the parent package

Đang mở
#151,631 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

stdlib type-bug
Ngôn ngữ chính
Python
Star
77.2k
Fork
35.9k
Chỉ số merge pull request
Chỉ số pull request đang chờ

Mô tả

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

https://github.com/python/cpython/blob/65afcdd8dfb3621ac696fce076e6282c76a04b2b/Lib/traceback.py#L2105-L2107

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

Linked PRs
  • gh-151835

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu tại Lib/traceback.py, quanh _find_incompatible_extension_module(), đặc biệt là lệnh gọi importlib.resources.files(parent) được đề cập trong issue. Chạy reproducer sys.modules được cung cấp và xác minh rằng traceback.format_exception() và logging.exception() không còn phát sinh ModuleNotFoundError thứ hai khi package cha bị chặn.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
tooling
Loại issue
Lỗi
Độ khó
2/5
Thời gian dự kiến
1-3 giờ
Mức độ hoạt động
Đình trệ
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
25/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.