Skip to content

Commit 1d5a2ce

Browse files
authored
Merge pull request #556 from python/bugfix/525-install-after-init
Install the finder only after the module is initialized
2 parents be2ea93 + fa2b269 commit 1d5a2ce

4 files changed

Lines changed: 57 additions & 3 deletions

File tree

‎importlib_metadata/__init__.py‎

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

10331033

1034-
@install
10351034
class MetadataPathFinder(NullFinder, DistributionFinder):
10361035
"""A degenerate finder for distribution packages on the file system.
10371036
@@ -1310,3 +1309,6 @@ def importable_name(name):
13101309
return '.' not in name
13111310

13121311
return filter(importable_name, opt_names)
1312+
1313+
1314+
install(MetadataPathFinder)

‎importlib_metadata/_compat/__init__.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,22 @@
66

77
def install(cls):
88
"""
9-
Class decorator for installation on sys.meta_path.
9+
Install cls on sys.meta_path.
1010
1111
Adds the backport DistributionFinder to sys.meta_path and
1212
attempts to disable the finder functionality of the stdlib
1313
DistributionFinder.
14+
15+
Call this only once the calling module is fully initialized, from
16+
its end. The installed finder resolves PathDistribution from that
17+
module's globals, so installing it any earlier publishes a finder
18+
that anything walking sys.meta_path would trip over (#525).
19+
20+
Returns nothing, so that use as a class decorator fails outright
21+
rather than reopening that window.
1422
"""
1523
sys.meta_path.append(cls())
1624
disable_stdlib_finder()
17-
return cls
1825

1926

2027
def disable_stdlib_finder():

‎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/compat/test_install.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
The finder installed on ``sys.meta_path`` is particular to the backport;
3+
the stdlib reaches ``MetadataPathFinder`` through its own ``PathFinder``
4+
and so never publishes a finder of its own (#525).
5+
"""
6+
7+
import contextlib
8+
import importlib.util
9+
import os
10+
import sys
11+
import unittest
12+
from unittest import mock
13+
14+
from importlib_metadata import _compat
15+
16+
17+
class FinderInstallTests(unittest.TestCase):
18+
def test_installed_after_module_initialized(self):
19+
"""
20+
Install the finder only after the module can satisfy it.
21+
22+
An installed finder resolves ``PathDistribution`` from the module
23+
globals, so a finder installed before the module body completes is
24+
one that anything walking ``sys.meta_path`` would trip over.
25+
"""
26+
snapshots = []
27+
28+
def capture(cls):
29+
snapshots.append(set(sys._getframe(1).f_globals))
30+
31+
spec = importlib.util.find_spec('importlib_metadata')
32+
with contextlib.ExitStack() as stack:
33+
stack.enter_context(mock.patch.object(_compat, 'install', capture))
34+
# Don't let the re-executed module leak a fork handler.
35+
stack.enter_context(mock.patch.object(os, 'register_at_fork', create=True))
36+
spec.loader.exec_module(importlib.util.module_from_spec(spec))
37+
38+
(globals_at_install,) = snapshots
39+
assert 'PathDistribution' in globals_at_install

0 commit comments

Comments
 (0)