17 from . import _adapters
, _meta
, _py39compat
18 from ._collections
import FreezableDefaultDict
, Pair
19 from ._compat
import (
24 from ._functools
import method_cache
, pass_none
25 from ._itertools
import always_iterable
, unique_everseen
26 from ._meta
import PackageMetadata
, SimplePath
28 from contextlib
import suppress
29 from importlib
import import_module
30 from importlib
.abc
import MetaPathFinder
31 from itertools
import starmap
32 from typing
import List
, Mapping
, Optional
39 'PackageNotFoundError',
45 'packages_distributions',
51 class PackageNotFoundError(ModuleNotFoundError
):
52 """The package was not found."""
55 return f
"No package metadata was found for {self.name}"
65 A simple entry point config parser for performance
67 >>> for item in Sectioned.read(Sectioned._sample):
69 Pair(name='sec1', value='# comments ignored')
70 Pair(name='sec1', value='a = 1')
71 Pair(name='sec1', value='b = 2')
72 Pair(name='sec2', value='a = 2')
74 >>> res = Sectioned.section_pairs(Sectioned._sample)
79 Pair(name='a', value='1')
82 Pair(name='b', value='2')
87 Pair(name='a', value='2')
92 _sample
= textwrap
.dedent(
105 def section_pairs(cls
, text
):
107 section
._replace
(value
=Pair
.parse(section
.value
))
108 for section
in cls
.read(text
, filter_
=cls
.valid
)
109 if section
.name
is not None
113 def read(text
, filter_
=None):
114 lines
= filter(filter_
, map(str.strip
, text
.splitlines()))
117 section_match
= value
.startswith('[') and value
.endswith(']')
119 name
= value
.strip('[]')
121 yield Pair(name
, value
)
125 return line
and not line
.startswith('#')
128 class DeprecatedTuple
:
130 Provide subscript item access for backward compatibility.
132 >>> recwarn = getfixture('recwarn')
133 >>> ep = EntryPoint(name='name', value='value', group='group')
135 ('name', 'value', 'group')
142 # Do not remove prior to 2023-05-01 or Python 3.13
143 _warn
= functools
.partial(
145 "EntryPoint tuple interface is deprecated. Access members by name.",
147 stacklevel
=pypy_partial(2),
150 def __getitem__(self
, item
):
152 return self
._key
()[item
]
155 class EntryPoint(DeprecatedTuple
):
156 """An entry point as defined by Python packaging conventions.
158 See `the packaging docs on entry points
159 <https://packaging.python.org/specifications/entry-points/>`_
160 for more information.
163 ... name=None, group=None, value='package.module:attr [extra1, extra2]')
172 pattern
= re
.compile(
173 r
'(?P<module>[\w.]+)\s*'
174 r
'(:\s*(?P<attr>[\w.]+)\s*)?'
175 r
'((?P<extras>\[.*\])\s*)?$'
178 A regular expression describing the syntax for an entry point,
179 which might look like:
183 - package.module:attribute
184 - package.module:object.attribute
185 - package.module:attr [extra1, extra2]
187 Other combinations are possible as well.
189 The expression is lenient about whitespace around the ':',
190 following the attr, and following any extras.
197 dist
: Optional
['Distribution'] = None
199 def __init__(self
, name
, value
, group
):
200 vars(self
).update(name
=name
, value
=value
, group
=group
)
203 """Load the entry point from its definition. If only a module
204 is indicated by the value, return that module. Otherwise,
205 return the named object.
207 match
= self
.pattern
.match(self
.value
)
208 module
= import_module(match
.group('module'))
209 attrs
= filter(None, (match
.group('attr') or '').split('.'))
210 return functools
.reduce(getattr, attrs
, module
)
214 match
= self
.pattern
.match(self
.value
)
215 return match
.group('module')
219 match
= self
.pattern
.match(self
.value
)
220 return match
.group('attr')
224 match
= self
.pattern
.match(self
.value
)
225 return re
.findall(r
'\w+', match
.group('extras') or '')
227 def _for(self
, dist
):
228 vars(self
).update(dist
=dist
)
231 def matches(self
, **params
):
233 EntryPoint matches the given parameters.
235 >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]')
236 >>> ep.matches(group='foo')
238 >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]')
240 >>> ep.matches(group='foo', name='other')
244 >>> ep.matches(extras=['extra1', 'extra2'])
246 >>> ep.matches(module='bing')
248 >>> ep.matches(attr='bong')
251 attrs
= (getattr(self
, param
) for param
in params
)
252 return all(map(operator
.eq
, params
.values(), attrs
))
255 return self
.name
, self
.value
, self
.group
257 def __lt__(self
, other
):
258 return self
._key
() < other
._key
()
260 def __eq__(self
, other
):
261 return self
._key
() == other
._key
()
263 def __setattr__(self
, name
, value
):
264 raise AttributeError("EntryPoint objects are immutable.")
268 f
'EntryPoint(name={self.name!r}, value={self.value!r}, '
269 f
'group={self.group!r})'
273 return hash(self
._key
())
276 class EntryPoints(tuple):
278 An immutable collection of selectable EntryPoint objects.
283 def __getitem__(self
, name
): # -> EntryPoint:
285 Get the EntryPoint in self matching name.
288 return next(iter(self
.select(name
=name
)))
289 except StopIteration:
292 def select(self
, **params
):
294 Select entry points from self that match the
295 given parameters (typically group and/or name).
297 return EntryPoints(ep
for ep
in self
if _py39compat
.ep_matches(ep
, **params
))
302 Return the set of all names of all entry points.
304 return {ep.name for ep in self}
309 Return the set of all groups of all entry points.
311 return {ep.group for ep in self}
314 def _from_text_for(cls
, text
, dist
):
315 return cls(ep
._for
(dist
) for ep
in cls
._from
_text
(text
))
318 def _from_text(text
):
320 EntryPoint(name
=item
.value
.name
, value
=item
.value
.value
, group
=item
.name
)
321 for item
in Sectioned
.section_pairs(text
or '')
325 class PackagePath(pathlib
.PurePosixPath
):
326 """A reference to a path in a package"""
328 def read_text(self
, encoding
='utf-8'):
329 with self
.locate().open(encoding
=encoding
) as stream
:
332 def read_binary(self
):
333 with self
.locate().open('rb') as stream
:
337 """Return a path-like object for this path"""
338 return self
.dist
.locate_file(self
)
342 def __init__(self
, spec
):
343 self
.mode
, _
, self
.value
= spec
.partition('=')
346 return f
'<FileHash mode: {self.mode} value: {self.value}>'
349 class Distribution(metaclass
=abc
.ABCMeta
):
350 """A Python distribution package."""
353 def read_text(self
, filename
):
354 """Attempt to load metadata file given by the name.
356 :param filename: The name of the file in the distribution info.
357 :return: The text if found, otherwise None.
361 def locate_file(self
, path
):
363 Given a path to a file in this distribution, return a path
368 def from_name(cls
, name
: str):
369 """Return the Distribution for the given package name.
371 :param name: The name of the distribution package to search for.
372 :return: The Distribution instance (or subclass thereof) for the named
374 :raises PackageNotFoundError: When the named package's distribution
375 metadata cannot be found.
376 :raises ValueError: When an invalid value is supplied for name.
379 raise ValueError("A distribution name is required.")
381 return next(cls
.discover(name
=name
))
382 except StopIteration:
383 raise PackageNotFoundError(name
)
386 def discover(cls
, **kwargs
):
387 """Return an iterable of Distribution objects for all packages.
389 Pass a ``context`` or pass keyword arguments for constructing
392 :context: A ``DistributionFinder.Context`` object.
393 :return: Iterable of Distribution objects for all packages.
395 context
= kwargs
.pop('context', None)
396 if context
and kwargs
:
397 raise ValueError("cannot accept context and kwargs")
398 context
= context
or DistributionFinder
.Context(**kwargs
)
399 return itertools
.chain
.from_iterable(
400 resolver(context
) for resolver
in cls
._discover
_resolvers
()
405 """Return a Distribution for the indicated metadata path
407 :param path: a string or path-like object
408 :return: a concrete Distribution instance for the path
410 return PathDistribution(pathlib
.Path(path
))
413 def _discover_resolvers():
414 """Search the meta_path for resolvers."""
416 getattr(finder
, 'find_distributions', None) for finder
in sys
.meta_path
418 return filter(None, declared
)
421 def metadata(self
) -> _meta
.PackageMetadata
:
422 """Return the parsed metadata for this Distribution.
424 The returned object will have keys that name the various bits of
425 metadata. See PEP 566 for details.
428 self
.read_text('METADATA')
429 or self
.read_text('PKG-INFO')
430 # This last clause is here to support old egg-info files. Its
431 # effect is to just end up using the PathDistribution's self._path
432 # (which points to the egg-info file) attribute unchanged.
433 or self
.read_text('')
435 return _adapters
.Message(email
.message_from_string(text
))
439 """Return the 'Name' metadata for the distribution package."""
440 return self
.metadata
['Name']
443 def _normalized_name(self
):
444 """Return a normalized version of the name."""
445 return Prepared
.normalize(self
.name
)
449 """Return the 'Version' metadata for the distribution package."""
450 return self
.metadata
['Version']
453 def entry_points(self
):
454 return EntryPoints
._from
_text
_for
(self
.read_text('entry_points.txt'), self
)
458 """Files in this distribution.
460 :return: List of PackagePath for this distribution or None
462 Result is `None` if the metadata file that enumerates files
463 (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
465 Result may be empty if the metadata exists but is empty.
468 def make_file(name
, hash=None, size_str
=None):
469 result
= PackagePath(name
)
470 result
.hash = FileHash(hash) if hash else None
471 result
.size
= int(size_str
) if size_str
else None
476 def make_files(lines
):
477 return list(starmap(make_file
, csv
.reader(lines
)))
479 return make_files(self
._read
_files
_distinfo
() or self
._read
_files
_egginfo
())
481 def _read_files_distinfo(self
):
483 Read the lines of RECORD
485 text
= self
.read_text('RECORD')
486 return text
and text
.splitlines()
488 def _read_files_egginfo(self
):
490 SOURCES.txt might contain literal commas, so wrap each line
493 text
= self
.read_text('SOURCES.txt')
494 return text
and map('"{}"'.format
, text
.splitlines())
498 """Generated requirements specified for this Distribution"""
499 reqs
= self
._read
_dist
_info
_reqs
() or self
._read
_egg
_info
_reqs
()
500 return reqs
and list(reqs
)
502 def _read_dist_info_reqs(self
):
503 return self
.metadata
.get_all('Requires-Dist')
505 def _read_egg_info_reqs(self
):
506 source
= self
.read_text('requires.txt')
507 return pass_none(self
._deps
_from
_requires
_text
)(source
)
510 def _deps_from_requires_text(cls
, source
):
511 return cls
._convert
_egg
_info
_reqs
_to
_simple
_reqs
(Sectioned
.read(source
))
514 def _convert_egg_info_reqs_to_simple_reqs(sections
):
516 Historically, setuptools would solicit and store 'extra'
517 requirements, including those with environment markers,
518 in separate sections. More modern tools expect each
519 dependency to be defined separately, with any relevant
520 extras and environment markers attached directly to that
521 requirement. This method converts the former to the
522 latter. See _test_deps_from_requires_text for an example.
525 def make_condition(name
):
526 return name
and f
'extra == "{name}"'
528 def quoted_marker(section
):
529 section
= section
or ''
530 extra
, sep
, markers
= section
.partition(':')
531 if extra
and markers
:
532 markers
= f
'({markers})'
533 conditions
= list(filter(None, [markers
, make_condition(extra
)]))
534 return '; ' + ' and '.join(conditions
) if conditions
else ''
536 def url_req_space(req
):
538 PEP 508 requires a space between the url_spec and the quoted_marker.
539 Ref python/importlib_metadata#357.
541 # '@' is uniquely indicative of a url_req.
542 return ' ' * ('@' in req
)
544 for section
in sections
:
545 space
= url_req_space(section
.value
)
546 yield section
.value
+ space
+ quoted_marker(section
.name
)
549 class DistributionFinder(MetaPathFinder
):
551 A MetaPathFinder capable of discovering installed distributions.
556 Keyword arguments presented by the caller to
557 ``distributions()`` or ``Distribution.discover()``
558 to narrow the scope of a search for distributions
559 in all DistributionFinders.
561 Each DistributionFinder may expect any parameters
562 and should attempt to honor the canonical
563 parameters defined below when appropriate.
568 Specific name for which a distribution finder should match.
569 A name of ``None`` matches all distributions.
572 def __init__(self
, **kwargs
):
573 vars(self
).update(kwargs
)
578 The sequence of directory path that a distribution finder
581 Typically refers to Python installed package paths such as
582 "site-packages" directories and defaults to ``sys.path``.
584 return vars(self
).get('path', sys
.path
)
587 def find_distributions(self
, context
=Context()):
591 Return an iterable of all Distribution instances capable of
592 loading the metadata for packages matching the ``context``,
593 a DistributionFinder.Context instance.
599 Micro-optimized class for searching a path for
602 >>> FastPath('').children()
606 @functools.lru_cache() # type: ignore
607 def __new__(cls
, root
):
608 return super().__new
__(cls
)
610 def __init__(self
, root
):
613 def joinpath(self
, child
):
614 return pathlib
.Path(self
.root
, child
)
617 with suppress(Exception):
618 return os
.listdir(self
.root
or '.')
619 with suppress(Exception):
620 return self
.zip_children()
623 def zip_children(self
):
624 zip_path
= zipp
.Path(self
.root
)
625 names
= zip_path
.root
.namelist()
626 self
.joinpath
= zip_path
.joinpath
628 return dict.fromkeys(child
.split(posixpath
.sep
, 1)[0] for child
in names
)
630 def search(self
, name
):
631 return self
.lookup(self
.mtime
).search(name
)
635 with suppress(OSError):
636 return os
.stat(self
.root
).st_mtime
637 self
.lookup
.cache_clear()
640 def lookup(self
, mtime
):
645 def __init__(self
, path
: FastPath
):
646 base
= os
.path
.basename(path
.root
).lower()
647 base_is_egg
= base
.endswith(".egg")
648 self
.infos
= FreezableDefaultDict(list)
649 self
.eggs
= FreezableDefaultDict(list)
651 for child
in path
.children():
653 if low
.endswith((".dist-info", ".egg-info")):
654 # rpartition is faster than splitext and suitable for this purpose.
655 name
= low
.rpartition(".")[0].partition("-")[0]
656 normalized
= Prepared
.normalize(name
)
657 self
.infos
[normalized
].append(path
.joinpath(child
))
658 elif base_is_egg
and low
== "egg-info":
659 name
= base
.rpartition(".")[0].partition("-")[0]
660 legacy_normalized
= Prepared
.legacy_normalize(name
)
661 self
.eggs
[legacy_normalized
].append(path
.joinpath(child
))
666 def search(self
, prepared
):
668 self
.infos
[prepared
.normalized
]
670 else itertools
.chain
.from_iterable(self
.infos
.values())
673 self
.eggs
[prepared
.legacy_normalized
]
675 else itertools
.chain
.from_iterable(self
.eggs
.values())
677 return itertools
.chain(infos
, eggs
)
682 A prepared search for metadata on a possibly-named package.
686 legacy_normalized
= None
688 def __init__(self
, name
):
692 self
.normalized
= self
.normalize(name
)
693 self
.legacy_normalized
= self
.legacy_normalize(name
)
698 PEP 503 normalization plus dashes as underscores.
700 return re
.sub(r
"[-_.]+", "-", name
).lower().replace('-', '_')
703 def legacy_normalize(name
):
705 Normalize the package name as found in the convention in
706 older packaging tools versions and specs.
708 return name
.lower().replace('-', '_')
711 return bool(self
.name
)
715 class MetadataPathFinder(NullFinder
, DistributionFinder
):
716 """A degenerate finder for distribution packages on the file system.
718 This finder supplies only a find_distributions() method for versions
719 of Python that do not have a PathFinder find_distributions().
722 def find_distributions(self
, context
=DistributionFinder
.Context()):
726 Return an iterable of all Distribution instances capable of
727 loading the metadata for packages matching ``context.name``
728 (or all names if ``None`` indicated) along the paths in the list
729 of directories ``context.path``.
731 found
= self
._search
_paths
(context
.name
, context
.path
)
732 return map(PathDistribution
, found
)
735 def _search_paths(cls
, name
, paths
):
736 """Find metadata directories in paths heuristically."""
737 prepared
= Prepared(name
)
738 return itertools
.chain
.from_iterable(
739 path
.search(prepared
) for path
in map(FastPath
, paths
)
742 def invalidate_caches(cls
):
743 FastPath
.__new
__.cache_clear()
746 class PathDistribution(Distribution
):
747 def __init__(self
, path
: SimplePath
):
748 """Construct a distribution.
750 :param path: SimplePath indicating the metadata directory.
754 def read_text(self
, filename
):
762 return self
._path
.joinpath(filename
).read_text(encoding
='utf-8')
764 read_text
.__doc
__ = Distribution
.read_text
.__doc
__
766 def locate_file(self
, path
):
767 return self
._path
.parent
/ path
770 def _normalized_name(self
):
772 Performance optimization: where possible, resolve the
773 normalized name from the file system path.
775 stem
= os
.path
.basename(str(self
._path
))
777 pass_none(Prepared
.normalize
)(self
._name
_from
_stem
(stem
))
778 or super()._normalized
_name
782 def _name_from_stem(stem
):
784 >>> PathDistribution._name_from_stem('foo-3.0.egg-info')
786 >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
788 >>> PathDistribution._name_from_stem('face.egg-info')
790 >>> PathDistribution._name_from_stem('foo.bar')
792 filename
, ext
= os
.path
.splitext(stem
)
793 if ext
not in ('.dist-info', '.egg-info'):
795 name
, sep
, rest
= filename
.partition('-')
799 def distribution(distribution_name
):
800 """Get the ``Distribution`` instance for the named package.
802 :param distribution_name: The name of the distribution package as a string.
803 :return: A ``Distribution`` instance (or subclass thereof).
805 return Distribution
.from_name(distribution_name
)
808 def distributions(**kwargs
):
809 """Get all ``Distribution`` instances in the current environment.
811 :return: An iterable of ``Distribution`` instances.
813 return Distribution
.discover(**kwargs
)
816 def metadata(distribution_name
) -> _meta
.PackageMetadata
:
817 """Get the metadata for the named package.
819 :param distribution_name: The name of the distribution package to query.
820 :return: A PackageMetadata containing the parsed metadata.
822 return Distribution
.from_name(distribution_name
).metadata
825 def version(distribution_name
):
826 """Get the version string for the named package.
828 :param distribution_name: The name of the distribution package to query.
829 :return: The version string for the package as defined in the package's
830 "Version" metadata key.
832 return distribution(distribution_name
).version
835 _unique
= functools
.partial(
837 key
=_py39compat
.normalized_name
,
840 Wrapper for ``distributions`` to return unique distributions by name.
844 def entry_points(**params
) -> EntryPoints
:
845 """Return EntryPoint objects for all installed packages.
847 Pass selection parameters (group or name) to filter the
848 result to entry points matching those properties (see
849 EntryPoints.select()).
851 :return: EntryPoints for all installed packages.
853 eps
= itertools
.chain
.from_iterable(
854 dist
.entry_points
for dist
in _unique(distributions())
856 return EntryPoints(eps
).select(**params
)
859 def files(distribution_name
):
860 """Return a list of files for the named package.
862 :param distribution_name: The name of the distribution package to query.
863 :return: List of files composing the distribution.
865 return distribution(distribution_name
).files
868 def requires(distribution_name
):
870 Return a list of requirements for the named package.
872 :return: An iterator of requirements, suitable for
873 packaging.requirement.Requirement.
875 return distribution(distribution_name
).requires
878 def packages_distributions() -> Mapping
[str, List
[str]]:
880 Return a mapping of top-level packages to their
883 >>> import collections.abc
884 >>> pkgs = packages_distributions()
885 >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
888 pkg_to_dist
= collections
.defaultdict(list)
889 for dist
in distributions():
890 for pkg
in _top_level_declared(dist
) or _top_level_inferred(dist
):
891 pkg_to_dist
[pkg
].append(dist
.metadata
['Name'])
892 return dict(pkg_to_dist
)
895 def _top_level_declared(dist
):
896 return (dist
.read_text('top_level.txt') or '').split()
899 def _top_level_inferred(dist
):
901 f
.parts
[0] if len(f
.parts
) > 1 else f
.with_suffix('').name
902 for f
in always_iterable(dist
.files
)