1+ from __future__ import annotations
2+
13import os
24import re
35import abc
2628from importlib import import_module
2729from importlib .abc import MetaPathFinder
2830from itertools import starmap
29- from typing import Iterable , List , Mapping , Optional , Set , Union , cast
31+ from typing import Any , Iterable , List , Mapping , Match , Optional , Set , cast
3032
3133__all__ = [
3234 'Distribution' ,
@@ -163,17 +165,17 @@ class EntryPoint:
163165 value : str
164166 group : str
165167
166- dist : Optional [' Distribution' ] = None
168+ dist : Optional [Distribution ] = None
167169
168170 def __init__ (self , name : str , value : str , group : str ) -> None :
169171 vars (self ).update (name = name , value = value , group = group )
170172
171- def load (self ):
173+ def load (self ) -> Any :
172174 """Load the entry point from its definition. If only a module
173175 is indicated by the value, return that module. Otherwise,
174176 return the named object.
175177 """
176- match = self .pattern .match (self .value )
178+ match = cast ( Match , self .pattern .match (self .value ) )
177179 module = import_module (match .group ('module' ))
178180 attrs = filter (None , (match .group ('attr' ) or '' ).split ('.' ))
179181 return functools .reduce (getattr , attrs , module )
@@ -268,7 +270,7 @@ def __repr__(self):
268270 """
269271 return '%s(%r)' % (self .__class__ .__name__ , tuple (self ))
270272
271- def select (self , ** params ):
273+ def select (self , ** params ) -> EntryPoints :
272274 """
273275 Select entry points from self that match the
274276 given parameters (typically group and/or name).
@@ -304,19 +306,17 @@ def _from_text(text):
304306class PackagePath (pathlib .PurePosixPath ):
305307 """A reference to a path in a package"""
306308
307- hash : Optional [" FileHash" ]
309+ hash : Optional [FileHash ]
308310 size : int
309- dist : " Distribution"
311+ dist : Distribution
310312
311313 def read_text (self , encoding : str = 'utf-8' ) -> str : # type: ignore[override]
312- with self .locate ().open (encoding = encoding ) as stream :
313- return stream .read ()
314+ return self .locate ().read_text (encoding = encoding )
314315
315316 def read_binary (self ) -> bytes :
316- with self .locate ().open ('rb' ) as stream :
317- return stream .read ()
317+ return self .locate ().read_bytes ()
318318
319- def locate (self ) -> pathlib . Path :
319+ def locate (self ) -> SimplePath :
320320 """Return a path-like object for this path"""
321321 return self .dist .locate_file (self )
322322
@@ -330,6 +330,7 @@ def __repr__(self) -> str:
330330
331331
332332class DeprecatedNonAbstract :
333+ # Required until Python 3.14
333334 def __new__ (cls , * args , ** kwargs ):
334335 all_names = {
335336 name for subclass in inspect .getmro (cls ) for name in vars (subclass )
@@ -349,25 +350,48 @@ def __new__(cls, *args, **kwargs):
349350
350351
351352class Distribution (DeprecatedNonAbstract ):
352- """A Python distribution package."""
353+ """
354+ An abstract Python distribution package.
355+
356+ Custom providers may derive from this class and define
357+ the abstract methods to provide a concrete implementation
358+ for their environment. Some providers may opt to override
359+ the default implementation of some properties to bypass
360+ the file-reading mechanism.
361+ """
353362
354363 @abc .abstractmethod
355364 def read_text (self , filename ) -> Optional [str ]:
356365 """Attempt to load metadata file given by the name.
357366
367+ Python distribution metadata is organized by blobs of text
368+ typically represented as "files" in the metadata directory
369+ (e.g. package-1.0.dist-info). These files include things
370+ like:
371+
372+ - METADATA: The distribution metadata including fields
373+ like Name and Version and Description.
374+ - entry_points.txt: A series of entry points as defined in
375+ `the entry points spec <https://packaging.python.org/en/latest/specifications/entry-points/#file-format>`_.
376+ - RECORD: A record of files according to
377+ `this recording spec <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-record-file>`_.
378+
379+ A package may provide any set of files, including those
380+ not listed here or none at all.
381+
358382 :param filename: The name of the file in the distribution info.
359383 :return: The text if found, otherwise None.
360384 """
361385
362386 @abc .abstractmethod
363- def locate_file (self , path : Union [ str , os .PathLike [str ]] ) -> pathlib . Path :
387+ def locate_file (self , path : str | os .PathLike [str ]) -> SimplePath :
364388 """
365- Given a path to a file in this distribution, return a path
389+ Given a path to a file in this distribution, return a SimplePath
366390 to it.
367391 """
368392
369393 @classmethod
370- def from_name (cls , name : str ) -> " Distribution" :
394+ def from_name (cls , name : str ) -> Distribution :
371395 """Return the Distribution for the given package name.
372396
373397 :param name: The name of the distribution package to search for.
@@ -385,16 +409,18 @@ def from_name(cls, name: str) -> "Distribution":
385409 raise PackageNotFoundError (name )
386410
387411 @classmethod
388- def discover (cls , ** kwargs ) -> Iterable ["Distribution" ]:
412+ def discover (
413+ cls , * , context : Optional [DistributionFinder .Context ] = None , ** kwargs
414+ ) -> Iterable [Distribution ]:
389415 """Return an iterable of Distribution objects for all packages.
390416
391417 Pass a ``context`` or pass keyword arguments for constructing
392418 a context.
393419
394420 :context: A ``DistributionFinder.Context`` object.
395- :return: Iterable of Distribution objects for all packages.
421+ :return: Iterable of Distribution objects for packages matching
422+ the context.
396423 """
397- context = kwargs .pop ('context' , None )
398424 if context and kwargs :
399425 raise ValueError ("cannot accept context and kwargs" )
400426 context = context or DistributionFinder .Context (** kwargs )
@@ -403,8 +429,8 @@ def discover(cls, **kwargs) -> Iterable["Distribution"]:
403429 )
404430
405431 @staticmethod
406- def at (path : Union [ str , os .PathLike [str ]] ) -> " Distribution" :
407- """Return a Distribution for the indicated metadata path
432+ def at (path : str | os .PathLike [str ]) -> Distribution :
433+ """Return a Distribution for the indicated metadata path.
408434
409435 :param path: a string or path-like object
410436 :return: a concrete Distribution instance for the path
@@ -413,7 +439,7 @@ def at(path: Union[str, os.PathLike[str]]) -> "Distribution":
413439
414440 @staticmethod
415441 def _discover_resolvers ():
416- """Search the meta_path for resolvers."""
442+ """Search the meta_path for resolvers (MetadataPathFinders) ."""
417443 declared = (
418444 getattr (finder , 'find_distributions' , None ) for finder in sys .meta_path
419445 )
@@ -424,7 +450,11 @@ def metadata(self) -> _meta.PackageMetadata:
424450 """Return the parsed metadata for this Distribution.
425451
426452 The returned object will have keys that name the various bits of
427- metadata. See PEP 566 for details.
453+ metadata per the
454+ `Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_.
455+
456+ Custom providers may provide the METADATA file or override this
457+ property.
428458 """
429459 opt_text = (
430460 self .read_text ('METADATA' )
@@ -454,6 +484,12 @@ def version(self) -> str:
454484
455485 @property
456486 def entry_points (self ) -> EntryPoints :
487+ """
488+ Return EntryPoints for this distribution.
489+
490+ Custom providers may provide the ``entry_points.txt`` file
491+ or override this property.
492+ """
457493 return EntryPoints ._from_text_for (self .read_text ('entry_points.txt' ), self )
458494
459495 @property
@@ -466,6 +502,10 @@ def files(self) -> Optional[List[PackagePath]]:
466502 (i.e. RECORD for dist-info, or installed-files.txt or
467503 SOURCES.txt for egg-info) is missing.
468504 Result may be empty if the metadata exists but is empty.
505+
506+ Custom providers are recommended to provide a "RECORD" file (in
507+ ``read_text``) or override this property to allow for callers to be
508+ able to resolve filenames provided by the package.
469509 """
470510
471511 def make_file (name , hash = None , size_str = None ):
@@ -497,7 +537,7 @@ def skip_missing_files(package_paths):
497537
498538 def _read_files_distinfo (self ):
499539 """
500- Read the lines of RECORD
540+ Read the lines of RECORD.
501541 """
502542 text = self .read_text ('RECORD' )
503543 return text and text .splitlines ()
@@ -611,6 +651,9 @@ def _load_json(self, filename):
611651class DistributionFinder (MetaPathFinder ):
612652 """
613653 A MetaPathFinder capable of discovering installed distributions.
654+
655+ Custom providers should implement this interface in order to
656+ supply metadata.
614657 """
615658
616659 class Context :
@@ -623,6 +666,17 @@ class Context:
623666 Each DistributionFinder may expect any parameters
624667 and should attempt to honor the canonical
625668 parameters defined below when appropriate.
669+
670+ This mechanism gives a custom provider a means to
671+ solicit additional details from the caller beyond
672+ "name" and "path" when searching distributions.
673+ For example, imagine a provider that exposes suites
674+ of packages in either a "public" or "private" ``realm``.
675+ A caller may wish to query only for distributions in
676+ a particular realm and could call
677+ ``distributions(realm="private")`` to signal to the
678+ custom provider to only include distributions from that
679+ realm.
626680 """
627681
628682 name = None
@@ -658,11 +712,18 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]:
658712
659713class FastPath :
660714 """
661- Micro-optimized class for searching a path for
662- children.
715+ Micro-optimized class for searching a root for children.
716+
717+ Root is a path on the file system that may contain metadata
718+ directories either as natural directories or within a zip file.
663719
664720 >>> FastPath('').children()
665721 ['...']
722+
723+ FastPath objects are cached and recycled for any given root.
724+
725+ >>> FastPath('foobar') is FastPath('foobar')
726+ True
666727 """
667728
668729 @functools .lru_cache () # type: ignore
@@ -704,7 +765,19 @@ def lookup(self, mtime):
704765
705766
706767class Lookup :
768+ """
769+ A micro-optimized class for searching a (fast) path for metadata.
770+ """
771+
707772 def __init__ (self , path : FastPath ):
773+ """
774+ Calculate all of the children representing metadata.
775+
776+ From the children in the path, calculate early all of the
777+ children that appear to represent metadata (infos) or legacy
778+ metadata (eggs).
779+ """
780+
708781 base = os .path .basename (path .root ).lower ()
709782 base_is_egg = base .endswith (".egg" )
710783 self .infos = FreezableDefaultDict (list )
@@ -725,7 +798,10 @@ def __init__(self, path: FastPath):
725798 self .infos .freeze ()
726799 self .eggs .freeze ()
727800
728- def search (self , prepared ):
801+ def search (self , prepared : Prepared ):
802+ """
803+ Yield all infos and eggs matching the Prepared query.
804+ """
729805 infos = (
730806 self .infos [prepared .normalized ]
731807 if prepared
@@ -741,13 +817,28 @@ def search(self, prepared):
741817
742818class Prepared :
743819 """
744- A prepared search for metadata on a possibly-named package.
820+ A prepared search query for metadata on a possibly-named package.
821+
822+ Pre-calculates the normalization to prevent repeated operations.
823+
824+ >>> none = Prepared(None)
825+ >>> none.normalized
826+ >>> none.legacy_normalized
827+ >>> bool(none)
828+ False
829+ >>> sample = Prepared('Sample__Pkg-name.foo')
830+ >>> sample.normalized
831+ 'sample_pkg_name_foo'
832+ >>> sample.legacy_normalized
833+ 'sample__pkg_name.foo'
834+ >>> bool(sample)
835+ True
745836 """
746837
747838 normalized = None
748839 legacy_normalized = None
749840
750- def __init__ (self , name ):
841+ def __init__ (self , name : Optional [ str ] ):
751842 self .name = name
752843 if name is None :
753844 return
@@ -777,7 +868,7 @@ class MetadataPathFinder(DistributionFinder):
777868 @classmethod
778869 def find_distributions (
779870 cls , context = DistributionFinder .Context ()
780- ) -> Iterable [" PathDistribution" ]:
871+ ) -> Iterable [PathDistribution ]:
781872 """
782873 Find distributions.
783874
@@ -810,7 +901,7 @@ def __init__(self, path: SimplePath) -> None:
810901 """
811902 self ._path = path
812903
813- def read_text (self , filename : Union [ str , os .PathLike [str ] ]) -> Optional [str ]:
904+ def read_text (self , filename : str | os .PathLike [str ]) -> Optional [str ]:
814905 with suppress (
815906 FileNotFoundError ,
816907 IsADirectoryError ,
@@ -824,7 +915,7 @@ def read_text(self, filename: Union[str, os.PathLike[str]]) -> Optional[str]:
824915
825916 read_text .__doc__ = Distribution .read_text .__doc__
826917
827- def locate_file (self , path : Union [ str , os .PathLike [str ]] ) -> pathlib . Path :
918+ def locate_file (self , path : str | os .PathLike [str ]) -> SimplePath :
828919 return self ._path .parent / path
829920
830921 @property
0 commit comments