Skip to content
Closed
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
25 changes: 25 additions & 0 deletions importlib_metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@

from __future__ import annotations

__lazy_modules__ = {
'contextlib',
'email',
f'{__name__}._collections',
f'{__name__}.compat',
}
"""
Imports to defer until something first reads the name they bind (PEP 810).

Honored by the interpreter from Python 3.15 and simply ignored before it.

Only a name that no module-level statement reads belongs here. A class
base, a decorator, or a class attribute is evaluated as this module's
body runs, so deferring its imports would save nothing: that is what
keeps ``pathlib``, ``re``, and ``textwrap`` off this list
(python/importlib_metadata#554).

A module that something else imports eagerly is also left off, however
late this module reads it, so that the declaration promises only what it
delivers. ``collections`` is the case to watch: ``typing`` imports it.

Naming the package through ``__name__`` keeps the declaration identical
in the stdlib, where this module is ``importlib.metadata``.
"""

import abc
import collections
import email
Expand Down
7 changes: 5 additions & 2 deletions importlib_metadata/_compat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import platform
import sys

__all__ = ['NullFinder', 'install']
Expand Down Expand Up @@ -46,11 +45,15 @@ def find_spec(*args, **kwargs):
return None


def pypy_partial(val):
def pypy_partial(val): # pragma: no cover
"""
Adjust for variable stacklevel on partial under PyPy.

Workaround for #327.
"""
# the only reader of platform, and no longer called from anywhere, so
# don't make every import pay for it (#554)
import platform

is_pypy = platform.python_implementation() == 'PyPy'
return val + is_pypy
11 changes: 7 additions & 4 deletions importlib_metadata/_compat/localize.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

import importlib_metadata

from ..compat import py312

if TYPE_CHECKING:
import email.message
import importlib.metadata
Expand Down Expand Up @@ -55,10 +53,12 @@ def dist(
if isinstance(dist, importlib_metadata.Distribution):
return dist

# only a foreign dist gets this far, so defer the cost of the import
# (python/cpython#109829)
# only a foreign dist gets this far, so defer the cost of the imports
# (python/cpython#109829, #554)
import importlib.metadata

from ..compat import py312

if isinstance(dist, importlib.metadata.PathDistribution):
return importlib_metadata.PathDistribution(cast('SimplePath', dist._path))
# Warn on what the interface requires and the class omits, rather than
Expand All @@ -77,6 +77,9 @@ def unimplemented(dist) -> list[str]:
"""
The members IDistribution requires that dist's class doesn't supply.
"""
# deferred for performance (#554)
from ..compat import py312

required = py312.required(importlib_metadata.IDistribution)
return sorted(attr for attr in required if not hasattr(type(dist), attr))

Expand Down
9 changes: 9 additions & 0 deletions newsfragments/554.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Deferred the imports that no module-level statement reads, cutting the
cost of ``import importlib_metadata`` by about 30% on Python 3.15 and 3
to 8% on earlier Pythons.

Python 3.15 honors the new ``__lazy_modules__`` declaration (PEP 810) and
so defers ``contextlib``, ``email``, and two of the package's own modules;
earlier Pythons ignore the declaration and keep importing them eagerly.
The gain they do see comes from ``platform`` and ``compat.py312``, which
moved into the functions that read them.
31 changes: 31 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import io
import pickle
import re
import subprocess
import sys
import unittest

import pyfakefs.fake_filesystem_unittest as ffs
Expand Down Expand Up @@ -108,6 +110,35 @@ def test_resolve_without_attr(self):
assert ep.load() is importlib_metadata


class LazyImportTests(unittest.TestCase):
@unittest.skipIf(
sys.version_info < (3, 15),
'lazy imports (PEP 810) are honored from Python 3.15',
)
def test_declared_modules_stay_unloaded(self):
"""
Importing the package loads none of the modules it defers.

Holds the rule that ``__lazy_modules__`` documents: a name some
module-level statement reads resolves as the body runs, so
declaring it would defer nothing.

Ref python/importlib_metadata#554.
"""
script = (
'import sys; before = frozenset(sys.modules); '
'import importlib_metadata as md; '
'print(*sorted(md.__lazy_modules__ & (sys.modules.keys() - before)))'
)
proc = subprocess.run(
[sys.executable, '-c', script],
capture_output=True,
encoding='utf-8',
check=True,
)
assert not proc.stdout.split(), f'imported eagerly: {proc.stdout.strip()}'


class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
@staticmethod
def make_pkg(name):
Expand Down
Loading