Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/importlib_metadata/__init__.py: 48%
397 statements
« prev ^ index » next coverage.py v7.3.3, created at 2023-12-15 06:13 +0000
« prev ^ index » next coverage.py v7.3.3, created at 2023-12-15 06:13 +0000
1import os
2import re
3import abc
4import csv
5import sys
6import json
7import zipp
8import email
9import types
10import inspect
11import pathlib
12import operator
13import textwrap
14import warnings
15import functools
16import itertools
17import posixpath
18import collections
20from . import _adapters, _meta, _py39compat
21from ._collections import FreezableDefaultDict, Pair
22from ._compat import (
23 NullFinder,
24 StrPath,
25 install,
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 EntryPoint:
131 """An entry point as defined by Python packaging conventions.
133 See `the packaging docs on entry points
134 <https://packaging.python.org/specifications/entry-points/>`_
135 for more information.
137 >>> ep = EntryPoint(
138 ... name=None, group=None, value='package.module:attr [extra1, extra2]')
139 >>> ep.module
140 'package.module'
141 >>> ep.attr
142 'attr'
143 >>> ep.extras
144 ['extra1', 'extra2']
145 """
147 pattern = re.compile(
148 r'(?P<module>[\w.]+)\s*'
149 r'(:\s*(?P<attr>[\w.]+)\s*)?'
150 r'((?P<extras>\[.*\])\s*)?$'
151 )
152 """
153 A regular expression describing the syntax for an entry point,
154 which might look like:
156 - module
157 - package.module
158 - package.module:attribute
159 - package.module:object.attribute
160 - package.module:attr [extra1, extra2]
162 Other combinations are possible as well.
164 The expression is lenient about whitespace around the ':',
165 following the attr, and following any extras.
166 """
168 name: str
169 value: str
170 group: str
172 dist: Optional['Distribution'] = None
174 def __init__(self, name: str, value: str, group: str) -> None:
175 vars(self).update(name=name, value=value, group=group)
177 def load(self):
178 """Load the entry point from its definition. If only a module
179 is indicated by the value, return that module. Otherwise,
180 return the named object.
181 """
182 match = self.pattern.match(self.value)
183 module = import_module(match.group('module'))
184 attrs = filter(None, (match.group('attr') or '').split('.'))
185 return functools.reduce(getattr, attrs, module)
187 @property
188 def module(self) -> str:
189 match = self.pattern.match(self.value)
190 assert match is not None
191 return match.group('module')
193 @property
194 def attr(self) -> str:
195 match = self.pattern.match(self.value)
196 assert match is not None
197 return match.group('attr')
199 @property
200 def extras(self) -> List[str]:
201 match = self.pattern.match(self.value)
202 assert match is not None
203 return re.findall(r'\w+', match.group('extras') or '')
205 def _for(self, dist):
206 vars(self).update(dist=dist)
207 return self
209 def matches(self, **params):
210 """
211 EntryPoint matches the given parameters.
213 >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]')
214 >>> ep.matches(group='foo')
215 True
216 >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]')
217 True
218 >>> ep.matches(group='foo', name='other')
219 False
220 >>> ep.matches()
221 True
222 >>> ep.matches(extras=['extra1', 'extra2'])
223 True
224 >>> ep.matches(module='bing')
225 True
226 >>> ep.matches(attr='bong')
227 True
228 """
229 attrs = (getattr(self, param) for param in params)
230 return all(map(operator.eq, params.values(), attrs))
232 def _key(self):
233 return self.name, self.value, self.group
235 def __lt__(self, other):
236 return self._key() < other._key()
238 def __eq__(self, other):
239 return self._key() == other._key()
241 def __setattr__(self, name, value):
242 raise AttributeError("EntryPoint objects are immutable.")
244 def __repr__(self):
245 return (
246 f'EntryPoint(name={self.name!r}, value={self.value!r}, '
247 f'group={self.group!r})'
248 )
250 def __hash__(self) -> int:
251 return hash(self._key())
254class EntryPoints(tuple):
255 """
256 An immutable collection of selectable EntryPoint objects.
257 """
259 __slots__ = ()
261 def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
262 """
263 Get the EntryPoint in self matching name.
264 """
265 try:
266 return next(iter(self.select(name=name)))
267 except StopIteration:
268 raise KeyError(name)
270 def __repr__(self):
271 """
272 Repr with classname and tuple constructor to
273 signal that we deviate from regular tuple behavior.
274 """
275 return '%s(%r)' % (self.__class__.__name__, tuple(self))
277 def select(self, **params):
278 """
279 Select entry points from self that match the
280 given parameters (typically group and/or name).
281 """
282 return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params))
284 @property
285 def names(self) -> Set[str]:
286 """
287 Return the set of all names of all entry points.
288 """
289 return {ep.name for ep in self}
291 @property
292 def groups(self) -> Set[str]:
293 """
294 Return the set of all groups of all entry points.
295 """
296 return {ep.group for ep in self}
298 @classmethod
299 def _from_text_for(cls, text, dist):
300 return cls(ep._for(dist) for ep in cls._from_text(text))
302 @staticmethod
303 def _from_text(text):
304 return (
305 EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
306 for item in Sectioned.section_pairs(text or '')
307 )
310class PackagePath(pathlib.PurePosixPath):
311 """A reference to a path in a package"""
313 hash: Optional["FileHash"]
314 size: int
315 dist: "Distribution"
317 def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
318 with self.locate().open(encoding=encoding) as stream:
319 return stream.read()
321 def read_binary(self) -> bytes:
322 with self.locate().open('rb') as stream:
323 return stream.read()
325 def locate(self) -> pathlib.Path:
326 """Return a path-like object for this path"""
327 return self.dist.locate_file(self)
330class FileHash:
331 def __init__(self, spec: str) -> None:
332 self.mode, _, self.value = spec.partition('=')
334 def __repr__(self) -> str:
335 return f'<FileHash mode: {self.mode} value: {self.value}>'
338class DeprecatedNonAbstract:
339 def __new__(cls, *args, **kwargs):
340 all_names = {
341 name for subclass in inspect.getmro(cls) for name in vars(subclass)
342 }
343 abstract = {
344 name
345 for name in all_names
346 if getattr(getattr(cls, name), '__isabstractmethod__', False)
347 }
348 if abstract:
349 warnings.warn(
350 f"Unimplemented abstract methods {abstract}",
351 DeprecationWarning,
352 stacklevel=2,
353 )
354 return super().__new__(cls)
357class Distribution(DeprecatedNonAbstract):
358 """A Python distribution package."""
360 @abc.abstractmethod
361 def read_text(self, filename) -> Optional[str]:
362 """Attempt to load metadata file given by the name.
364 :param filename: The name of the file in the distribution info.
365 :return: The text if found, otherwise None.
366 """
368 @abc.abstractmethod
369 def locate_file(self, path: StrPath) -> pathlib.Path:
370 """
371 Given a path to a file in this distribution, return a path
372 to it.
373 """
375 @classmethod
376 def from_name(cls, name: str) -> "Distribution":
377 """Return the Distribution for the given package name.
379 :param name: The name of the distribution package to search for.
380 :return: The Distribution instance (or subclass thereof) for the named
381 package, if found.
382 :raises PackageNotFoundError: When the named package's distribution
383 metadata cannot be found.
384 :raises ValueError: When an invalid value is supplied for name.
385 """
386 if not name:
387 raise ValueError("A distribution name is required.")
388 try:
389 return next(iter(cls.discover(name=name)))
390 except StopIteration:
391 raise PackageNotFoundError(name)
393 @classmethod
394 def discover(cls, **kwargs) -> Iterable["Distribution"]:
395 """Return an iterable of Distribution objects for all packages.
397 Pass a ``context`` or pass keyword arguments for constructing
398 a context.
400 :context: A ``DistributionFinder.Context`` object.
401 :return: Iterable of Distribution objects for all packages.
402 """
403 context = kwargs.pop('context', None)
404 if context and kwargs:
405 raise ValueError("cannot accept context and kwargs")
406 context = context or DistributionFinder.Context(**kwargs)
407 return itertools.chain.from_iterable(
408 resolver(context) for resolver in cls._discover_resolvers()
409 )
411 @staticmethod
412 def at(path: StrPath) -> "Distribution":
413 """Return a Distribution for the indicated metadata path
415 :param path: a string or path-like object
416 :return: a concrete Distribution instance for the path
417 """
418 return PathDistribution(pathlib.Path(path))
420 @staticmethod
421 def _discover_resolvers():
422 """Search the meta_path for resolvers."""
423 declared = (
424 getattr(finder, 'find_distributions', None) for finder in sys.meta_path
425 )
426 return filter(None, declared)
428 @property
429 def metadata(self) -> _meta.PackageMetadata:
430 """Return the parsed metadata for this Distribution.
432 The returned object will have keys that name the various bits of
433 metadata. See PEP 566 for details.
434 """
435 opt_text = (
436 self.read_text('METADATA')
437 or self.read_text('PKG-INFO')
438 # This last clause is here to support old egg-info files. Its
439 # effect is to just end up using the PathDistribution's self._path
440 # (which points to the egg-info file) attribute unchanged.
441 or self.read_text('')
442 )
443 text = cast(str, opt_text)
444 return _adapters.Message(email.message_from_string(text))
446 @property
447 def name(self) -> str:
448 """Return the 'Name' metadata for the distribution package."""
449 return self.metadata['Name']
451 @property
452 def _normalized_name(self):
453 """Return a normalized version of the name."""
454 return Prepared.normalize(self.name)
456 @property
457 def version(self) -> str:
458 """Return the 'Version' metadata for the distribution package."""
459 return self.metadata['Version']
461 @property
462 def entry_points(self) -> EntryPoints:
463 return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
465 @property
466 def files(self) -> Optional[List[PackagePath]]:
467 """Files in this distribution.
469 :return: List of PackagePath for this distribution or None
471 Result is `None` if the metadata file that enumerates files
472 (i.e. RECORD for dist-info, or installed-files.txt or
473 SOURCES.txt for egg-info) is missing.
474 Result may be empty if the metadata exists but is empty.
475 """
477 def make_file(name, hash=None, size_str=None):
478 result = PackagePath(name)
479 result.hash = FileHash(hash) if hash else None
480 result.size = int(size_str) if size_str else None
481 result.dist = self
482 return result
484 @pass_none
485 def make_files(lines):
486 return starmap(make_file, csv.reader(lines))
488 @pass_none
489 def skip_missing_files(package_paths):
490 return list(filter(lambda path: path.locate().exists(), package_paths))
492 return skip_missing_files(
493 make_files(
494 self._read_files_distinfo()
495 or self._read_files_egginfo_installed()
496 or self._read_files_egginfo_sources()
497 )
498 )
500 def _read_files_distinfo(self):
501 """
502 Read the lines of RECORD
503 """
504 text = self.read_text('RECORD')
505 return text and text.splitlines()
507 def _read_files_egginfo_installed(self):
508 """
509 Read installed-files.txt and return lines in a similar
510 CSV-parsable format as RECORD: each file must be placed
511 relative to the site-packages directory and must also be
512 quoted (since file names can contain literal commas).
514 This file is written when the package is installed by pip,
515 but it might not be written for other installation methods.
516 Assume the file is accurate if it exists.
517 """
518 text = self.read_text('installed-files.txt')
519 # Prepend the .egg-info/ subdir to the lines in this file.
520 # But this subdir is only available from PathDistribution's
521 # self._path.
522 subdir = getattr(self, '_path', None)
523 if not text or not subdir:
524 return
526 paths = (
527 (subdir / name)
528 .resolve()
529 .relative_to(self.locate_file('').resolve())
530 .as_posix()
531 for name in text.splitlines()
532 )
533 return map('"{}"'.format, paths)
535 def _read_files_egginfo_sources(self):
536 """
537 Read SOURCES.txt and return lines in a similar CSV-parsable
538 format as RECORD: each file name must be quoted (since it
539 might contain literal commas).
541 Note that SOURCES.txt is not a reliable source for what
542 files are installed by a package. This file is generated
543 for a source archive, and the files that are present
544 there (e.g. setup.py) may not correctly reflect the files
545 that are present after the package has been installed.
546 """
547 text = self.read_text('SOURCES.txt')
548 return text and map('"{}"'.format, text.splitlines())
550 @property
551 def requires(self) -> Optional[List[str]]:
552 """Generated requirements specified for this Distribution"""
553 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
554 return reqs and list(reqs)
556 def _read_dist_info_reqs(self):
557 return self.metadata.get_all('Requires-Dist')
559 def _read_egg_info_reqs(self):
560 source = self.read_text('requires.txt')
561 return pass_none(self._deps_from_requires_text)(source)
563 @classmethod
564 def _deps_from_requires_text(cls, source):
565 return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
567 @staticmethod
568 def _convert_egg_info_reqs_to_simple_reqs(sections):
569 """
570 Historically, setuptools would solicit and store 'extra'
571 requirements, including those with environment markers,
572 in separate sections. More modern tools expect each
573 dependency to be defined separately, with any relevant
574 extras and environment markers attached directly to that
575 requirement. This method converts the former to the
576 latter. See _test_deps_from_requires_text for an example.
577 """
579 def make_condition(name):
580 return name and f'extra == "{name}"'
582 def quoted_marker(section):
583 section = section or ''
584 extra, sep, markers = section.partition(':')
585 if extra and markers:
586 markers = f'({markers})'
587 conditions = list(filter(None, [markers, make_condition(extra)]))
588 return '; ' + ' and '.join(conditions) if conditions else ''
590 def url_req_space(req):
591 """
592 PEP 508 requires a space between the url_spec and the quoted_marker.
593 Ref python/importlib_metadata#357.
594 """
595 # '@' is uniquely indicative of a url_req.
596 return ' ' * ('@' in req)
598 for section in sections:
599 space = url_req_space(section.value)
600 yield section.value + space + quoted_marker(section.name)
602 @property
603 def origin(self):
604 return self._load_json('direct_url.json')
606 def _load_json(self, filename):
607 return pass_none(json.loads)(
608 self.read_text(filename),
609 object_hook=lambda data: types.SimpleNamespace(**data),
610 )
613class DistributionFinder(MetaPathFinder):
614 """
615 A MetaPathFinder capable of discovering installed distributions.
616 """
618 class Context:
619 """
620 Keyword arguments presented by the caller to
621 ``distributions()`` or ``Distribution.discover()``
622 to narrow the scope of a search for distributions
623 in all DistributionFinders.
625 Each DistributionFinder may expect any parameters
626 and should attempt to honor the canonical
627 parameters defined below when appropriate.
628 """
630 name = None
631 """
632 Specific name for which a distribution finder should match.
633 A name of ``None`` matches all distributions.
634 """
636 def __init__(self, **kwargs):
637 vars(self).update(kwargs)
639 @property
640 def path(self) -> List[str]:
641 """
642 The sequence of directory path that a distribution finder
643 should search.
645 Typically refers to Python installed package paths such as
646 "site-packages" directories and defaults to ``sys.path``.
647 """
648 return vars(self).get('path', sys.path)
650 @abc.abstractmethod
651 def find_distributions(self, context=Context()) -> Iterable[Distribution]:
652 """
653 Find distributions.
655 Return an iterable of all Distribution instances capable of
656 loading the metadata for packages matching the ``context``,
657 a DistributionFinder.Context instance.
658 """
661class FastPath:
662 """
663 Micro-optimized class for searching a path for
664 children.
666 >>> FastPath('').children()
667 ['...']
668 """
670 @functools.lru_cache() # type: ignore
671 def __new__(cls, root):
672 return super().__new__(cls)
674 def __init__(self, root):
675 self.root = root
677 def joinpath(self, child):
678 return pathlib.Path(self.root, child)
680 def children(self):
681 with suppress(Exception):
682 return os.listdir(self.root or '.')
683 with suppress(Exception):
684 return self.zip_children()
685 return []
687 def zip_children(self):
688 zip_path = zipp.Path(self.root)
689 names = zip_path.root.namelist()
690 self.joinpath = zip_path.joinpath
692 return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
694 def search(self, name):
695 return self.lookup(self.mtime).search(name)
697 @property
698 def mtime(self):
699 with suppress(OSError):
700 return os.stat(self.root).st_mtime
701 self.lookup.cache_clear()
703 @method_cache
704 def lookup(self, mtime):
705 return Lookup(self)
708class Lookup:
709 def __init__(self, path: FastPath):
710 base = os.path.basename(path.root).lower()
711 base_is_egg = base.endswith(".egg")
712 self.infos = FreezableDefaultDict(list)
713 self.eggs = FreezableDefaultDict(list)
715 for child in path.children():
716 low = child.lower()
717 if low.endswith((".dist-info", ".egg-info")):
718 # rpartition is faster than splitext and suitable for this purpose.
719 name = low.rpartition(".")[0].partition("-")[0]
720 normalized = Prepared.normalize(name)
721 self.infos[normalized].append(path.joinpath(child))
722 elif base_is_egg and low == "egg-info":
723 name = base.rpartition(".")[0].partition("-")[0]
724 legacy_normalized = Prepared.legacy_normalize(name)
725 self.eggs[legacy_normalized].append(path.joinpath(child))
727 self.infos.freeze()
728 self.eggs.freeze()
730 def search(self, prepared):
731 infos = (
732 self.infos[prepared.normalized]
733 if prepared
734 else itertools.chain.from_iterable(self.infos.values())
735 )
736 eggs = (
737 self.eggs[prepared.legacy_normalized]
738 if prepared
739 else itertools.chain.from_iterable(self.eggs.values())
740 )
741 return itertools.chain(infos, eggs)
744class Prepared:
745 """
746 A prepared search for metadata on a possibly-named package.
747 """
749 normalized = None
750 legacy_normalized = None
752 def __init__(self, name):
753 self.name = name
754 if name is None:
755 return
756 self.normalized = self.normalize(name)
757 self.legacy_normalized = self.legacy_normalize(name)
759 @staticmethod
760 def normalize(name):
761 """
762 PEP 503 normalization plus dashes as underscores.
763 """
764 return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
766 @staticmethod
767 def legacy_normalize(name):
768 """
769 Normalize the package name as found in the convention in
770 older packaging tools versions and specs.
771 """
772 return name.lower().replace('-', '_')
774 def __bool__(self):
775 return bool(self.name)
778@install
779class MetadataPathFinder(NullFinder, DistributionFinder):
780 """A degenerate finder for distribution packages on the file system.
782 This finder supplies only a find_distributions() method for versions
783 of Python that do not have a PathFinder find_distributions().
784 """
786 def find_distributions(
787 self, context=DistributionFinder.Context()
788 ) -> Iterable["PathDistribution"]:
789 """
790 Find distributions.
792 Return an iterable of all Distribution instances capable of
793 loading the metadata for packages matching ``context.name``
794 (or all names if ``None`` indicated) along the paths in the list
795 of directories ``context.path``.
796 """
797 found = self._search_paths(context.name, context.path)
798 return map(PathDistribution, found)
800 @classmethod
801 def _search_paths(cls, name, paths):
802 """Find metadata directories in paths heuristically."""
803 prepared = Prepared(name)
804 return itertools.chain.from_iterable(
805 path.search(prepared) for path in map(FastPath, paths)
806 )
808 def invalidate_caches(cls) -> None:
809 FastPath.__new__.cache_clear()
812class PathDistribution(Distribution):
813 def __init__(self, path: SimplePath) -> None:
814 """Construct a distribution.
816 :param path: SimplePath indicating the metadata directory.
817 """
818 self._path = path
820 def read_text(self, filename: StrPath) -> Optional[str]:
821 with suppress(
822 FileNotFoundError,
823 IsADirectoryError,
824 KeyError,
825 NotADirectoryError,
826 PermissionError,
827 ):
828 return self._path.joinpath(filename).read_text(encoding='utf-8')
830 return None
832 read_text.__doc__ = Distribution.read_text.__doc__
834 def locate_file(self, path: StrPath) -> pathlib.Path:
835 return self._path.parent / path
837 @property
838 def _normalized_name(self):
839 """
840 Performance optimization: where possible, resolve the
841 normalized name from the file system path.
842 """
843 stem = os.path.basename(str(self._path))
844 return (
845 pass_none(Prepared.normalize)(self._name_from_stem(stem))
846 or super()._normalized_name
847 )
849 @staticmethod
850 def _name_from_stem(stem):
851 """
852 >>> PathDistribution._name_from_stem('foo-3.0.egg-info')
853 'foo'
854 >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
855 'CherryPy'
856 >>> PathDistribution._name_from_stem('face.egg-info')
857 'face'
858 >>> PathDistribution._name_from_stem('foo.bar')
859 """
860 filename, ext = os.path.splitext(stem)
861 if ext not in ('.dist-info', '.egg-info'):
862 return
863 name, sep, rest = filename.partition('-')
864 return name
867def distribution(distribution_name: str) -> Distribution:
868 """Get the ``Distribution`` instance for the named package.
870 :param distribution_name: The name of the distribution package as a string.
871 :return: A ``Distribution`` instance (or subclass thereof).
872 """
873 return Distribution.from_name(distribution_name)
876def distributions(**kwargs) -> Iterable[Distribution]:
877 """Get all ``Distribution`` instances in the current environment.
879 :return: An iterable of ``Distribution`` instances.
880 """
881 return Distribution.discover(**kwargs)
884def metadata(distribution_name: str) -> _meta.PackageMetadata:
885 """Get the metadata for the named package.
887 :param distribution_name: The name of the distribution package to query.
888 :return: A PackageMetadata containing the parsed metadata.
889 """
890 return Distribution.from_name(distribution_name).metadata
893def version(distribution_name: str) -> str:
894 """Get the version string for the named package.
896 :param distribution_name: The name of the distribution package to query.
897 :return: The version string for the package as defined in the package's
898 "Version" metadata key.
899 """
900 return distribution(distribution_name).version
903_unique = functools.partial(
904 unique_everseen,
905 key=_py39compat.normalized_name,
906)
907"""
908Wrapper for ``distributions`` to return unique distributions by name.
909"""
912def entry_points(**params) -> EntryPoints:
913 """Return EntryPoint objects for all installed packages.
915 Pass selection parameters (group or name) to filter the
916 result to entry points matching those properties (see
917 EntryPoints.select()).
919 :return: EntryPoints for all installed packages.
920 """
921 eps = itertools.chain.from_iterable(
922 dist.entry_points for dist in _unique(distributions())
923 )
924 return EntryPoints(eps).select(**params)
927def files(distribution_name: str) -> Optional[List[PackagePath]]:
928 """Return a list of files for the named package.
930 :param distribution_name: The name of the distribution package to query.
931 :return: List of files composing the distribution.
932 """
933 return distribution(distribution_name).files
936def requires(distribution_name: str) -> Optional[List[str]]:
937 """
938 Return a list of requirements for the named package.
940 :return: An iterable of requirements, suitable for
941 packaging.requirement.Requirement.
942 """
943 return distribution(distribution_name).requires
946def packages_distributions() -> Mapping[str, List[str]]:
947 """
948 Return a mapping of top-level packages to their
949 distributions.
951 >>> import collections.abc
952 >>> pkgs = packages_distributions()
953 >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
954 True
955 """
956 pkg_to_dist = collections.defaultdict(list)
957 for dist in distributions():
958 for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
959 pkg_to_dist[pkg].append(dist.metadata['Name'])
960 return dict(pkg_to_dist)
963def _top_level_declared(dist):
964 return (dist.read_text('top_level.txt') or '').split()
967def _topmost(name: PackagePath) -> Optional[str]:
968 """
969 Return the top-most parent as long as there is a parent.
970 """
971 top, *rest = name.parts
972 return top if rest else None
975def _get_toplevel_name(name: PackagePath) -> str:
976 """
977 Infer a possibly importable module name from a name presumed on
978 sys.path.
980 >>> _get_toplevel_name(PackagePath('foo.py'))
981 'foo'
982 >>> _get_toplevel_name(PackagePath('foo'))
983 'foo'
984 >>> _get_toplevel_name(PackagePath('foo.pyc'))
985 'foo'
986 >>> _get_toplevel_name(PackagePath('foo/__init__.py'))
987 'foo'
988 >>> _get_toplevel_name(PackagePath('foo.pth'))
989 'foo.pth'
990 >>> _get_toplevel_name(PackagePath('foo.dist-info'))
991 'foo.dist-info'
992 """
993 return _topmost(name) or (
994 # python/typeshed#10328
995 inspect.getmodulename(name) # type: ignore
996 or str(name)
997 )
1000def _top_level_inferred(dist):
1001 opt_names = set(map(_get_toplevel_name, always_iterable(dist.files)))
1003 def importable_name(name):
1004 return '.' not in name
1006 return filter(importable_name, opt_names)