Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/importlib_metadata/__init__.py: 65%
393 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
1import os
2import re
3import abc
4import csv
5import sys
6import zipp
7import email
8import inspect
9import pathlib
10import operator
11import textwrap
12import warnings
13import functools
14import itertools
15import posixpath
16import collections
18from . import _adapters, _meta, _py39compat
19from ._collections import FreezableDefaultDict, Pair
20from ._compat import (
21 NullFinder,
22 StrPath,
23 install,
24 pypy_partial,
25)
26from ._functools import method_cache, pass_none
27from ._itertools import always_iterable, unique_everseen
28from ._meta import PackageMetadata, SimplePath
30from contextlib import suppress
31from importlib import import_module
32from importlib.abc import MetaPathFinder
33from itertools import starmap
34from typing import Iterable, List, Mapping, Optional, Set, cast
36__all__ = [
37 'Distribution',
38 'DistributionFinder',
39 'PackageMetadata',
40 'PackageNotFoundError',
41 'distribution',
42 'distributions',
43 'entry_points',
44 'files',
45 'metadata',
46 'packages_distributions',
47 'requires',
48 'version',
49]
52class PackageNotFoundError(ModuleNotFoundError):
53 """The package was not found."""
55 def __str__(self) -> str:
56 return f"No package metadata was found for {self.name}"
58 @property
59 def name(self) -> str: # type: ignore[override]
60 (name,) = self.args
61 return name
64class Sectioned:
65 """
66 A simple entry point config parser for performance
68 >>> for item in Sectioned.read(Sectioned._sample):
69 ... print(item)
70 Pair(name='sec1', value='# comments ignored')
71 Pair(name='sec1', value='a = 1')
72 Pair(name='sec1', value='b = 2')
73 Pair(name='sec2', value='a = 2')
75 >>> res = Sectioned.section_pairs(Sectioned._sample)
76 >>> item = next(res)
77 >>> item.name
78 'sec1'
79 >>> item.value
80 Pair(name='a', value='1')
81 >>> item = next(res)
82 >>> item.value
83 Pair(name='b', value='2')
84 >>> item = next(res)
85 >>> item.name
86 'sec2'
87 >>> item.value
88 Pair(name='a', value='2')
89 >>> list(res)
90 []
91 """
93 _sample = textwrap.dedent(
94 """
95 [sec1]
96 # comments ignored
97 a = 1
98 b = 2
100 [sec2]
101 a = 2
102 """
103 ).lstrip()
105 @classmethod
106 def section_pairs(cls, text):
107 return (
108 section._replace(value=Pair.parse(section.value))
109 for section in cls.read(text, filter_=cls.valid)
110 if section.name is not None
111 )
113 @staticmethod
114 def read(text, filter_=None):
115 lines = filter(filter_, map(str.strip, text.splitlines()))
116 name = None
117 for value in lines:
118 section_match = value.startswith('[') and value.endswith(']')
119 if section_match:
120 name = value.strip('[]')
121 continue
122 yield Pair(name, value)
124 @staticmethod
125 def valid(line: str):
126 return line and not line.startswith('#')
129class DeprecatedTuple:
130 """
131 Provide subscript item access for backward compatibility.
133 >>> recwarn = getfixture('recwarn')
134 >>> ep = EntryPoint(name='name', value='value', group='group')
135 >>> ep[:]
136 ('name', 'value', 'group')
137 >>> ep[0]
138 'name'
139 >>> len(recwarn)
140 1
141 """
143 # Do not remove prior to 2023-05-01 or Python 3.13
144 _warn = functools.partial(
145 warnings.warn,
146 "EntryPoint tuple interface is deprecated. Access members by name.",
147 DeprecationWarning,
148 stacklevel=pypy_partial(2),
149 )
151 def __getitem__(self, item):
152 self._warn()
153 return self._key()[item]
156class EntryPoint(DeprecatedTuple):
157 """An entry point as defined by Python packaging conventions.
159 See `the packaging docs on entry points
160 <https://packaging.python.org/specifications/entry-points/>`_
161 for more information.
163 >>> ep = EntryPoint(
164 ... name=None, group=None, value='package.module:attr [extra1, extra2]')
165 >>> ep.module
166 'package.module'
167 >>> ep.attr
168 'attr'
169 >>> ep.extras
170 ['extra1', 'extra2']
171 """
173 pattern = re.compile(
174 r'(?P<module>[\w.]+)\s*'
175 r'(:\s*(?P<attr>[\w.]+)\s*)?'
176 r'((?P<extras>\[.*\])\s*)?$'
177 )
178 """
179 A regular expression describing the syntax for an entry point,
180 which might look like:
182 - module
183 - package.module
184 - package.module:attribute
185 - package.module:object.attribute
186 - package.module:attr [extra1, extra2]
188 Other combinations are possible as well.
190 The expression is lenient about whitespace around the ':',
191 following the attr, and following any extras.
192 """
194 name: str
195 value: str
196 group: str
198 dist: Optional['Distribution'] = None
200 def __init__(self, name: str, value: str, group: str) -> None:
201 vars(self).update(name=name, value=value, group=group)
203 def load(self):
204 """Load the entry point from its definition. If only a module
205 is indicated by the value, return that module. Otherwise,
206 return the named object.
207 """
208 match = self.pattern.match(self.value)
209 module = import_module(match.group('module'))
210 attrs = filter(None, (match.group('attr') or '').split('.'))
211 return functools.reduce(getattr, attrs, module)
213 @property
214 def module(self) -> str:
215 match = self.pattern.match(self.value)
216 assert match is not None
217 return match.group('module')
219 @property
220 def attr(self) -> str:
221 match = self.pattern.match(self.value)
222 assert match is not None
223 return match.group('attr')
225 @property
226 def extras(self) -> List[str]:
227 match = self.pattern.match(self.value)
228 assert match is not None
229 return re.findall(r'\w+', match.group('extras') or '')
231 def _for(self, dist):
232 vars(self).update(dist=dist)
233 return self
235 def matches(self, **params):
236 """
237 EntryPoint matches the given parameters.
239 >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]')
240 >>> ep.matches(group='foo')
241 True
242 >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]')
243 True
244 >>> ep.matches(group='foo', name='other')
245 False
246 >>> ep.matches()
247 True
248 >>> ep.matches(extras=['extra1', 'extra2'])
249 True
250 >>> ep.matches(module='bing')
251 True
252 >>> ep.matches(attr='bong')
253 True
254 """
255 attrs = (getattr(self, param) for param in params)
256 return all(map(operator.eq, params.values(), attrs))
258 def _key(self):
259 return self.name, self.value, self.group
261 def __lt__(self, other):
262 return self._key() < other._key()
264 def __eq__(self, other):
265 return self._key() == other._key()
267 def __setattr__(self, name, value):
268 raise AttributeError("EntryPoint objects are immutable.")
270 def __repr__(self):
271 return (
272 f'EntryPoint(name={self.name!r}, value={self.value!r}, '
273 f'group={self.group!r})'
274 )
276 def __hash__(self) -> int:
277 return hash(self._key())
280class EntryPoints(tuple):
281 """
282 An immutable collection of selectable EntryPoint objects.
283 """
285 __slots__ = ()
287 def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
288 """
289 Get the EntryPoint in self matching name.
290 """
291 try:
292 return next(iter(self.select(name=name)))
293 except StopIteration:
294 raise KeyError(name)
296 def select(self, **params):
297 """
298 Select entry points from self that match the
299 given parameters (typically group and/or name).
300 """
301 return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params))
303 @property
304 def names(self) -> Set[str]:
305 """
306 Return the set of all names of all entry points.
307 """
308 return {ep.name for ep in self}
310 @property
311 def groups(self) -> Set[str]:
312 """
313 Return the set of all groups of all entry points.
314 """
315 return {ep.group for ep in self}
317 @classmethod
318 def _from_text_for(cls, text, dist):
319 return cls(ep._for(dist) for ep in cls._from_text(text))
321 @staticmethod
322 def _from_text(text):
323 return (
324 EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
325 for item in Sectioned.section_pairs(text or '')
326 )
329class PackagePath(pathlib.PurePosixPath):
330 """A reference to a path in a package"""
332 hash: Optional["FileHash"]
333 size: int
334 dist: "Distribution"
336 def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
337 with self.locate().open(encoding=encoding) as stream:
338 return stream.read()
340 def read_binary(self) -> bytes:
341 with self.locate().open('rb') as stream:
342 return stream.read()
344 def locate(self) -> pathlib.Path:
345 """Return a path-like object for this path"""
346 return self.dist.locate_file(self)
349class FileHash:
350 def __init__(self, spec: str) -> None:
351 self.mode, _, self.value = spec.partition('=')
353 def __repr__(self) -> str:
354 return f'<FileHash mode: {self.mode} value: {self.value}>'
357class DeprecatedNonAbstract:
358 def __new__(cls, *args, **kwargs):
359 all_names = {
360 name for subclass in inspect.getmro(cls) for name in vars(subclass)
361 }
362 abstract = {
363 name
364 for name in all_names
365 if getattr(getattr(cls, name), '__isabstractmethod__', False)
366 }
367 if abstract:
368 warnings.warn(
369 f"Unimplemented abstract methods {abstract}",
370 DeprecationWarning,
371 stacklevel=2,
372 )
373 return super().__new__(cls)
376class Distribution(DeprecatedNonAbstract):
377 """A Python distribution package."""
379 @abc.abstractmethod
380 def read_text(self, filename) -> Optional[str]:
381 """Attempt to load metadata file given by the name.
383 :param filename: The name of the file in the distribution info.
384 :return: The text if found, otherwise None.
385 """
387 @abc.abstractmethod
388 def locate_file(self, path: StrPath) -> pathlib.Path:
389 """
390 Given a path to a file in this distribution, return a path
391 to it.
392 """
394 @classmethod
395 def from_name(cls, name: str) -> "Distribution":
396 """Return the Distribution for the given package name.
398 :param name: The name of the distribution package to search for.
399 :return: The Distribution instance (or subclass thereof) for the named
400 package, if found.
401 :raises PackageNotFoundError: When the named package's distribution
402 metadata cannot be found.
403 :raises ValueError: When an invalid value is supplied for name.
404 """
405 if not name:
406 raise ValueError("A distribution name is required.")
407 try:
408 return next(iter(cls.discover(name=name)))
409 except StopIteration:
410 raise PackageNotFoundError(name)
412 @classmethod
413 def discover(cls, **kwargs) -> Iterable["Distribution"]:
414 """Return an iterable of Distribution objects for all packages.
416 Pass a ``context`` or pass keyword arguments for constructing
417 a context.
419 :context: A ``DistributionFinder.Context`` object.
420 :return: Iterable of Distribution objects for all packages.
421 """
422 context = kwargs.pop('context', None)
423 if context and kwargs:
424 raise ValueError("cannot accept context and kwargs")
425 context = context or DistributionFinder.Context(**kwargs)
426 return itertools.chain.from_iterable(
427 resolver(context) for resolver in cls._discover_resolvers()
428 )
430 @staticmethod
431 def at(path: StrPath) -> "Distribution":
432 """Return a Distribution for the indicated metadata path
434 :param path: a string or path-like object
435 :return: a concrete Distribution instance for the path
436 """
437 return PathDistribution(pathlib.Path(path))
439 @staticmethod
440 def _discover_resolvers():
441 """Search the meta_path for resolvers."""
442 declared = (
443 getattr(finder, 'find_distributions', None) for finder in sys.meta_path
444 )
445 return filter(None, declared)
447 @property
448 def metadata(self) -> _meta.PackageMetadata:
449 """Return the parsed metadata for this Distribution.
451 The returned object will have keys that name the various bits of
452 metadata. See PEP 566 for details.
453 """
454 opt_text = (
455 self.read_text('METADATA')
456 or self.read_text('PKG-INFO')
457 # This last clause is here to support old egg-info files. Its
458 # effect is to just end up using the PathDistribution's self._path
459 # (which points to the egg-info file) attribute unchanged.
460 or self.read_text('')
461 )
462 text = cast(str, opt_text)
463 return _adapters.Message(email.message_from_string(text))
465 @property
466 def name(self) -> str:
467 """Return the 'Name' metadata for the distribution package."""
468 return self.metadata['Name']
470 @property
471 def _normalized_name(self):
472 """Return a normalized version of the name."""
473 return Prepared.normalize(self.name)
475 @property
476 def version(self) -> str:
477 """Return the 'Version' metadata for the distribution package."""
478 return self.metadata['Version']
480 @property
481 def entry_points(self) -> EntryPoints:
482 return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
484 @property
485 def files(self) -> Optional[List[PackagePath]]:
486 """Files in this distribution.
488 :return: List of PackagePath for this distribution or None
490 Result is `None` if the metadata file that enumerates files
491 (i.e. RECORD for dist-info, or installed-files.txt or
492 SOURCES.txt for egg-info) is missing.
493 Result may be empty if the metadata exists but is empty.
494 """
496 def make_file(name, hash=None, size_str=None):
497 result = PackagePath(name)
498 result.hash = FileHash(hash) if hash else None
499 result.size = int(size_str) if size_str else None
500 result.dist = self
501 return result
503 @pass_none
504 def make_files(lines):
505 return starmap(make_file, csv.reader(lines))
507 @pass_none
508 def skip_missing_files(package_paths):
509 return list(filter(lambda path: path.locate().exists(), package_paths))
511 return skip_missing_files(
512 make_files(
513 self._read_files_distinfo()
514 or self._read_files_egginfo_installed()
515 or self._read_files_egginfo_sources()
516 )
517 )
519 def _read_files_distinfo(self):
520 """
521 Read the lines of RECORD
522 """
523 text = self.read_text('RECORD')
524 return text and text.splitlines()
526 def _read_files_egginfo_installed(self):
527 """
528 Read installed-files.txt and return lines in a similar
529 CSV-parsable format as RECORD: each file must be placed
530 relative to the site-packages directory and must also be
531 quoted (since file names can contain literal commas).
533 This file is written when the package is installed by pip,
534 but it might not be written for other installation methods.
535 Assume the file is accurate if it exists.
536 """
537 text = self.read_text('installed-files.txt')
538 # Prepend the .egg-info/ subdir to the lines in this file.
539 # But this subdir is only available from PathDistribution's
540 # self._path.
541 subdir = getattr(self, '_path', None)
542 if not text or not subdir:
543 return
545 paths = (
546 (subdir / name)
547 .resolve()
548 .relative_to(self.locate_file('').resolve())
549 .as_posix()
550 for name in text.splitlines()
551 )
552 return map('"{}"'.format, paths)
554 def _read_files_egginfo_sources(self):
555 """
556 Read SOURCES.txt and return lines in a similar CSV-parsable
557 format as RECORD: each file name must be quoted (since it
558 might contain literal commas).
560 Note that SOURCES.txt is not a reliable source for what
561 files are installed by a package. This file is generated
562 for a source archive, and the files that are present
563 there (e.g. setup.py) may not correctly reflect the files
564 that are present after the package has been installed.
565 """
566 text = self.read_text('SOURCES.txt')
567 return text and map('"{}"'.format, text.splitlines())
569 @property
570 def requires(self) -> Optional[List[str]]:
571 """Generated requirements specified for this Distribution"""
572 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
573 return reqs and list(reqs)
575 def _read_dist_info_reqs(self):
576 return self.metadata.get_all('Requires-Dist')
578 def _read_egg_info_reqs(self):
579 source = self.read_text('requires.txt')
580 return pass_none(self._deps_from_requires_text)(source)
582 @classmethod
583 def _deps_from_requires_text(cls, source):
584 return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
586 @staticmethod
587 def _convert_egg_info_reqs_to_simple_reqs(sections):
588 """
589 Historically, setuptools would solicit and store 'extra'
590 requirements, including those with environment markers,
591 in separate sections. More modern tools expect each
592 dependency to be defined separately, with any relevant
593 extras and environment markers attached directly to that
594 requirement. This method converts the former to the
595 latter. See _test_deps_from_requires_text for an example.
596 """
598 def make_condition(name):
599 return name and f'extra == "{name}"'
601 def quoted_marker(section):
602 section = section or ''
603 extra, sep, markers = section.partition(':')
604 if extra and markers:
605 markers = f'({markers})'
606 conditions = list(filter(None, [markers, make_condition(extra)]))
607 return '; ' + ' and '.join(conditions) if conditions else ''
609 def url_req_space(req):
610 """
611 PEP 508 requires a space between the url_spec and the quoted_marker.
612 Ref python/importlib_metadata#357.
613 """
614 # '@' is uniquely indicative of a url_req.
615 return ' ' * ('@' in req)
617 for section in sections:
618 space = url_req_space(section.value)
619 yield section.value + space + quoted_marker(section.name)
622class DistributionFinder(MetaPathFinder):
623 """
624 A MetaPathFinder capable of discovering installed distributions.
625 """
627 class Context:
628 """
629 Keyword arguments presented by the caller to
630 ``distributions()`` or ``Distribution.discover()``
631 to narrow the scope of a search for distributions
632 in all DistributionFinders.
634 Each DistributionFinder may expect any parameters
635 and should attempt to honor the canonical
636 parameters defined below when appropriate.
637 """
639 name = None
640 """
641 Specific name for which a distribution finder should match.
642 A name of ``None`` matches all distributions.
643 """
645 def __init__(self, **kwargs):
646 vars(self).update(kwargs)
648 @property
649 def path(self) -> List[str]:
650 """
651 The sequence of directory path that a distribution finder
652 should search.
654 Typically refers to Python installed package paths such as
655 "site-packages" directories and defaults to ``sys.path``.
656 """
657 return vars(self).get('path', sys.path)
659 @abc.abstractmethod
660 def find_distributions(self, context=Context()) -> Iterable[Distribution]:
661 """
662 Find distributions.
664 Return an iterable of all Distribution instances capable of
665 loading the metadata for packages matching the ``context``,
666 a DistributionFinder.Context instance.
667 """
670class FastPath:
671 """
672 Micro-optimized class for searching a path for
673 children.
675 >>> FastPath('').children()
676 ['...']
677 """
679 @functools.lru_cache() # type: ignore
680 def __new__(cls, root):
681 return super().__new__(cls)
683 def __init__(self, root):
684 self.root = root
686 def joinpath(self, child):
687 return pathlib.Path(self.root, child)
689 def children(self):
690 with suppress(Exception):
691 return os.listdir(self.root or '.')
692 with suppress(Exception):
693 return self.zip_children()
694 return []
696 def zip_children(self):
697 zip_path = zipp.Path(self.root)
698 names = zip_path.root.namelist()
699 self.joinpath = zip_path.joinpath
701 return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
703 def search(self, name):
704 return self.lookup(self.mtime).search(name)
706 @property
707 def mtime(self):
708 with suppress(OSError):
709 return os.stat(self.root).st_mtime
710 self.lookup.cache_clear()
712 @method_cache
713 def lookup(self, mtime):
714 return Lookup(self)
717class Lookup:
718 def __init__(self, path: FastPath):
719 base = os.path.basename(path.root).lower()
720 base_is_egg = base.endswith(".egg")
721 self.infos = FreezableDefaultDict(list)
722 self.eggs = FreezableDefaultDict(list)
724 for child in path.children():
725 low = child.lower()
726 if low.endswith((".dist-info", ".egg-info")):
727 # rpartition is faster than splitext and suitable for this purpose.
728 name = low.rpartition(".")[0].partition("-")[0]
729 normalized = Prepared.normalize(name)
730 self.infos[normalized].append(path.joinpath(child))
731 elif base_is_egg and low == "egg-info":
732 name = base.rpartition(".")[0].partition("-")[0]
733 legacy_normalized = Prepared.legacy_normalize(name)
734 self.eggs[legacy_normalized].append(path.joinpath(child))
736 self.infos.freeze()
737 self.eggs.freeze()
739 def search(self, prepared):
740 infos = (
741 self.infos[prepared.normalized]
742 if prepared
743 else itertools.chain.from_iterable(self.infos.values())
744 )
745 eggs = (
746 self.eggs[prepared.legacy_normalized]
747 if prepared
748 else itertools.chain.from_iterable(self.eggs.values())
749 )
750 return itertools.chain(infos, eggs)
753class Prepared:
754 """
755 A prepared search for metadata on a possibly-named package.
756 """
758 normalized = None
759 legacy_normalized = None
761 def __init__(self, name):
762 self.name = name
763 if name is None:
764 return
765 self.normalized = self.normalize(name)
766 self.legacy_normalized = self.legacy_normalize(name)
768 @staticmethod
769 def normalize(name):
770 """
771 PEP 503 normalization plus dashes as underscores.
772 """
773 return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
775 @staticmethod
776 def legacy_normalize(name):
777 """
778 Normalize the package name as found in the convention in
779 older packaging tools versions and specs.
780 """
781 return name.lower().replace('-', '_')
783 def __bool__(self):
784 return bool(self.name)
787@install
788class MetadataPathFinder(NullFinder, DistributionFinder):
789 """A degenerate finder for distribution packages on the file system.
791 This finder supplies only a find_distributions() method for versions
792 of Python that do not have a PathFinder find_distributions().
793 """
795 def find_distributions(
796 self, context=DistributionFinder.Context()
797 ) -> Iterable["PathDistribution"]:
798 """
799 Find distributions.
801 Return an iterable of all Distribution instances capable of
802 loading the metadata for packages matching ``context.name``
803 (or all names if ``None`` indicated) along the paths in the list
804 of directories ``context.path``.
805 """
806 found = self._search_paths(context.name, context.path)
807 return map(PathDistribution, found)
809 @classmethod
810 def _search_paths(cls, name, paths):
811 """Find metadata directories in paths heuristically."""
812 prepared = Prepared(name)
813 return itertools.chain.from_iterable(
814 path.search(prepared) for path in map(FastPath, paths)
815 )
817 def invalidate_caches(cls) -> None:
818 FastPath.__new__.cache_clear()
821class PathDistribution(Distribution):
822 def __init__(self, path: SimplePath) -> None:
823 """Construct a distribution.
825 :param path: SimplePath indicating the metadata directory.
826 """
827 self._path = path
829 def read_text(self, filename: StrPath) -> Optional[str]:
830 with suppress(
831 FileNotFoundError,
832 IsADirectoryError,
833 KeyError,
834 NotADirectoryError,
835 PermissionError,
836 ):
837 return self._path.joinpath(filename).read_text(encoding='utf-8')
839 return None
841 read_text.__doc__ = Distribution.read_text.__doc__
843 def locate_file(self, path: StrPath) -> pathlib.Path:
844 return self._path.parent / path
846 @property
847 def _normalized_name(self):
848 """
849 Performance optimization: where possible, resolve the
850 normalized name from the file system path.
851 """
852 stem = os.path.basename(str(self._path))
853 return (
854 pass_none(Prepared.normalize)(self._name_from_stem(stem))
855 or super()._normalized_name
856 )
858 @staticmethod
859 def _name_from_stem(stem):
860 """
861 >>> PathDistribution._name_from_stem('foo-3.0.egg-info')
862 'foo'
863 >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
864 'CherryPy'
865 >>> PathDistribution._name_from_stem('face.egg-info')
866 'face'
867 >>> PathDistribution._name_from_stem('foo.bar')
868 """
869 filename, ext = os.path.splitext(stem)
870 if ext not in ('.dist-info', '.egg-info'):
871 return
872 name, sep, rest = filename.partition('-')
873 return name
876def distribution(distribution_name) -> Distribution:
877 """Get the ``Distribution`` instance for the named package.
879 :param distribution_name: The name of the distribution package as a string.
880 :return: A ``Distribution`` instance (or subclass thereof).
881 """
882 return Distribution.from_name(distribution_name)
885def distributions(**kwargs) -> Iterable[Distribution]:
886 """Get all ``Distribution`` instances in the current environment.
888 :return: An iterable of ``Distribution`` instances.
889 """
890 return Distribution.discover(**kwargs)
893def metadata(distribution_name) -> _meta.PackageMetadata:
894 """Get the metadata for the named package.
896 :param distribution_name: The name of the distribution package to query.
897 :return: A PackageMetadata containing the parsed metadata.
898 """
899 return Distribution.from_name(distribution_name).metadata
902def version(distribution_name) -> str:
903 """Get the version string for the named package.
905 :param distribution_name: The name of the distribution package to query.
906 :return: The version string for the package as defined in the package's
907 "Version" metadata key.
908 """
909 return distribution(distribution_name).version
912_unique = functools.partial(
913 unique_everseen,
914 key=_py39compat.normalized_name,
915)
916"""
917Wrapper for ``distributions`` to return unique distributions by name.
918"""
921def entry_points(**params) -> EntryPoints:
922 """Return EntryPoint objects for all installed packages.
924 Pass selection parameters (group or name) to filter the
925 result to entry points matching those properties (see
926 EntryPoints.select()).
928 :return: EntryPoints for all installed packages.
929 """
930 eps = itertools.chain.from_iterable(
931 dist.entry_points for dist in _unique(distributions())
932 )
933 return EntryPoints(eps).select(**params)
936def files(distribution_name) -> Optional[List[PackagePath]]:
937 """Return a list of files for the named package.
939 :param distribution_name: The name of the distribution package to query.
940 :return: List of files composing the distribution.
941 """
942 return distribution(distribution_name).files
945def requires(distribution_name) -> Optional[List[str]]:
946 """
947 Return a list of requirements for the named package.
949 :return: An iterable of requirements, suitable for
950 packaging.requirement.Requirement.
951 """
952 return distribution(distribution_name).requires
955def packages_distributions() -> Mapping[str, List[str]]:
956 """
957 Return a mapping of top-level packages to their
958 distributions.
960 >>> import collections.abc
961 >>> pkgs = packages_distributions()
962 >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
963 True
964 """
965 pkg_to_dist = collections.defaultdict(list)
966 for dist in distributions():
967 for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
968 pkg_to_dist[pkg].append(dist.metadata['Name'])
969 return dict(pkg_to_dist)
972def _top_level_declared(dist):
973 return (dist.read_text('top_level.txt') or '').split()
976def _topmost(name: PackagePath) -> Optional[str]:
977 """
978 Return the top-most parent as long as there is a parent.
979 """
980 top, *rest = name.parts
981 return top if rest else None
984def _get_toplevel_name(name: PackagePath) -> str:
985 """
986 Infer a possibly importable module name from a name presumed on
987 sys.path.
989 >>> _get_toplevel_name(PackagePath('foo.py'))
990 'foo'
991 >>> _get_toplevel_name(PackagePath('foo'))
992 'foo'
993 >>> _get_toplevel_name(PackagePath('foo.pyc'))
994 'foo'
995 >>> _get_toplevel_name(PackagePath('foo/__init__.py'))
996 'foo'
997 >>> _get_toplevel_name(PackagePath('foo.pth'))
998 'foo.pth'
999 >>> _get_toplevel_name(PackagePath('foo.dist-info'))
1000 'foo.dist-info'
1001 """
1002 return _topmost(name) or (
1003 # python/typeshed#10328
1004 inspect.getmodulename(name) # type: ignore
1005 or str(name)
1006 )
1009def _top_level_inferred(dist):
1010 opt_names = set(map(_get_toplevel_name, always_iterable(dist.files)))
1012 def importable_name(name):
1013 return '.' not in name
1015 return filter(importable_name, opt_names)