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+
110from __future__ import annotations
211
3- import os
4- import re
512import abc
6- import sys
7- import json
13+ import collections
814import email
9- import types
10- import inspect
11- import pathlib
12- import zipfile
13- import operator
14- import textwrap
1515import functools
1616import itertools
17+ import operator
18+ import os
19+ import pathlib
1720import 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
2032from . import _meta
2133from ._collections import FreezableDefaultDict , Pair
2234from ._functools import method_cache , pass_none
2335from ._itertools import always_iterable , bucket , unique_everseen
2436from ._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+
126141class 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):
306345class 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
10771137def _top_level_inferred (dist ):
0 commit comments