Skip to content
Open
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
8 changes: 7 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,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)
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.
32 changes: 32 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,6 +18,7 @@
EntryPoint,
MetadataNotFound,
PackageNotFoundError,
_compat,
_unique,
diagnose,
distributions,
Expand Down Expand Up @@ -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):
Expand Down
Loading