Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion importlib_metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -1309,3 +1308,6 @@ def importable_name(name):
return '.' not in name

return filter(importable_name, opt_names)


install(MetadataPathFinder)
11 changes: 9 additions & 2 deletions importlib_metadata/_compat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@

def install(cls):
"""
Class decorator for installation on sys.meta_path.
Install cls on sys.meta_path.

Adds the backport DistributionFinder to sys.meta_path and
attempts to disable the finder functionality of the stdlib
DistributionFinder.

Call this only once the calling module is fully initialized, from
its end. The installed finder resolves PathDistribution from that
module's globals, so installing it any earlier publishes a finder
that anything walking sys.meta_path would trip over (#525).

Returns nothing, so that use as a class decorator fails outright
rather than reopening that window.
"""
sys.meta_path.append(cls())
disable_stdlib_finder()
return cls


def disable_stdlib_finder():
Expand Down
6 changes: 6 additions & 0 deletions newsfragments/525.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 39 additions & 0 deletions tests/compat/test_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
The finder installed on ``sys.meta_path`` is particular to the backport;
the stdlib reaches ``MetadataPathFinder`` through its own ``PathFinder``
and so never publishes a finder of its own (#525).
"""

import contextlib
import importlib.util
import os
import sys
import unittest
from unittest import mock

from importlib_metadata import _compat


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.
"""
snapshots = []

def capture(cls):
snapshots.append(set(sys._getframe(1).f_globals))

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
Loading