File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 99
1010from __future__ import annotations
1111
12+ __lazy_modules__ = {
13+ 'contextlib' ,
14+ 'email' ,
15+ f'{ __name__ } ._collections' ,
16+ f'{ __name__ } .compat' ,
17+ }
18+ """
19+ Imports to defer until something first reads the name they bind (PEP 810).
20+
21+ Honored by the interpreter from Python 3.15 and simply ignored before it.
22+
23+ Only a name that no module-level statement reads belongs here. A class
24+ base, a decorator, or a class attribute is evaluated as this module's
25+ body runs, so deferring its imports would save nothing: that is what
26+ keeps ``pathlib``, ``re``, and ``textwrap`` off this list
27+ (python/importlib_metadata#554).
28+
29+ A module that something else imports eagerly is also left off, however
30+ late this module reads it, so that the declaration promises only what it
31+ delivers. ``collections`` is the case to watch: ``typing`` imports it.
32+
33+ Naming the package through ``__name__`` keeps the declaration identical
34+ in the stdlib, where this module is ``importlib.metadata``.
35+ """
36+
1237import abc
1338import collections
1439import email
Original file line number Diff line number Diff line change 1- import platform
21import sys
32
43__all__ = ['NullFinder' , 'install' ]
@@ -46,11 +45,15 @@ def find_spec(*args, **kwargs):
4645 return None
4746
4847
49- def pypy_partial (val ):
48+ def pypy_partial (val ): # pragma: no cover
5049 """
5150 Adjust for variable stacklevel on partial under PyPy.
5251
5352 Workaround for #327.
5453 """
54+ # the only reader of platform, and no longer called from anywhere, so
55+ # don't make every import pay for it (#554)
56+ import platform
57+
5558 is_pypy = platform .python_implementation () == 'PyPy'
5659 return val + is_pypy
Original file line number Diff line number Diff line change 55
66import importlib_metadata
77
8- from ..compat import py312
9-
108if TYPE_CHECKING :
119 import email .message
1210 import importlib .metadata
@@ -55,10 +53,12 @@ def dist(
5553 if isinstance (dist , importlib_metadata .Distribution ):
5654 return dist
5755
58- # only a foreign dist gets this far, so defer the cost of the import
59- # (python/cpython#109829)
56+ # only a foreign dist gets this far, so defer the cost of the imports
57+ # (python/cpython#109829, #554 )
6058 import importlib .metadata
6159
60+ from ..compat import py312
61+
6262 if isinstance (dist , importlib .metadata .PathDistribution ):
6363 return importlib_metadata .PathDistribution (cast ('SimplePath' , dist ._path ))
6464 # Warn on what the interface requires and the class omits, rather than
@@ -77,6 +77,9 @@ def unimplemented(dist) -> list[str]:
7777 """
7878 The members IDistribution requires that dist's class doesn't supply.
7979 """
80+ # deferred for performance (#554)
81+ from ..compat import py312
82+
8083 required = py312 .required (importlib_metadata .IDistribution )
8184 return sorted (attr for attr in required if not hasattr (type (dist ), attr ))
8285
Original file line number Diff line number Diff line change 1+ Deferred the imports that no module-level statement reads, cutting the
2+ cost of ``import importlib_metadata `` by about 30% on Python 3.15 and 3
3+ to 8% on earlier Pythons.
4+
5+ Python 3.15 honors the new ``__lazy_modules__ `` declaration (PEP 810) and
6+ so defers ``contextlib ``, ``email ``, and two of the package's own modules;
7+ earlier Pythons ignore the declaration and keep importing them eagerly.
8+ The gain they do see comes from ``platform `` and ``compat.py312 ``, which
9+ moved into the functions that read them.
Original file line number Diff line number Diff line change 33import io
44import pickle
55import re
6+ import subprocess
7+ import sys
68import unittest
79
810import pyfakefs .fake_filesystem_unittest as ffs
@@ -108,6 +110,35 @@ def test_resolve_without_attr(self):
108110 assert ep .load () is importlib_metadata
109111
110112
113+ class LazyImportTests (unittest .TestCase ):
114+ @unittest .skipIf (
115+ sys .version_info < (3 , 15 ),
116+ 'lazy imports (PEP 810) are honored from Python 3.15' ,
117+ )
118+ def test_declared_modules_stay_unloaded (self ):
119+ """
120+ Importing the package loads none of the modules it defers.
121+
122+ Holds the rule that ``__lazy_modules__`` documents: a name some
123+ module-level statement reads resolves as the body runs, so
124+ declaring it would defer nothing.
125+
126+ Ref python/importlib_metadata#554.
127+ """
128+ script = (
129+ 'import sys; before = frozenset(sys.modules); '
130+ 'import importlib_metadata as md; '
131+ 'print(*sorted(md.__lazy_modules__ & (sys.modules.keys() - before)))'
132+ )
133+ proc = subprocess .run (
134+ [sys .executable , '-c' , script ],
135+ capture_output = True ,
136+ encoding = 'utf-8' ,
137+ check = True ,
138+ )
139+ assert not proc .stdout .split (), f'imported eagerly: { proc .stdout .strip ()} '
140+
141+
111142class NameNormalizationTests (fixtures .OnSysPath , fixtures .SiteDir , unittest .TestCase ):
112143 @staticmethod
113144 def make_pkg (name ):
You can’t perform that action at this time.
0 commit comments