From 27909fe4dfeb649683d947a5800c339abcc8846b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 31 Oct 2026 15:27:09 -0400 Subject: [PATCH] Install the finder only after the module is initialized. `install` appended a `MetadataPathFinder` to `sys.meta_path` while the module body was still executing, 36 lines before `PathDistribution` is bound, and `PathDistribution` is the only global the installed finder has yet to see (`Prepared`, `FastPath` and `DistributionFinder` all precede it). Anything walking `sys.meta_path` in that window resolves it against incomplete globals and raises `NameError: name 'PathDistribution' is not defined`. Two conditions reach the window. Another thread calling `entry_points()` takes no module lock, as it imports nothing and only iterates `sys.meta_path`. And an import aborted in the window, as an async exception such as a signal handler or a soft time limit can do, leaves the half-built finder installed for the life of the process: the module is dropped from `sys.modules`, `install` has already deleted `PathFinder.find_distributions` so the orphan is the only provider, and even a later successful import does not displace it. Installing at the end of the module, rather than just after `PathDistribution`, states the invariant the finder depends on and so survives any later reordering. The stdlib copy is unaffected, where `install` is stripped and `PathFinder.find_distributions` imports `MetadataPathFinder` lazily. Closes #525. Co-Authored-By: Claude Opus 5 --- importlib_metadata/__init__.py | 8 +++++++- newsfragments/525.bugfix.rst | 6 ++++++ tests/test_main.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 newsfragments/525.bugfix.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index ac0969d3..f01dfa5e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1030,7 +1030,6 @@ def __bool__(self): return bool(self.name) -@install class MetadataPathFinder(NullFinder, DistributionFinder): """A degenerate finder for distribution packages on the file system. @@ -1309,3 +1308,10 @@ def importable_name(name): return '.' not in name return filter(importable_name, opt_names) + + +# Install the finder only after the module is fully initialized. Installing it +# earlier would publish a finder whose globals are still incomplete, and +# anything walking sys.meta_path in that window (another thread, a finalizer) +# would fail to resolve PathDistribution (#525). +install(MetadataPathFinder) diff --git a/newsfragments/525.bugfix.rst b/newsfragments/525.bugfix.rst new file mode 100644 index 00000000..b8a8c85f --- /dev/null +++ b/newsfragments/525.bugfix.rst @@ -0,0 +1,6 @@ +``MetadataPathFinder`` is now installed on ``sys.meta_path`` only after the +module is fully initialized. Previously it was installed partway through, so +anything walking ``sys.meta_path`` during that window, such as another thread +calling ``entry_points()``, would raise ``NameError: name 'PathDistribution' +is not defined``. An import interrupted in that window left the half-built +finder installed for the life of the process, failing every subsequent lookup. diff --git a/tests/test_main.py b/tests/test_main.py index b03911a3..9c5b418f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,9 +1,13 @@ import contextlib import importlib +import importlib.util import io +import os import pickle import re +import sys import unittest +from unittest import mock import pyfakefs.fake_filesystem_unittest as ffs from test.support import os_helper @@ -14,6 +18,7 @@ EntryPoint, MetadataNotFound, PackageNotFoundError, + _compat, _unique, diagnose, distributions, @@ -108,6 +113,33 @@ def test_resolve_without_attr(self): assert ep.load() is importlib_metadata +class FinderInstallTests(unittest.TestCase): + def test_installed_after_module_initialized(self): + """ + Install the finder only after the module can satisfy it. + + An installed finder resolves ``PathDistribution`` from the module + globals, so a finder installed before the module body completes is + one that anything walking ``sys.meta_path`` would trip over + (python/importlib_metadata#525). + """ + snapshots = [] + + def capture(cls): + snapshots.append(set(sys._getframe(1).f_globals)) + return cls + + spec = importlib.util.find_spec('importlib_metadata') + with contextlib.ExitStack() as stack: + stack.enter_context(mock.patch.object(_compat, 'install', capture)) + # Don't let the re-executed module leak a fork handler. + stack.enter_context(mock.patch.object(os, 'register_at_fork', create=True)) + spec.loader.exec_module(importlib.util.module_from_spec(spec)) + + (globals_at_install,) = snapshots + assert 'PathDistribution' in globals_at_install + + class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): @staticmethod def make_pkg(name):