Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/importlib_metadata/__init__.py: 66%
390 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 06:10 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 06:10 +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 contextlib
17import collections
19from . import _adapters, _meta, _py39compat
20from ._collections import FreezableDefaultDict, Pair
21from ._compat import (
22 NullFinder,
23 StrPath,
24 install,
25 pypy_partial,
26)
27from ._functools import method_cache, pass_none
28from ._itertools import always_iterable, unique_everseen
29from ._meta import PackageMetadata, SimplePath
31from contextlib import suppress
32from importlib import import_module
33from importlib.abc import MetaPathFinder
34from itertools import starmap
35from typing import Iterable, List, Mapping, Optional, Set, cast
37__all__ = [
38 'Distribution',
39 'DistributionFinder',
40 'PackageMetadata',
41 'PackageNotFoundError',
42 'distribution',
43 'distributions',
44 'entry_points',
45 'files',
46 'metadata',
47 'packages_distributions',
48 'requires',
49 'version',
50]
53class PackageNotFoundError(ModuleNotFoundError):
54 """The package was not found."""
56 def __str__(self) -> str:
57 return f"No package metadata was found for {self.name}"
59 @property
60 def name(self) -> str: # type: ignore[override]
61 (name,) = self.args
62 return name
65class Sectioned:
66 """
67 A simple entry point config parser for performance
69 >>> for item in Sectioned.read(Sectioned._sample):
70 ... print(item)
71 Pair(name='sec1', value='# comments ignored')
72 Pair(name='sec1', value='a = 1')
73 Pair(name='sec1', value='b = 2')
74 Pair(name='sec2', value='a = 2')
76 >>> res = Sectioned.section_pairs(Sectioned._sample)
77 >>> item = next(res)
78 >>> item.name
79 'sec1'
80 >>> item.value
81 Pair(name='a', value='1')
82 >>> item = next(res)
83 >>> item.value
84 Pair(name='b', value='2')
85 >>> item = next(res)
86 >>> item.name
87 'sec2'
88 >>> item.value
89 Pair(name='a', value='2')
90 >>> list(res)
91 []
92 """
94 _sample = textwrap.dedent(
95 """
96 [sec1]
97 # comments ignored
98 a = 1
99 b = 2
101 [sec2]
102 a = 2
103 """
104 ).lstrip()
106 @classmethod
107 def section_pairs(cls, text):
108 return (
109 section._replace(value=Pair.parse(section.value))
110 for section in cls.read(text, filter_=cls.valid)
111 if section.name is not None
112 )
114 @staticmethod
115 def read(text, filter_=None):
116 lines = filter(filter_, map(str.strip, text.splitlines()))
117 name = None
118 for value in lines:
119 section_match = value.startswith('[') and value.endswith(']')
120 if section_match:
121 name = value.strip('[]')
122 continue
123 yield Pair(name, value)
125 @staticmethod
126 def valid(line: str):
127 return line and not line.startswith('#')
130class DeprecatedTuple:
131 """
132 Provide subscript item access for backward compatibility.
134 >>> recwarn = getfixture('recwarn')
135 >>> ep = EntryPoint(name='name', value='value', group='group')
136 >>> ep[:]
137 ('name', 'value', 'group')
138 >>> ep[0]
139 'name'
140 >>> len(recwarn)
141 1
142 """
144 # Do not remove prior to 2023-05-01 or Python 3.13
145 _warn = functools.partial(
146 warnings.warn,
147 "EntryPoint tuple interface is deprecated. Access members by name.",
148 DeprecationWarning,
149 stacklevel=pypy_partial(2),
150 )
152 def __getitem__(self, item):
153 self._warn()
154 return self._key()[item]
157class EntryPoint(DeprecatedTuple):
158 """An entry point as defined by Python packaging conventions.
160 See `the packaging docs on entry points
161 <https://packaging.python.org/specifications/entry-points/>`_
162 for more information.
164 >>> ep = EntryPoint(
165 ... name=None, group=None, value='package.module:attr [extra1, extra2]')
166 >>> ep.module
167 'package.module'
168 >>> ep.attr
169 'attr'
170 >>> ep.extras
171 ['extra1', 'extra2']
172 """
174 pattern = re.compile(
175 r'(?P<module>[\w.]+)\s*'
176 r'(:\s*(?P<attr>[\w.]+)\s*)?'
177 r'((?P<extras>\[.*\])\s*)?$'
178 )
179 """
180 A regular expression describing the syntax for an entry point,
181 which might look like:
183 - module
184 - package.module
185 - package.module:attribute
186 - package.module:object.attribute
187 - package.module:attr [extra1, extra2]
189 Other combinations are possible as well.
191 The expression is lenient about whitespace around the ':',
192 following the attr, and following any extras.
193 """
195 name: str
196 value: str
197 group: str
199 dist: Optional['Distribution'] = None
201 def __init__(self, name: str, value: str, group: str) -> None:
202 vars(self).update(name=name, value=value, group=group)
204 def load(self):
205 """Load the entry point from its definition. If only a module
206 is indicated by the value, return that module. Otherwise,
207 return the named object.
208 """
209 match = self.pattern.match(self.value)
210 module = import_module(match.group('module'))
211 attrs = filter(None, (match.group('attr') or '').split('.'))
212 return functools.reduce(getattr, attrs, module)
214 @property
215 def module(self) -> str:
216 match = self.pattern.match(self.value)
217 assert match is not None
218 return match.group('module')
220 @property
221 def attr(self) -> str:
222 match = self.pattern.match(self.value)
223 assert match is not None
224 return match.group('attr')
226 @property
227 def extras(self) -> List[str]:
228 match = self.pattern.match(self.value)
229 assert match is not None
230 return re.findall(r'\w+', match.group('extras') or '')
232 def _for(self, dist):
233 vars(self).update(dist=dist)
234 return self
236 def matches(self, **params):
237 """
238 EntryPoint matches the given parameters.
240 >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]')
241 >>> ep.matches(group='foo')
242 True
243 >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]')
244 True
245 >>> ep.matches(group='foo', name='other')
246 False
247 >>> ep.matches()
248 True
249 >>> ep.matches(extras=['extra1', 'extra2'])
250 True
251 >>> ep.matches(module='bing')
252 True
253 >>> ep.matches(attr='bong')
254 True
255 """
256 attrs = (getattr(self, param) for param in params)
257 return all(map(operator.eq, params.values(), attrs))
259 def _key(self):
260 return self.name, self.value, self.group
262 def __lt__(self, other):
263 return self._key() < other._key()
265 def __eq__(self, other):
266 return self._key() == other._key()
268 def __setattr__(self, name, value):
269 raise AttributeError("EntryPoint objects are immutable.")
271 def __repr__(self):
272 return (
273 f'EntryPoint(name={self.name!r}, value={self.value!r}, '
274 f'group={self.group!r})'
275 )
277 def __hash__(self) -> int:
278 return hash(self._key())
281class EntryPoints(tuple):
282 """
283 An immutable collection of selectable EntryPoint objects.
284 """
286 __slots__ = ()
288 def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
289 """
290 Get the EntryPoint in self matching name.
291 """
292 try:
293 return next(iter(self.select(name=name)))
294 except StopIteration:
295 raise KeyError(name)
297 def select(self, **params):
298 """
299 Select entry points from self that match the
300 given parameters (typically group and/or name).
301 """
302 return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params))
304 @property
305 def names(self) -> Set[str]:
306 """
307 Return the set of all names of all entry points.
308 """
309 return {ep.name for ep in self}
311 @property
312 def groups(self) -> Set[str]:
313 """
314 Return the set of all groups of all entry points.
315 """
316 return {ep.group for ep in self}
318 @classmethod
319 def _from_text_for(cls, text, dist):
320 return cls(ep._for(dist) for ep in cls._from_text(text))
322 @staticmethod
323 def _from_text(text):
324 return (
325 EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
326 for item in Sectioned.section_pairs(text or '')
327 )
330class PackagePath(pathlib.PurePosixPath):
331 """A reference to a path in a package"""
333 hash: Optional["FileHash"]
334 size: int
335 dist: "Distribution"
337 def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
338 with self.locate().open(encoding=encoding) as stream:
339 return stream.read()
341 def read_binary(self) -> bytes:
342 with self.locate().open('rb') as stream:
343 return stream.read()
345 def locate(self) -> pathlib.Path:
346 """Return a path-like object for this path"""
347 return self.dist.locate_file(self)
350class FileHash:
351 def __init__(self, spec: str) -> None:
352 self.mode, _, self.value = spec.partition('=')
354 def __repr__(self) -> str:
355 return f'<FileHash mode: {self.mode} value: {self.value}>'
358class DeprecatedNonAbstract:
359 def __new__(cls, *args, **kwargs):
360 all_names = {
361 name for subclass in inspect.getmro(cls) for name in vars(subclass)
362 }
363 abstract = {
364 name
365 for name in all_names
366 if getattr(getattr(cls, name), '__isabstractmethod__', False)
367 }
368 if abstract:
369 warnings.warn(
370 f"Unimplemented abstract methods {abstract}",
371 DeprecationWarning,
372 stacklevel=2,
373 )
374 return super().__new__(cls)
377class Distribution(DeprecatedNonAbstract):
378 """A Python distribution package."""
380 @abc.abstractmethod
381 def read_text(self, filename) -> Optional[str]:
382 """Attempt to load metadata file given by the name.
384 :param filename: The name of the file in the distribution info.
385 :return: The text if found, otherwise None.
386 """
388 @abc.abstractmethod
389 def locate_file(self, path: StrPath) -> pathlib.Path:
390 """
391 Given a path to a file in this distribution, return a path
392 to it.
393 """
395 @classmethod
396 def from_name(cls, name: str) -> "Distribution":
397 """Return the Distribution for the given package name.
399 :param name: The name of the distribution package to search for.
400 :return: The Distribution instance (or subclass thereof) for the named
401 package, if found.
402 :raises PackageNotFoundError: When the named package's distribution
403 metadata cannot be found.
404 :raises ValueError: When an invalid value is supplied for name.
405 """
406 if not name:
407 raise ValueError("A distribution name is required.")
408 try:
409 return next(iter(cls.discover(name=name)))
410 except StopIteration:
411 raise PackageNotFoundError(name)
413 @classmethod
414 def discover(cls, **kwargs) -> Iterable["Distribution"]:
415 """Return an iterable of Distribution objects for all packages.
417 Pass a ``context`` or pass keyword arguments for constructing
418 a context.
420 :context: A ``DistributionFinder.Context`` object.
421 :return: Iterable of Distribution objects for all packages.
422 """
423 context = kwargs.pop('context', None)
424 if context and kwargs:
425 raise ValueError("cannot accept context and kwargs")
426 context = context or DistributionFinder.Context(**kwargs)
427 return itertools.chain.from_iterable(
428 resolver(context) for resolver in cls._discover_resolvers()
429 )
431 @staticmethod
432 def at(path: StrPath) -> "Distribution":
433 """Return a Distribution for the indicated metadata path
435 :param path: a string or path-like object
436 :return: a concrete Distribution instance for the path
437 """
438 return PathDistribution(pathlib.Path(path))
440 @staticmethod
441 def _discover_resolvers():
442 """Search the meta_path for resolvers."""
443 declared = (
444 getattr(finder, 'find_distributions', None) for finder in sys.meta_path
445 )
446 return filter(None, declared)
448 @property
449 def metadata(self) -> _meta.PackageMetadata:
450 """Return the parsed metadata for this Distribution.
452 The returned object will have keys that name the various bits of
453 metadata. See PEP 566 for details.
454 """
455 opt_text = (
456 self.read_text('METADATA')
457 or self.read_text('PKG-INFO')
458 # This last clause is here to support old egg-info files. Its
459 # effect is to just end up using the PathDistribution's self._path
460 # (which points to the egg-info file) attribute unchanged.
461 or self.read_text('')
462 )
463 text = cast(str, opt_text)
464 return _adapters.Message(email.message_from_string(text))
466 @property
467 def name(self) -> str:
468 """Return the 'Name' metadata for the distribution package."""
469 return self.metadata['Name']
471 @property
472 def _normalized_name(self):
473 """Return a normalized version of the name."""
474 return Prepared.normalize(self.name)
476 @property
477 def version(self) -> str:
478 """Return the 'Version' metadata for the distribution package."""
479 return self.metadata['Version']
481 @property
482 def entry_points(self) -> EntryPoints:
483 return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
485 @property
486 def files(self) -> Optional[List[PackagePath]]:
487 """Files in this distribution.
489 :return: List of PackagePath for this distribution or None
491 Result is `None` if the metadata file that enumerates files
492 (i.e. RECORD for dist-info, or installed-files.txt or
493 SOURCES.txt for egg-info) is missing.
494 Result may be empty if the metadata exists but is empty.
495 """
497 def make_file(name, hash=None, size_str=None):
498 result = PackagePath(name)
499 result.hash = FileHash(hash) if hash else None
500 result.size = int(size_str) if size_str else None
501 result.dist = self
502 return result
504 @pass_none
505 def make_files(lines):
506 return starmap(make_file, csv.reader(lines))
508 @pass_none
509 def skip_missing_files(package_paths):
510 return list(filter(lambda path: path.locate().exists(), package_paths))
512 return skip_missing_files(
513 make_files(
514 self._read_files_distinfo()
515 or self._read_files_egginfo_installed()
516 or self._read_files_egginfo_sources()
517 )
518 )
520 def _read_files_distinfo(self):
521 """
522 Read the lines of RECORD
523 """
524 text = self.read_text('RECORD')
525 return text and text.splitlines()
527 def _read_files_egginfo_installed(self):
528 """
529 Read installed-files.txt and return lines in a similar
530 CSV-parsable format as RECORD: each file must be placed
531 relative to the site-packages directory and must also be
532 quoted (since file names can contain literal commas).
534 This file is written when the package is installed by pip,
535 but it might not be written for other installation methods.
536 Assume the file is accurate if it exists.
537 """
538 text = self.read_text('installed-files.txt')
539 # Prepend the .egg-info/ subdir to the lines in this file.
540 # But this subdir is only available from PathDistribution's
541 # self._path.
542 subdir = getattr(self, '_path', None)
543 if not text or not subdir:
544 return
546 paths = (
547 (subdir / name)
548 .resolve()
549 .relative_to(self.locate_file('').resolve())
550 .as_posix()
551 for name in text.splitlines()
552 )
553 return map('"{}"'.format, paths)
555 def _read_files_egginfo_sources(self):
556 """
557 Read SOURCES.txt and return lines in a similar CSV-parsable
558 format as RECORD: each file name must be quoted (since it
559 might contain literal commas).
561 Note that SOURCES.txt is not a reliable source for what
562 files are installed by a package. This file is generated
563 for a source archive, and the files that are present
564 there (e.g. setup.py) may not correctly reflect the files
565 that are present after the package has been installed.
566 """
567 text = self.read_text('SOURCES.txt')
568 return text and map('"{}"'.format, text.splitlines())
570 @property
571 def requires(self) -> Optional[List[str]]:
572 """Generated requirements specified for this Distribution"""
573 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
574 return reqs and list(reqs)
576 def _read_dist_info_reqs(self):
577 return self.metadata.get_all('Requires-Dist')
579 def _read_egg_info_reqs(self):
580 source = self.read_text('requires.txt')
581 return pass_none(self._deps_from_requires_text)(source)
583 @classmethod
584 def _deps_from_requires_text(cls, source):
585 return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
587 @staticmethod
588 def _convert_egg_info_reqs_to_simple_reqs(sections):
589 """
590 Historically, setuptools would solicit and store 'extra'
591 requirements, including those with environment markers,
592 in separate sections. More modern tools expect each
593 dependency to be defined separately, with any relevant
594 extras and environment markers attached directly to that
595 requirement. This method converts the former to the
596 latter. See _test_deps_from_requires_text for an example.
597 """
599 def make_condition(name):
600 return name and f'extra == "{name}"'
602 def quoted_marker(section):
603 section = section or ''
604 extra, sep, markers = section.partition(':')
605 if extra and markers:
606 markers = f'({markers})'
607 conditions = list(filter(None, [markers, make_condition(extra)]))
608 return '; ' + ' and '.join(conditions) if conditions else ''
610 def url_req_space(req):
611 """
612 PEP 508 requires a space between the url_spec and the quoted_marker.
613 Ref python/importlib_metadata#357.
614 """
615 # '@' is uniquely indicative of a url_req.
616 return ' ' * ('@' in req)
618 for section in sections:
619 space = url_req_space(section.value)
620 yield section.value + space + quoted_marker(section.name)
623class DistributionFinder(MetaPathFinder):
624 """
625 A MetaPathFinder capable of discovering installed distributions.
626 """
628 class Context:
629 """
630 Keyword arguments presented by the caller to
631 ``distributions()`` or ``Distribution.discover()``
632 to narrow the scope of a search for distributions
633 in all DistributionFinders.
635 Each DistributionFinder may expect any parameters
636 and should attempt to honor the canonical
637 parameters defined below when appropriate.
638 """
640 name = None
641 """
642 Specific name for which a distribution finder should match.
643 A name of ``None`` matches all distributions.
644 """
646 def __init__(self, **kwargs):
647 vars(self).update(kwargs)
649 @property
650 def path(self) -> List[str]:
651 """
652 The sequence of directory path that a distribution finder
653 should search.
655 Typically refers to Python installed package paths such as
656 "site-packages" directories and defaults to ``sys.path``.
657 """
658 return vars(self).get('path', sys.path)
660 @abc.abstractmethod
661 def find_distributions(self, context=Context()) -> Iterable[Distribution]:
662 """
663 Find distributions.
665 Return an iterable of all Distribution instances capable of
666 loading the metadata for packages matching the ``context``,
667 a DistributionFinder.Context instance.
668 """
671class FastPath:
672 """
673 Micro-optimized class for searching a path for
674 children.
676 >>> FastPath('').children()
677 ['...']
678 """
680 @functools.lru_cache() # type: ignore
681 def __new__(cls, root):
682 return super().__new__(cls)
684 def __init__(self, root):
685 self.root = root
687 def joinpath(self, child):
688 return pathlib.Path(self.root, child)
690 def children(self):
691 with suppress(Exception):
692 return os.listdir(self.root or '.')
693 with suppress(Exception):
694 return self.zip_children()
695 return []
697 def zip_children(self):
698 zip_path = zipp.Path(self.root)
699 names = zip_path.root.namelist()
700 self.joinpath = zip_path.joinpath
702 return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
704 def search(self, name):
705 return self.lookup(self.mtime).search(name)
707 @property
708 def mtime(self):
709 with suppress(OSError):
710 return os.stat(self.root).st_mtime
711 self.lookup.cache_clear()
713 @method_cache
714 def lookup(self, mtime):
715 return Lookup(self)
718class Lookup:
719 def __init__(self, path: FastPath):
720 base = os.path.basename(path.root).lower()
721 base_is_egg = base.endswith(".egg")
722 self.infos = FreezableDefaultDict(list)
723 self.eggs = FreezableDefaultDict(list)
725 for child in path.children():
726 low = child.lower()
727 if low.endswith((".dist-info", ".egg-info")):
728 # rpartition is faster than splitext and suitable for this purpose.
729 name = low.rpartition(".")[0].partition("-")[0]
730 normalized = Prepared.normalize(name)
731 self.infos[normalized].append(path.joinpath(child))
732 elif base_is_egg and low == "egg-info":
733 name = base.rpartition(".")[0].partition("-")[0]
734 legacy_normalized = Prepared.legacy_normalize(name)
735 self.eggs[legacy_normalized].append(path.joinpath(child))
737 self.infos.freeze()
738 self.eggs.freeze()
740 def search(self, prepared):
741 infos = (
742 self.infos[prepared.normalized]
743 if prepared
744 else itertools.chain.from_iterable(self.infos.values())
745 )
746 eggs = (
747 self.eggs[prepared.legacy_normalized]
748 if prepared
749 else itertools.chain.from_iterable(self.eggs.values())
750 )
751 return itertools.chain(infos, eggs)
754class Prepared:
755 """
756 A prepared search for metadata on a possibly-named package.
757 """
759 normalized = None
760 legacy_normalized = None
762 def __init__(self, name):
763 self.name = name
764 if name is None:
765 return
766 self.normalized = self.normalize(name)
767 self.legacy_normalized = self.legacy_normalize(name)
769 @staticmethod
770 def normalize(name):
771 """
772 PEP 503 normalization plus dashes as underscores.
773 """
774 return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
776 @staticmethod
777 def legacy_normalize(name):
778 """
779 Normalize the package name as found in the convention in
780 older packaging tools versions and specs.
781 """
782 return name.lower().replace('-', '_')
784 def __bool__(self):
785 return bool(self.name)
788@install
789class MetadataPathFinder(NullFinder, DistributionFinder):
790 """A degenerate finder for distribution packages on the file system.
792 This finder supplies only a find_distributions() method for versions
793 of Python that do not have a PathFinder find_distributions().
794 """
796 def find_distributions(
797 self, context=DistributionFinder.Context()
798 ) -> Iterable["PathDistribution"]:
799 """
800 Find distributions.
802 Return an iterable of all Distribution instances capable of
803 loading the metadata for packages matching ``context.name``
804 (or all names if ``None`` indicated) along the paths in the list
805 of directories ``context.path``.
806 """
807 found = self._search_paths(context.name, context.path)
808 return map(PathDistribution, found)
810 @classmethod
811 def _search_paths(cls, name, paths):
812 """Find metadata directories in paths heuristically."""
813 prepared = Prepared(name)
814 return itertools.chain.from_iterable(
815 path.search(prepared) for path in map(FastPath, paths)
816 )
818 def invalidate_caches(cls) -> None:
819 FastPath.__new__.cache_clear()
822class PathDistribution(Distribution):
823 def __init__(self, path: SimplePath) -> None:
824 """Construct a distribution.
826 :param path: SimplePath indicating the metadata directory.
827 """
828 self._path = path
830 def read_text(self, filename: StrPath) -> Optional[str]:
831 with suppress(
832 FileNotFoundError,
833 IsADirectoryError,
834 KeyError,
835 NotADirectoryError,
836 PermissionError,
837 ):
838 return self._path.joinpath(filename).read_text(encoding='utf-8')
840 return None
842 read_text.__doc__ = Distribution.read_text.__doc__
844 def locate_file(self, path: StrPath) -> pathlib.Path:
845 return self._path.parent / path
847 @property
848 def _normalized_name(self):
849 """
850 Performance optimization: where possible, resolve the
851 normalized name from the file system path.
852 """
853 stem = os.path.basename(str(self._path))
854 return (
855 pass_none(Prepared.normalize)(self._name_from_stem(stem))
856 or super()._normalized_name
857 )
859 @staticmethod
860 def _name_from_stem(stem):
861 """
862 >>> PathDistribution._name_from_stem('foo-3.0.egg-info')
863 'foo'
864 >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
865 'CherryPy'
866 >>> PathDistribution._name_from_stem('face.egg-info')
867 'face'
868 >>> PathDistribution._name_from_stem('foo.bar')
869 """
870 filename, ext = os.path.splitext(stem)
871 if ext not in ('.dist-info', '.egg-info'):
872 return
873 name, sep, rest = filename.partition('-')
874 return name
877def distribution(distribution_name) -> Distribution:
878 """Get the ``Distribution`` instance for the named package.
880 :param distribution_name: The name of the distribution package as a string.
881 :return: A ``Distribution`` instance (or subclass thereof).
882 """
883 return Distribution.from_name(distribution_name)
886def distributions(**kwargs) -> Iterable[Distribution]:
887 """Get all ``Distribution`` instances in the current environment.
889 :return: An iterable of ``Distribution`` instances.
890 """
891 return Distribution.discover(**kwargs)
894def metadata(distribution_name) -> _meta.PackageMetadata:
895 """Get the metadata for the named package.
897 :param distribution_name: The name of the distribution package to query.
898 :return: A PackageMetadata containing the parsed metadata.
899 """
900 return Distribution.from_name(distribution_name).metadata
903def version(distribution_name) -> str:
904 """Get the version string for the named package.
906 :param distribution_name: The name of the distribution package to query.
907 :return: The version string for the package as defined in the package's
908 "Version" metadata key.
909 """
910 return distribution(distribution_name).version
913_unique = functools.partial(
914 unique_everseen,
915 key=_py39compat.normalized_name,
916)
917"""
918Wrapper for ``distributions`` to return unique distributions by name.
919"""
922def entry_points(**params) -> EntryPoints:
923 """Return EntryPoint objects for all installed packages.
925 Pass selection parameters (group or name) to filter the
926 result to entry points matching those properties (see
927 EntryPoints.select()).
929 :return: EntryPoints for all installed packages.
930 """
931 eps = itertools.chain.from_iterable(
932 dist.entry_points for dist in _unique(distributions())
933 )
934 return EntryPoints(eps).select(**params)
937def files(distribution_name) -> Optional[List[PackagePath]]:
938 """Return a list of files for the named package.
940 :param distribution_name: The name of the distribution package to query.
941 :return: List of files composing the distribution.
942 """
943 return distribution(distribution_name).files
946def requires(distribution_name) -> Optional[List[str]]:
947 """
948 Return a list of requirements for the named package.
950 :return: An iterable of requirements, suitable for
951 packaging.requirement.Requirement.
952 """
953 return distribution(distribution_name).requires
956def packages_distributions() -> Mapping[str, List[str]]:
957 """
958 Return a mapping of top-level packages to their
959 distributions.
961 >>> import collections.abc
962 >>> pkgs = packages_distributions()
963 >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
964 True
965 """
966 pkg_to_dist = collections.defaultdict(list)
967 for dist in distributions():
968 for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
969 pkg_to_dist[pkg].append(dist.metadata['Name'])
970 return dict(pkg_to_dist)
973def _top_level_declared(dist):
974 return (dist.read_text('top_level.txt') or '').split()
977def _top_level_inferred(dist):
978 opt_names = {
979 f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f)
980 for f in always_iterable(dist.files)
981 }
983 @pass_none
984 def importable_name(name):
985 return '.' not in name
987 return filter(importable_name, opt_names)