Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/importlib_metadata/__init__.py: 48%
383 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-22 06:15 +0000
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-22 06:15 +0000
1import os
2import re
3import abc
4import csv
5import sys
6import zipp
7import email
8import pathlib
9import operator
10import textwrap
11import warnings
12import functools
13import itertools
14import posixpath
15import contextlib
16import collections
17import inspect
19from . import _adapters, _meta, _py39compat
20from ._collections import FreezableDefaultDict, Pair
21from ._compat import (
22 NullFinder,
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 List, Mapping, Optional, 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):
57 return f"No package metadata was found for {self.name}"
59 @property
60 def name(self):
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):
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, value, group):
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):
216 match = self.pattern.match(self.value)
217 return match.group('module')
219 @property
220 def attr(self):
221 match = self.pattern.match(self.value)
222 return match.group('attr')
224 @property
225 def extras(self):
226 match = self.pattern.match(self.value)
227 return re.findall(r'\w+', match.group('extras') or '')
229 def _for(self, dist):
230 vars(self).update(dist=dist)
231 return self
233 def matches(self, **params):
234 """
235 EntryPoint matches the given parameters.
237 >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]')
238 >>> ep.matches(group='foo')
239 True
240 >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]')
241 True
242 >>> ep.matches(group='foo', name='other')
243 False
244 >>> ep.matches()
245 True
246 >>> ep.matches(extras=['extra1', 'extra2'])
247 True
248 >>> ep.matches(module='bing')
249 True
250 >>> ep.matches(attr='bong')
251 True
252 """
253 attrs = (getattr(self, param) for param in params)
254 return all(map(operator.eq, params.values(), attrs))
256 def _key(self):
257 return self.name, self.value, self.group
259 def __lt__(self, other):
260 return self._key() < other._key()
262 def __eq__(self, other):
263 return self._key() == other._key()
265 def __setattr__(self, name, value):
266 raise AttributeError("EntryPoint objects are immutable.")
268 def __repr__(self):
269 return (
270 f'EntryPoint(name={self.name!r}, value={self.value!r}, '
271 f'group={self.group!r})'
272 )
274 def __hash__(self):
275 return hash(self._key())
278class EntryPoints(tuple):
279 """
280 An immutable collection of selectable EntryPoint objects.
281 """
283 __slots__ = ()
285 def __getitem__(self, name): # -> EntryPoint:
286 """
287 Get the EntryPoint in self matching name.
288 """
289 try:
290 return next(iter(self.select(name=name)))
291 except StopIteration:
292 raise KeyError(name)
294 def select(self, **params):
295 """
296 Select entry points from self that match the
297 given parameters (typically group and/or name).
298 """
299 return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params))
301 @property
302 def names(self):
303 """
304 Return the set of all names of all entry points.
305 """
306 return {ep.name for ep in self}
308 @property
309 def groups(self):
310 """
311 Return the set of all groups of all entry points.
312 """
313 return {ep.group for ep in self}
315 @classmethod
316 def _from_text_for(cls, text, dist):
317 return cls(ep._for(dist) for ep in cls._from_text(text))
319 @staticmethod
320 def _from_text(text):
321 return (
322 EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
323 for item in Sectioned.section_pairs(text or '')
324 )
327class PackagePath(pathlib.PurePosixPath):
328 """A reference to a path in a package"""
330 def read_text(self, encoding='utf-8'):
331 with self.locate().open(encoding=encoding) as stream:
332 return stream.read()
334 def read_binary(self):
335 with self.locate().open('rb') as stream:
336 return stream.read()
338 def locate(self):
339 """Return a path-like object for this path"""
340 return self.dist.locate_file(self)
343class FileHash:
344 def __init__(self, spec):
345 self.mode, _, self.value = spec.partition('=')
347 def __repr__(self):
348 return f'<FileHash mode: {self.mode} value: {self.value}>'
351class DeprecatedNonAbstract:
352 def __new__(cls, *args, **kwargs):
353 all_names = {
354 name for subclass in inspect.getmro(cls) for name in vars(subclass)
355 }
356 abstract = {
357 name
358 for name in all_names
359 if getattr(getattr(cls, name), '__isabstractmethod__', False)
360 }
361 if abstract:
362 warnings.warn(
363 f"Unimplemented abstract methods {abstract}",
364 DeprecationWarning,
365 stacklevel=2,
366 )
367 return super().__new__(cls)
370class Distribution(DeprecatedNonAbstract):
371 """A Python distribution package."""
373 @abc.abstractmethod
374 def read_text(self, filename) -> Optional[str]:
375 """Attempt to load metadata file given by the name.
377 :param filename: The name of the file in the distribution info.
378 :return: The text if found, otherwise None.
379 """
381 @abc.abstractmethod
382 def locate_file(self, path):
383 """
384 Given a path to a file in this distribution, return a path
385 to it.
386 """
388 @classmethod
389 def from_name(cls, name: str):
390 """Return the Distribution for the given package name.
392 :param name: The name of the distribution package to search for.
393 :return: The Distribution instance (or subclass thereof) for the named
394 package, if found.
395 :raises PackageNotFoundError: When the named package's distribution
396 metadata cannot be found.
397 :raises ValueError: When an invalid value is supplied for name.
398 """
399 if not name:
400 raise ValueError("A distribution name is required.")
401 try:
402 return next(cls.discover(name=name))
403 except StopIteration:
404 raise PackageNotFoundError(name)
406 @classmethod
407 def discover(cls, **kwargs):
408 """Return an iterable of Distribution objects for all packages.
410 Pass a ``context`` or pass keyword arguments for constructing
411 a context.
413 :context: A ``DistributionFinder.Context`` object.
414 :return: Iterable of Distribution objects for all packages.
415 """
416 context = kwargs.pop('context', None)
417 if context and kwargs:
418 raise ValueError("cannot accept context and kwargs")
419 context = context or DistributionFinder.Context(**kwargs)
420 return itertools.chain.from_iterable(
421 resolver(context) for resolver in cls._discover_resolvers()
422 )
424 @staticmethod
425 def at(path):
426 """Return a Distribution for the indicated metadata path
428 :param path: a string or path-like object
429 :return: a concrete Distribution instance for the path
430 """
431 return PathDistribution(pathlib.Path(path))
433 @staticmethod
434 def _discover_resolvers():
435 """Search the meta_path for resolvers."""
436 declared = (
437 getattr(finder, 'find_distributions', None) for finder in sys.meta_path
438 )
439 return filter(None, declared)
441 @property
442 def metadata(self) -> _meta.PackageMetadata:
443 """Return the parsed metadata for this Distribution.
445 The returned object will have keys that name the various bits of
446 metadata. See PEP 566 for details.
447 """
448 opt_text = (
449 self.read_text('METADATA')
450 or self.read_text('PKG-INFO')
451 # This last clause is here to support old egg-info files. Its
452 # effect is to just end up using the PathDistribution's self._path
453 # (which points to the egg-info file) attribute unchanged.
454 or self.read_text('')
455 )
456 text = cast(str, opt_text)
457 return _adapters.Message(email.message_from_string(text))
459 @property
460 def name(self):
461 """Return the 'Name' metadata for the distribution package."""
462 return self.metadata['Name']
464 @property
465 def _normalized_name(self):
466 """Return a normalized version of the name."""
467 return Prepared.normalize(self.name)
469 @property
470 def version(self):
471 """Return the 'Version' metadata for the distribution package."""
472 return self.metadata['Version']
474 @property
475 def entry_points(self):
476 return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
478 @property
479 def files(self):
480 """Files in this distribution.
482 :return: List of PackagePath for this distribution or None
484 Result is `None` if the metadata file that enumerates files
485 (i.e. RECORD for dist-info, or installed-files.txt or
486 SOURCES.txt for egg-info) is missing.
487 Result may be empty if the metadata exists but is empty.
488 """
490 def make_file(name, hash=None, size_str=None):
491 result = PackagePath(name)
492 result.hash = FileHash(hash) if hash else None
493 result.size = int(size_str) if size_str else None
494 result.dist = self
495 return result
497 @pass_none
498 def make_files(lines):
499 return starmap(make_file, csv.reader(lines))
501 @pass_none
502 def skip_missing_files(package_paths):
503 return list(filter(lambda path: path.locate().exists(), package_paths))
505 return skip_missing_files(
506 make_files(
507 self._read_files_distinfo()
508 or self._read_files_egginfo_installed()
509 or self._read_files_egginfo_sources()
510 )
511 )
513 def _read_files_distinfo(self):
514 """
515 Read the lines of RECORD
516 """
517 text = self.read_text('RECORD')
518 return text and text.splitlines()
520 def _read_files_egginfo_installed(self):
521 """
522 Read installed-files.txt and return lines in a similar
523 CSV-parsable format as RECORD: each file must be placed
524 relative to the site-packages directory and must also be
525 quoted (since file names can contain literal commas).
527 This file is written when the package is installed by pip,
528 but it might not be written for other installation methods.
529 Assume the file is accurate if it exists.
530 """
531 text = self.read_text('installed-files.txt')
532 # Prepend the .egg-info/ subdir to the lines in this file.
533 # But this subdir is only available from PathDistribution's
534 # self._path.
535 subdir = getattr(self, '_path', None)
536 if not text or not subdir:
537 return
539 paths = (
540 (subdir / name)
541 .resolve()
542 .relative_to(self.locate_file('').resolve())
543 .as_posix()
544 for name in text.splitlines()
545 )
546 return map('"{}"'.format, paths)
548 def _read_files_egginfo_sources(self):
549 """
550 Read SOURCES.txt and return lines in a similar CSV-parsable
551 format as RECORD: each file name must be quoted (since it
552 might contain literal commas).
554 Note that SOURCES.txt is not a reliable source for what
555 files are installed by a package. This file is generated
556 for a source archive, and the files that are present
557 there (e.g. setup.py) may not correctly reflect the files
558 that are present after the package has been installed.
559 """
560 text = self.read_text('SOURCES.txt')
561 return text and map('"{}"'.format, text.splitlines())
563 @property
564 def requires(self):
565 """Generated requirements specified for this Distribution"""
566 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
567 return reqs and list(reqs)
569 def _read_dist_info_reqs(self):
570 return self.metadata.get_all('Requires-Dist')
572 def _read_egg_info_reqs(self):
573 source = self.read_text('requires.txt')
574 return pass_none(self._deps_from_requires_text)(source)
576 @classmethod
577 def _deps_from_requires_text(cls, source):
578 return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
580 @staticmethod
581 def _convert_egg_info_reqs_to_simple_reqs(sections):
582 """
583 Historically, setuptools would solicit and store 'extra'
584 requirements, including those with environment markers,
585 in separate sections. More modern tools expect each
586 dependency to be defined separately, with any relevant
587 extras and environment markers attached directly to that
588 requirement. This method converts the former to the
589 latter. See _test_deps_from_requires_text for an example.
590 """
592 def make_condition(name):
593 return name and f'extra == "{name}"'
595 def quoted_marker(section):
596 section = section or ''
597 extra, sep, markers = section.partition(':')
598 if extra and markers:
599 markers = f'({markers})'
600 conditions = list(filter(None, [markers, make_condition(extra)]))
601 return '; ' + ' and '.join(conditions) if conditions else ''
603 def url_req_space(req):
604 """
605 PEP 508 requires a space between the url_spec and the quoted_marker.
606 Ref python/importlib_metadata#357.
607 """
608 # '@' is uniquely indicative of a url_req.
609 return ' ' * ('@' in req)
611 for section in sections:
612 space = url_req_space(section.value)
613 yield section.value + space + quoted_marker(section.name)
616class DistributionFinder(MetaPathFinder):
617 """
618 A MetaPathFinder capable of discovering installed distributions.
619 """
621 class Context:
622 """
623 Keyword arguments presented by the caller to
624 ``distributions()`` or ``Distribution.discover()``
625 to narrow the scope of a search for distributions
626 in all DistributionFinders.
628 Each DistributionFinder may expect any parameters
629 and should attempt to honor the canonical
630 parameters defined below when appropriate.
631 """
633 name = None
634 """
635 Specific name for which a distribution finder should match.
636 A name of ``None`` matches all distributions.
637 """
639 def __init__(self, **kwargs):
640 vars(self).update(kwargs)
642 @property
643 def path(self):
644 """
645 The sequence of directory path that a distribution finder
646 should search.
648 Typically refers to Python installed package paths such as
649 "site-packages" directories and defaults to ``sys.path``.
650 """
651 return vars(self).get('path', sys.path)
653 @abc.abstractmethod
654 def find_distributions(self, context=Context()):
655 """
656 Find distributions.
658 Return an iterable of all Distribution instances capable of
659 loading the metadata for packages matching the ``context``,
660 a DistributionFinder.Context instance.
661 """
664class FastPath:
665 """
666 Micro-optimized class for searching a path for
667 children.
669 >>> FastPath('').children()
670 ['...']
671 """
673 @functools.lru_cache() # type: ignore
674 def __new__(cls, root):
675 return super().__new__(cls)
677 def __init__(self, root):
678 self.root = root
680 def joinpath(self, child):
681 return pathlib.Path(self.root, child)
683 def children(self):
684 with suppress(Exception):
685 return os.listdir(self.root or '.')
686 with suppress(Exception):
687 return self.zip_children()
688 return []
690 def zip_children(self):
691 zip_path = zipp.Path(self.root)
692 names = zip_path.root.namelist()
693 self.joinpath = zip_path.joinpath
695 return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
697 def search(self, name):
698 return self.lookup(self.mtime).search(name)
700 @property
701 def mtime(self):
702 with suppress(OSError):
703 return os.stat(self.root).st_mtime
704 self.lookup.cache_clear()
706 @method_cache
707 def lookup(self, mtime):
708 return Lookup(self)
711class Lookup:
712 def __init__(self, path: FastPath):
713 base = os.path.basename(path.root).lower()
714 base_is_egg = base.endswith(".egg")
715 self.infos = FreezableDefaultDict(list)
716 self.eggs = FreezableDefaultDict(list)
718 for child in path.children():
719 low = child.lower()
720 if low.endswith((".dist-info", ".egg-info")):
721 # rpartition is faster than splitext and suitable for this purpose.
722 name = low.rpartition(".")[0].partition("-")[0]
723 normalized = Prepared.normalize(name)
724 self.infos[normalized].append(path.joinpath(child))
725 elif base_is_egg and low == "egg-info":
726 name = base.rpartition(".")[0].partition("-")[0]
727 legacy_normalized = Prepared.legacy_normalize(name)
728 self.eggs[legacy_normalized].append(path.joinpath(child))
730 self.infos.freeze()
731 self.eggs.freeze()
733 def search(self, prepared):
734 infos = (
735 self.infos[prepared.normalized]
736 if prepared
737 else itertools.chain.from_iterable(self.infos.values())
738 )
739 eggs = (
740 self.eggs[prepared.legacy_normalized]
741 if prepared
742 else itertools.chain.from_iterable(self.eggs.values())
743 )
744 return itertools.chain(infos, eggs)
747class Prepared:
748 """
749 A prepared search for metadata on a possibly-named package.
750 """
752 normalized = None
753 legacy_normalized = None
755 def __init__(self, name):
756 self.name = name
757 if name is None:
758 return
759 self.normalized = self.normalize(name)
760 self.legacy_normalized = self.legacy_normalize(name)
762 @staticmethod
763 def normalize(name):
764 """
765 PEP 503 normalization plus dashes as underscores.
766 """
767 return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
769 @staticmethod
770 def legacy_normalize(name):
771 """
772 Normalize the package name as found in the convention in
773 older packaging tools versions and specs.
774 """
775 return name.lower().replace('-', '_')
777 def __bool__(self):
778 return bool(self.name)
781@install
782class MetadataPathFinder(NullFinder, DistributionFinder):
783 """A degenerate finder for distribution packages on the file system.
785 This finder supplies only a find_distributions() method for versions
786 of Python that do not have a PathFinder find_distributions().
787 """
789 def find_distributions(self, context=DistributionFinder.Context()):
790 """
791 Find distributions.
793 Return an iterable of all Distribution instances capable of
794 loading the metadata for packages matching ``context.name``
795 (or all names if ``None`` indicated) along the paths in the list
796 of directories ``context.path``.
797 """
798 found = self._search_paths(context.name, context.path)
799 return map(PathDistribution, found)
801 @classmethod
802 def _search_paths(cls, name, paths):
803 """Find metadata directories in paths heuristically."""
804 prepared = Prepared(name)
805 return itertools.chain.from_iterable(
806 path.search(prepared) for path in map(FastPath, paths)
807 )
809 def invalidate_caches(cls):
810 FastPath.__new__.cache_clear()
813class PathDistribution(Distribution):
814 def __init__(self, path: SimplePath):
815 """Construct a distribution.
817 :param path: SimplePath indicating the metadata directory.
818 """
819 self._path = path
821 def read_text(self, filename):
822 with suppress(
823 FileNotFoundError,
824 IsADirectoryError,
825 KeyError,
826 NotADirectoryError,
827 PermissionError,
828 ):
829 return self._path.joinpath(filename).read_text(encoding='utf-8')
831 read_text.__doc__ = Distribution.read_text.__doc__
833 def locate_file(self, path):
834 return self._path.parent / path
836 @property
837 def _normalized_name(self):
838 """
839 Performance optimization: where possible, resolve the
840 normalized name from the file system path.
841 """
842 stem = os.path.basename(str(self._path))
843 return (
844 pass_none(Prepared.normalize)(self._name_from_stem(stem))
845 or super()._normalized_name
846 )
848 @staticmethod
849 def _name_from_stem(stem):
850 """
851 >>> PathDistribution._name_from_stem('foo-3.0.egg-info')
852 'foo'
853 >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
854 'CherryPy'
855 >>> PathDistribution._name_from_stem('face.egg-info')
856 'face'
857 >>> PathDistribution._name_from_stem('foo.bar')
858 """
859 filename, ext = os.path.splitext(stem)
860 if ext not in ('.dist-info', '.egg-info'):
861 return
862 name, sep, rest = filename.partition('-')
863 return name
866def distribution(distribution_name):
867 """Get the ``Distribution`` instance for the named package.
869 :param distribution_name: The name of the distribution package as a string.
870 :return: A ``Distribution`` instance (or subclass thereof).
871 """
872 return Distribution.from_name(distribution_name)
875def distributions(**kwargs):
876 """Get all ``Distribution`` instances in the current environment.
878 :return: An iterable of ``Distribution`` instances.
879 """
880 return Distribution.discover(**kwargs)
883def metadata(distribution_name) -> _meta.PackageMetadata:
884 """Get the metadata for the named package.
886 :param distribution_name: The name of the distribution package to query.
887 :return: A PackageMetadata containing the parsed metadata.
888 """
889 return Distribution.from_name(distribution_name).metadata
892def version(distribution_name):
893 """Get the version string for the named package.
895 :param distribution_name: The name of the distribution package to query.
896 :return: The version string for the package as defined in the package's
897 "Version" metadata key.
898 """
899 return distribution(distribution_name).version
902_unique = functools.partial(
903 unique_everseen,
904 key=_py39compat.normalized_name,
905)
906"""
907Wrapper for ``distributions`` to return unique distributions by name.
908"""
911def entry_points(**params) -> EntryPoints:
912 """Return EntryPoint objects for all installed packages.
914 Pass selection parameters (group or name) to filter the
915 result to entry points matching those properties (see
916 EntryPoints.select()).
918 :return: EntryPoints for all installed packages.
919 """
920 eps = itertools.chain.from_iterable(
921 dist.entry_points for dist in _unique(distributions())
922 )
923 return EntryPoints(eps).select(**params)
926def files(distribution_name):
927 """Return a list of files for the named package.
929 :param distribution_name: The name of the distribution package to query.
930 :return: List of files composing the distribution.
931 """
932 return distribution(distribution_name).files
935def requires(distribution_name):
936 """
937 Return a list of requirements for the named package.
939 :return: An iterator of requirements, suitable for
940 packaging.requirement.Requirement.
941 """
942 return distribution(distribution_name).requires
945def packages_distributions() -> Mapping[str, List[str]]:
946 """
947 Return a mapping of top-level packages to their
948 distributions.
950 >>> import collections.abc
951 >>> pkgs = packages_distributions()
952 >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
953 True
954 """
955 pkg_to_dist = collections.defaultdict(list)
956 for dist in distributions():
957 for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
958 pkg_to_dist[pkg].append(dist.metadata['Name'])
959 return dict(pkg_to_dist)
962def _top_level_declared(dist):
963 return (dist.read_text('top_level.txt') or '').split()
966def _top_level_inferred(dist):
967 opt_names = {
968 f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f)
969 for f in always_iterable(dist.files)
970 }
972 @pass_none
973 def importable_name(name):
974 return '.' not in name
976 return filter(importable_name, opt_names)