Skip to content

Commit 27909fe

Browse files
jaracoclaude
andcommitted
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 <noreply@anthropic.com>
1 parent d196fdb commit 27909fe

3 files changed

Lines changed: 45 additions & 1 deletion

File tree

‎importlib_metadata/__init__.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,6 @@ def __bool__(self):
10301030
return bool(self.name)
10311031

10321032

1033-
@install
10341033
class MetadataPathFinder(NullFinder, DistributionFinder):
10351034
"""A degenerate finder for distribution packages on the file system.
10361035
@@ -1309,3 +1308,10 @@ def importable_name(name):
13091308
return '.' not in name
13101309

13111310
return filter(importable_name, opt_names)
1311+
1312+
1313+
# Install the finder only after the module is fully initialized. Installing it
1314+
# earlier would publish a finder whose globals are still incomplete, and
1315+
# anything walking sys.meta_path in that window (another thread, a finalizer)
1316+
# would fail to resolve PathDistribution (#525).
1317+
install(MetadataPathFinder)

‎newsfragments/525.bugfix.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
``MetadataPathFinder`` is now installed on ``sys.meta_path`` only after the
2+
module is fully initialized. Previously it was installed partway through, so
3+
anything walking ``sys.meta_path`` during that window, such as another thread
4+
calling ``entry_points()``, would raise ``NameError: name 'PathDistribution'
5+
is not defined``. An import interrupted in that window left the half-built
6+
finder installed for the life of the process, failing every subsequent lookup.

‎tests/test_main.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import contextlib
22
import importlib
3+
import importlib.util
34
import io
5+
import os
46
import pickle
57
import re
8+
import sys
69
import unittest
10+
from unittest import mock
711

812
import pyfakefs.fake_filesystem_unittest as ffs
913
from test.support import os_helper
@@ -14,6 +18,7 @@
1418
EntryPoint,
1519
MetadataNotFound,
1620
PackageNotFoundError,
21+
_compat,
1722
_unique,
1823
diagnose,
1924
distributions,
@@ -108,6 +113,33 @@ def test_resolve_without_attr(self):
108113
assert ep.load() is importlib_metadata
109114

110115

116+
class FinderInstallTests(unittest.TestCase):
117+
def test_installed_after_module_initialized(self):
118+
"""
119+
Install the finder only after the module can satisfy it.
120+
121+
An installed finder resolves ``PathDistribution`` from the module
122+
globals, so a finder installed before the module body completes is
123+
one that anything walking ``sys.meta_path`` would trip over
124+
(python/importlib_metadata#525).
125+
"""
126+
snapshots = []
127+
128+
def capture(cls):
129+
snapshots.append(set(sys._getframe(1).f_globals))
130+
return cls
131+
132+
spec = importlib.util.find_spec('importlib_metadata')
133+
with contextlib.ExitStack() as stack:
134+
stack.enter_context(mock.patch.object(_compat, 'install', capture))
135+
# Don't let the re-executed module leak a fork handler.
136+
stack.enter_context(mock.patch.object(os, 'register_at_fork', create=True))
137+
spec.loader.exec_module(importlib.util.module_from_spec(spec))
138+
139+
(globals_at_install,) = snapshots
140+
assert 'PathDistribution' in globals_at_install
141+
142+
111143
class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
112144
@staticmethod
113145
def make_pkg(name):

0 commit comments

Comments
 (0)