Skip to content

Commit 6bdaff0

Browse files
committed
Re-add most of importlib.metadata
1 parent 56450f9 commit 6bdaff0

File tree

1 file changed

+108
-48
lines changed

1 file changed

+108
-48
lines changed

Lib/importlib/metadata/__init__.py

Lines changed: 108 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,40 @@
1+
"""
2+
APIs exposing metadata from third-party Python packages.
3+
4+
This codebase is shared between importlib.metadata in the stdlib
5+
and importlib_metadata in PyPI. See
6+
https://github.com/python/importlib_metadata/wiki/Development-Methodology
7+
for more detail.
8+
"""
9+
110
from __future__ import annotations
211

3-
import os
4-
import re
512
import abc
6-
import sys
7-
import json
13+
import collections
814
import email
9-
import types
10-
import inspect
11-
import pathlib
12-
import zipfile
13-
import operator
14-
import textwrap
1515
import functools
1616
import itertools
17+
import operator
18+
import os
19+
import pathlib
1720
import posixpath
18-
import collections
21+
import re
22+
import sys
23+
import textwrap
24+
import types
25+
from collections.abc import Iterable, Mapping
26+
from contextlib import suppress
27+
from importlib import import_module
28+
from importlib.abc import MetaPathFinder
29+
from itertools import starmap
30+
from typing import Any
1931

2032
from . import _meta
2133
from ._collections import FreezableDefaultDict, Pair
2234
from ._functools import method_cache, pass_none
2335
from ._itertools import always_iterable, bucket, unique_everseen
2436
from ._meta import PackageMetadata, SimplePath
25-
26-
from contextlib import suppress
27-
from importlib import import_module
28-
from importlib.abc import MetaPathFinder
29-
from itertools import starmap
30-
from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast
37+
from ._typing import md_none
3138

3239
__all__ = [
3340
'Distribution',
@@ -53,7 +60,7 @@ def __str__(self) -> str:
5360
return f"No package metadata was found for {self.name}"
5461

5562
@property
56-
def name(self) -> str: # type: ignore[override]
63+
def name(self) -> str: # type: ignore[override] # make readonly
5764
(name,) = self.args
5865
return name
5966

@@ -123,6 +130,14 @@ def valid(line: str):
123130
return line and not line.startswith('#')
124131

125132

133+
class _EntryPointMatch(types.SimpleNamespace):
134+
module: str
135+
attr: str
136+
extras: str
137+
138+
139+
from typing import Optional, List, cast, Match
140+
126141
class EntryPoint:
127142
"""An entry point as defined by Python packaging conventions.
128143
@@ -138,6 +153,30 @@ class EntryPoint:
138153
'attr'
139154
>>> ep.extras
140155
['extra1', 'extra2']
156+
157+
If the value package or module are not valid identifiers, a
158+
ValueError is raised on access.
159+
160+
>>> EntryPoint(name=None, group=None, value='invalid-name').module
161+
Traceback (most recent call last):
162+
...
163+
ValueError: ('Invalid object reference...invalid-name...
164+
>>> EntryPoint(name=None, group=None, value='invalid-name').attr
165+
Traceback (most recent call last):
166+
...
167+
ValueError: ('Invalid object reference...invalid-name...
168+
>>> EntryPoint(name=None, group=None, value='invalid-name').extras
169+
Traceback (most recent call last):
170+
...
171+
ValueError: ('Invalid object reference...invalid-name...
172+
173+
The same thing happens on construction.
174+
175+
>>> EntryPoint(name=None, group=None, value='invalid-name')
176+
Traceback (most recent call last):
177+
...
178+
ValueError: ('Invalid object reference...invalid-name...
179+
141180
"""
142181

143182
pattern = re.compile(
@@ -254,7 +293,7 @@ class EntryPoints(tuple):
254293

255294
__slots__ = ()
256295

257-
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
296+
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int
258297
"""
259298
Get the EntryPoint in self matching name.
260299
"""
@@ -278,14 +317,14 @@ def select(self, **params) -> EntryPoints:
278317
return EntryPoints(ep for ep in self if ep.matches(**params))
279318

280319
@property
281-
def names(self) -> Set[str]:
320+
def names(self) -> set[str]:
282321
"""
283322
Return the set of all names of all entry points.
284323
"""
285324
return {ep.name for ep in self}
286325

287326
@property
288-
def groups(self) -> Set[str]:
327+
def groups(self) -> set[str]:
289328
"""
290329
Return the set of all groups of all entry points.
291330
"""
@@ -306,11 +345,11 @@ def _from_text(text):
306345
class PackagePath(pathlib.PurePosixPath):
307346
"""A reference to a path in a package"""
308347

309-
hash: Optional[FileHash]
348+
hash: FileHash | None
310349
size: int
311350
dist: Distribution
312351

313-
def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
352+
def read_text(self, encoding: str = 'utf-8') -> str:
314353
return self.locate().read_text(encoding=encoding)
315354

316355
def read_binary(self) -> bytes:
@@ -341,7 +380,7 @@ class Distribution(metaclass=abc.ABCMeta):
341380
"""
342381

343382
@abc.abstractmethod
344-
def read_text(self, filename) -> Optional[str]:
383+
def read_text(self, filename) -> str | None:
345384
"""Attempt to load metadata file given by the name.
346385
347386
Python distribution metadata is organized by blobs of text
@@ -368,6 +407,17 @@ def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
368407
"""
369408
Given a path to a file in this distribution, return a SimplePath
370409
to it.
410+
411+
This method is used by callers of ``Distribution.files()`` to
412+
locate files within the distribution. If it's possible for a
413+
Distribution to represent files in the distribution as
414+
``SimplePath`` objects, it should implement this method
415+
to resolve such objects.
416+
417+
Some Distribution providers may elect not to resolve SimplePath
418+
objects within the distribution by raising a
419+
NotImplementedError, but consumers of such a Distribution would
420+
be unable to invoke ``Distribution.files()``.
371421
"""
372422

373423
@classmethod
@@ -390,7 +440,7 @@ def from_name(cls, name: str) -> Distribution:
390440

391441
@classmethod
392442
def discover(
393-
cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs
443+
cls, *, context: DistributionFinder.Context | None = None, **kwargs
394444
) -> Iterable[Distribution]:
395445
"""Return an iterable of Distribution objects for all packages.
396446
@@ -436,7 +486,7 @@ def _discover_resolvers():
436486
return filter(None, declared)
437487

438488
@property
439-
def metadata(self) -> _meta.PackageMetadata:
489+
def metadata(self) -> _meta.PackageMetadata | None:
440490
"""Return the parsed metadata for this Distribution.
441491
442492
The returned object will have keys that name the various bits of
@@ -446,24 +496,29 @@ def metadata(self) -> _meta.PackageMetadata:
446496
Custom providers may provide the METADATA file or override this
447497
property.
448498
"""
449-
# deferred for performance (python/cpython#109829)
450-
from . import _adapters
451499

452-
opt_text = (
500+
text = (
453501
self.read_text('METADATA')
454502
or self.read_text('PKG-INFO')
455503
# This last clause is here to support old egg-info files. Its
456504
# effect is to just end up using the PathDistribution's self._path
457505
# (which points to the egg-info file) attribute unchanged.
458506
or self.read_text('')
459507
)
460-
text = cast(str, opt_text)
508+
return self._assemble_message(text)
509+
510+
@staticmethod
511+
@pass_none
512+
def _assemble_message(text: str) -> _meta.PackageMetadata:
513+
# deferred for performance (python/cpython#109829)
514+
from . import _adapters
515+
461516
return _adapters.Message(email.message_from_string(text))
462517

463518
@property
464519
def name(self) -> str:
465520
"""Return the 'Name' metadata for the distribution package."""
466-
return self.metadata['Name']
521+
return md_none(self.metadata)['Name']
467522

468523
@property
469524
def _normalized_name(self):
@@ -473,7 +528,7 @@ def _normalized_name(self):
473528
@property
474529
def version(self) -> str:
475530
"""Return the 'Version' metadata for the distribution package."""
476-
return self.metadata['Version']
531+
return md_none(self.metadata)['Version']
477532

478533
@property
479534
def entry_points(self) -> EntryPoints:
@@ -486,7 +541,7 @@ def entry_points(self) -> EntryPoints:
486541
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
487542

488543
@property
489-
def files(self) -> Optional[List[PackagePath]]:
544+
def files(self) -> list[PackagePath] | None:
490545
"""Files in this distribution.
491546
492547
:return: List of PackagePath for this distribution or None
@@ -579,7 +634,7 @@ def _read_files_egginfo_sources(self):
579634
return text and map('"{}"'.format, text.splitlines())
580635

581636
@property
582-
def requires(self) -> Optional[List[str]]:
637+
def requires(self) -> list[str] | None:
583638
"""Generated requirements specified for this Distribution"""
584639
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
585640
return reqs and list(reqs)
@@ -635,6 +690,9 @@ def origin(self):
635690
return self._load_json('direct_url.json')
636691

637692
def _load_json(self, filename):
693+
# Deferred for performance (python/importlib_metadata#503)
694+
import json
695+
638696
return pass_none(json.loads)(
639697
self.read_text(filename),
640698
object_hook=lambda data: types.SimpleNamespace(**data),
@@ -682,7 +740,7 @@ def __init__(self, **kwargs):
682740
vars(self).update(kwargs)
683741

684742
@property
685-
def path(self) -> List[str]:
743+
def path(self) -> list[str]:
686744
"""
687745
The sequence of directory path that a distribution finder
688746
should search.
@@ -719,7 +777,7 @@ class FastPath:
719777
True
720778
"""
721779

722-
@functools.lru_cache() # type: ignore
780+
@functools.lru_cache() # type: ignore[misc]
723781
def __new__(cls, root):
724782
return super().__new__(cls)
725783

@@ -737,6 +795,9 @@ def children(self):
737795
return []
738796

739797
def zip_children(self):
798+
# deferred for performance (python/importlib_metadata#502)
799+
import zipfile
800+
740801
zip_path = zipfile.Path(self.root)
741802
names = zip_path.root.namelist()
742803
self.joinpath = zip_path.joinpath
@@ -831,7 +892,7 @@ class Prepared:
831892
normalized = None
832893
legacy_normalized = None
833894

834-
def __init__(self, name: Optional[str]):
895+
def __init__(self, name: str | None):
835896
self.name = name
836897
if name is None:
837898
return
@@ -894,7 +955,7 @@ def __init__(self, path: SimplePath) -> None:
894955
"""
895956
self._path = path
896957

897-
def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]:
958+
def read_text(self, filename: str | os.PathLike[str]) -> str | None:
898959
with suppress(
899960
FileNotFoundError,
900961
IsADirectoryError,
@@ -958,7 +1019,7 @@ def distributions(**kwargs) -> Iterable[Distribution]:
9581019
return Distribution.discover(**kwargs)
9591020

9601021

961-
def metadata(distribution_name: str) -> _meta.PackageMetadata:
1022+
def metadata(distribution_name: str) -> _meta.PackageMetadata | None:
9621023
"""Get the metadata for the named package.
9631024
9641025
:param distribution_name: The name of the distribution package to query.
@@ -1001,7 +1062,7 @@ def entry_points(**params) -> EntryPoints:
10011062
return EntryPoints(eps).select(**params)
10021063

10031064

1004-
def files(distribution_name: str) -> Optional[List[PackagePath]]:
1065+
def files(distribution_name: str) -> list[PackagePath] | None:
10051066
"""Return a list of files for the named package.
10061067
10071068
:param distribution_name: The name of the distribution package to query.
@@ -1010,7 +1071,7 @@ def files(distribution_name: str) -> Optional[List[PackagePath]]:
10101071
return distribution(distribution_name).files
10111072

10121073

1013-
def requires(distribution_name: str) -> Optional[List[str]]:
1074+
def requires(distribution_name: str) -> list[str] | None:
10141075
"""
10151076
Return a list of requirements for the named package.
10161077
@@ -1020,7 +1081,7 @@ def requires(distribution_name: str) -> Optional[List[str]]:
10201081
return distribution(distribution_name).requires
10211082

10221083

1023-
def packages_distributions() -> Mapping[str, List[str]]:
1084+
def packages_distributions() -> Mapping[str, list[str]]:
10241085
"""
10251086
Return a mapping of top-level packages to their
10261087
distributions.
@@ -1041,7 +1102,7 @@ def _top_level_declared(dist):
10411102
return (dist.read_text('top_level.txt') or '').split()
10421103

10431104

1044-
def _topmost(name: PackagePath) -> Optional[str]:
1105+
def _topmost(name: PackagePath) -> str | None:
10451106
"""
10461107
Return the top-most parent as long as there is a parent.
10471108
"""
@@ -1067,11 +1128,10 @@ def _get_toplevel_name(name: PackagePath) -> str:
10671128
>>> _get_toplevel_name(PackagePath('foo.dist-info'))
10681129
'foo.dist-info'
10691130
"""
1070-
return _topmost(name) or (
1071-
# python/typeshed#10328
1072-
inspect.getmodulename(name) # type: ignore
1073-
or str(name)
1074-
)
1131+
# Defer import of inspect for performance (python/cpython#118761)
1132+
import inspect
1133+
1134+
return _topmost(name) or inspect.getmodulename(name) or str(name)
10751135

10761136

10771137
def _top_level_inferred(dist):

0 commit comments

Comments
 (0)