Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/importlib_metadata/__init__.py: 48%
376 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-10 06:20 +0000
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-10 06:20 +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
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 Distribution(metaclass=abc.ABCMeta):
352 """A Python distribution package."""
354 @abc.abstractmethod
355 def read_text(self, filename):
356 """Attempt to load metadata file given by the name.
358 :param filename: The name of the file in the distribution info.
359 :return: The text if found, otherwise None.
360 """
362 @abc.abstractmethod
363 def locate_file(self, path):
364 """
365 Given a path to a file in this distribution, return a path
366 to it.
367 """
369 @classmethod
370 def from_name(cls, name: str):
371 """Return the Distribution for the given package name.
373 :param name: The name of the distribution package to search for.
374 :return: The Distribution instance (or subclass thereof) for the named
375 package, if found.
376 :raises PackageNotFoundError: When the named package's distribution
377 metadata cannot be found.
378 :raises ValueError: When an invalid value is supplied for name.
379 """
380 if not name:
381 raise ValueError("A distribution name is required.")
382 try:
383 return next(cls.discover(name=name))
384 except StopIteration:
385 raise PackageNotFoundError(name)
387 @classmethod
388 def discover(cls, **kwargs):
389 """Return an iterable of Distribution objects for all packages.
391 Pass a ``context`` or pass keyword arguments for constructing
392 a context.
394 :context: A ``DistributionFinder.Context`` object.
395 :return: Iterable of Distribution objects for all packages.
396 """
397 context = kwargs.pop('context', None)
398 if context and kwargs:
399 raise ValueError("cannot accept context and kwargs")
400 context = context or DistributionFinder.Context(**kwargs)
401 return itertools.chain.from_iterable(
402 resolver(context) for resolver in cls._discover_resolvers()
403 )
405 @staticmethod
406 def at(path):
407 """Return a Distribution for the indicated metadata path
409 :param path: a string or path-like object
410 :return: a concrete Distribution instance for the path
411 """
412 return PathDistribution(pathlib.Path(path))
414 @staticmethod
415 def _discover_resolvers():
416 """Search the meta_path for resolvers."""
417 declared = (
418 getattr(finder, 'find_distributions', None) for finder in sys.meta_path
419 )
420 return filter(None, declared)
422 @property
423 def metadata(self) -> _meta.PackageMetadata:
424 """Return the parsed metadata for this Distribution.
426 The returned object will have keys that name the various bits of
427 metadata. See PEP 566 for details.
428 """
429 text = (
430 self.read_text('METADATA')
431 or self.read_text('PKG-INFO')
432 # This last clause is here to support old egg-info files. Its
433 # effect is to just end up using the PathDistribution's self._path
434 # (which points to the egg-info file) attribute unchanged.
435 or self.read_text('')
436 )
437 return _adapters.Message(email.message_from_string(text))
439 @property
440 def name(self):
441 """Return the 'Name' metadata for the distribution package."""
442 return self.metadata['Name']
444 @property
445 def _normalized_name(self):
446 """Return a normalized version of the name."""
447 return Prepared.normalize(self.name)
449 @property
450 def version(self):
451 """Return the 'Version' metadata for the distribution package."""
452 return self.metadata['Version']
454 @property
455 def entry_points(self):
456 return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
458 @property
459 def files(self):
460 """Files in this distribution.
462 :return: List of PackagePath for this distribution or None
464 Result is `None` if the metadata file that enumerates files
465 (i.e. RECORD for dist-info, or installed-files.txt or
466 SOURCES.txt for egg-info) is missing.
467 Result may be empty if the metadata exists but is empty.
468 """
470 def make_file(name, hash=None, size_str=None):
471 result = PackagePath(name)
472 result.hash = FileHash(hash) if hash else None
473 result.size = int(size_str) if size_str else None
474 result.dist = self
475 return result
477 @pass_none
478 def make_files(lines):
479 return starmap(make_file, csv.reader(lines))
481 @pass_none
482 def skip_missing_files(package_paths):
483 return list(filter(lambda path: path.locate().exists(), package_paths))
485 return skip_missing_files(
486 make_files(
487 self._read_files_distinfo()
488 or self._read_files_egginfo_installed()
489 or self._read_files_egginfo_sources()
490 )
491 )
493 def _read_files_distinfo(self):
494 """
495 Read the lines of RECORD
496 """
497 text = self.read_text('RECORD')
498 return text and text.splitlines()
500 def _read_files_egginfo_installed(self):
501 """
502 Read installed-files.txt and return lines in a similar
503 CSV-parsable format as RECORD: each file must be placed
504 relative to the site-packages directory, and must also be
505 quoted (since file names can contain literal commas).
507 This file is written when the package is installed by pip,
508 but it might not be written for other installation methods.
509 Hence, even if we can assume that this file is accurate
510 when it exists, we cannot assume that it always exists.
511 """
512 text = self.read_text('installed-files.txt')
513 # We need to prepend the .egg-info/ subdir to the lines in this file.
514 # But this subdir is only available in the PathDistribution's self._path
515 # which is not easily accessible from this base class...
516 subdir = getattr(self, '_path', None)
517 if not text or not subdir:
518 return
519 with contextlib.suppress(Exception):
520 ret = [
521 str((subdir / line).resolve().relative_to(self.locate_file('')))
522 for line in text.splitlines()
523 ]
524 return map('"{}"'.format, ret)
526 def _read_files_egginfo_sources(self):
527 """
528 Read SOURCES.txt and return lines in a similar CSV-parsable
529 format as RECORD: each file name must be quoted (since it
530 might contain literal commas).
532 Note that SOURCES.txt is not a reliable source for what
533 files are installed by a package. This file is generated
534 for a source archive, and the files that are present
535 there (e.g. setup.py) may not correctly reflect the files
536 that are present after the package has been installed.
537 """
538 text = self.read_text('SOURCES.txt')
539 return text and map('"{}"'.format, text.splitlines())
541 @property
542 def requires(self):
543 """Generated requirements specified for this Distribution"""
544 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
545 return reqs and list(reqs)
547 def _read_dist_info_reqs(self):
548 return self.metadata.get_all('Requires-Dist')
550 def _read_egg_info_reqs(self):
551 source = self.read_text('requires.txt')
552 return pass_none(self._deps_from_requires_text)(source)
554 @classmethod
555 def _deps_from_requires_text(cls, source):
556 return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
558 @staticmethod
559 def _convert_egg_info_reqs_to_simple_reqs(sections):
560 """
561 Historically, setuptools would solicit and store 'extra'
562 requirements, including those with environment markers,
563 in separate sections. More modern tools expect each
564 dependency to be defined separately, with any relevant
565 extras and environment markers attached directly to that
566 requirement. This method converts the former to the
567 latter. See _test_deps_from_requires_text for an example.
568 """
570 def make_condition(name):
571 return name and f'extra == "{name}"'
573 def quoted_marker(section):
574 section = section or ''
575 extra, sep, markers = section.partition(':')
576 if extra and markers:
577 markers = f'({markers})'
578 conditions = list(filter(None, [markers, make_condition(extra)]))
579 return '; ' + ' and '.join(conditions) if conditions else ''
581 def url_req_space(req):
582 """
583 PEP 508 requires a space between the url_spec and the quoted_marker.
584 Ref python/importlib_metadata#357.
585 """
586 # '@' is uniquely indicative of a url_req.
587 return ' ' * ('@' in req)
589 for section in sections:
590 space = url_req_space(section.value)
591 yield section.value + space + quoted_marker(section.name)
594class DistributionFinder(MetaPathFinder):
595 """
596 A MetaPathFinder capable of discovering installed distributions.
597 """
599 class Context:
600 """
601 Keyword arguments presented by the caller to
602 ``distributions()`` or ``Distribution.discover()``
603 to narrow the scope of a search for distributions
604 in all DistributionFinders.
606 Each DistributionFinder may expect any parameters
607 and should attempt to honor the canonical
608 parameters defined below when appropriate.
609 """
611 name = None
612 """
613 Specific name for which a distribution finder should match.
614 A name of ``None`` matches all distributions.
615 """
617 def __init__(self, **kwargs):
618 vars(self).update(kwargs)
620 @property
621 def path(self):
622 """
623 The sequence of directory path that a distribution finder
624 should search.
626 Typically refers to Python installed package paths such as
627 "site-packages" directories and defaults to ``sys.path``.
628 """
629 return vars(self).get('path', sys.path)
631 @abc.abstractmethod
632 def find_distributions(self, context=Context()):
633 """
634 Find distributions.
636 Return an iterable of all Distribution instances capable of
637 loading the metadata for packages matching the ``context``,
638 a DistributionFinder.Context instance.
639 """
642class FastPath:
643 """
644 Micro-optimized class for searching a path for
645 children.
647 >>> FastPath('').children()
648 ['...']
649 """
651 @functools.lru_cache() # type: ignore
652 def __new__(cls, root):
653 return super().__new__(cls)
655 def __init__(self, root):
656 self.root = root
658 def joinpath(self, child):
659 return pathlib.Path(self.root, child)
661 def children(self):
662 with suppress(Exception):
663 return os.listdir(self.root or '.')
664 with suppress(Exception):
665 return self.zip_children()
666 return []
668 def zip_children(self):
669 zip_path = zipp.Path(self.root)
670 names = zip_path.root.namelist()
671 self.joinpath = zip_path.joinpath
673 return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
675 def search(self, name):
676 return self.lookup(self.mtime).search(name)
678 @property
679 def mtime(self):
680 with suppress(OSError):
681 return os.stat(self.root).st_mtime
682 self.lookup.cache_clear()
684 @method_cache
685 def lookup(self, mtime):
686 return Lookup(self)
689class Lookup:
690 def __init__(self, path: FastPath):
691 base = os.path.basename(path.root).lower()
692 base_is_egg = base.endswith(".egg")
693 self.infos = FreezableDefaultDict(list)
694 self.eggs = FreezableDefaultDict(list)
696 for child in path.children():
697 low = child.lower()
698 if low.endswith((".dist-info", ".egg-info")):
699 # rpartition is faster than splitext and suitable for this purpose.
700 name = low.rpartition(".")[0].partition("-")[0]
701 normalized = Prepared.normalize(name)
702 self.infos[normalized].append(path.joinpath(child))
703 elif base_is_egg and low == "egg-info":
704 name = base.rpartition(".")[0].partition("-")[0]
705 legacy_normalized = Prepared.legacy_normalize(name)
706 self.eggs[legacy_normalized].append(path.joinpath(child))
708 self.infos.freeze()
709 self.eggs.freeze()
711 def search(self, prepared):
712 infos = (
713 self.infos[prepared.normalized]
714 if prepared
715 else itertools.chain.from_iterable(self.infos.values())
716 )
717 eggs = (
718 self.eggs[prepared.legacy_normalized]
719 if prepared
720 else itertools.chain.from_iterable(self.eggs.values())
721 )
722 return itertools.chain(infos, eggs)
725class Prepared:
726 """
727 A prepared search for metadata on a possibly-named package.
728 """
730 normalized = None
731 legacy_normalized = None
733 def __init__(self, name):
734 self.name = name
735 if name is None:
736 return
737 self.normalized = self.normalize(name)
738 self.legacy_normalized = self.legacy_normalize(name)
740 @staticmethod
741 def normalize(name):
742 """
743 PEP 503 normalization plus dashes as underscores.
744 """
745 return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
747 @staticmethod
748 def legacy_normalize(name):
749 """
750 Normalize the package name as found in the convention in
751 older packaging tools versions and specs.
752 """
753 return name.lower().replace('-', '_')
755 def __bool__(self):
756 return bool(self.name)
759@install
760class MetadataPathFinder(NullFinder, DistributionFinder):
761 """A degenerate finder for distribution packages on the file system.
763 This finder supplies only a find_distributions() method for versions
764 of Python that do not have a PathFinder find_distributions().
765 """
767 def find_distributions(self, context=DistributionFinder.Context()):
768 """
769 Find distributions.
771 Return an iterable of all Distribution instances capable of
772 loading the metadata for packages matching ``context.name``
773 (or all names if ``None`` indicated) along the paths in the list
774 of directories ``context.path``.
775 """
776 found = self._search_paths(context.name, context.path)
777 return map(PathDistribution, found)
779 @classmethod
780 def _search_paths(cls, name, paths):
781 """Find metadata directories in paths heuristically."""
782 prepared = Prepared(name)
783 return itertools.chain.from_iterable(
784 path.search(prepared) for path in map(FastPath, paths)
785 )
787 def invalidate_caches(cls):
788 FastPath.__new__.cache_clear()
791class PathDistribution(Distribution):
792 def __init__(self, path: SimplePath):
793 """Construct a distribution.
795 :param path: SimplePath indicating the metadata directory.
796 """
797 self._path = path
799 def read_text(self, filename):
800 with suppress(
801 FileNotFoundError,
802 IsADirectoryError,
803 KeyError,
804 NotADirectoryError,
805 PermissionError,
806 ):
807 return self._path.joinpath(filename).read_text(encoding='utf-8')
809 read_text.__doc__ = Distribution.read_text.__doc__
811 def locate_file(self, path):
812 return self._path.parent / path
814 @property
815 def _normalized_name(self):
816 """
817 Performance optimization: where possible, resolve the
818 normalized name from the file system path.
819 """
820 stem = os.path.basename(str(self._path))
821 return (
822 pass_none(Prepared.normalize)(self._name_from_stem(stem))
823 or super()._normalized_name
824 )
826 @staticmethod
827 def _name_from_stem(stem):
828 """
829 >>> PathDistribution._name_from_stem('foo-3.0.egg-info')
830 'foo'
831 >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
832 'CherryPy'
833 >>> PathDistribution._name_from_stem('face.egg-info')
834 'face'
835 >>> PathDistribution._name_from_stem('foo.bar')
836 """
837 filename, ext = os.path.splitext(stem)
838 if ext not in ('.dist-info', '.egg-info'):
839 return
840 name, sep, rest = filename.partition('-')
841 return name
844def distribution(distribution_name):
845 """Get the ``Distribution`` instance for the named package.
847 :param distribution_name: The name of the distribution package as a string.
848 :return: A ``Distribution`` instance (or subclass thereof).
849 """
850 return Distribution.from_name(distribution_name)
853def distributions(**kwargs):
854 """Get all ``Distribution`` instances in the current environment.
856 :return: An iterable of ``Distribution`` instances.
857 """
858 return Distribution.discover(**kwargs)
861def metadata(distribution_name) -> _meta.PackageMetadata:
862 """Get the metadata for the named package.
864 :param distribution_name: The name of the distribution package to query.
865 :return: A PackageMetadata containing the parsed metadata.
866 """
867 return Distribution.from_name(distribution_name).metadata
870def version(distribution_name):
871 """Get the version string for the named package.
873 :param distribution_name: The name of the distribution package to query.
874 :return: The version string for the package as defined in the package's
875 "Version" metadata key.
876 """
877 return distribution(distribution_name).version
880_unique = functools.partial(
881 unique_everseen,
882 key=_py39compat.normalized_name,
883)
884"""
885Wrapper for ``distributions`` to return unique distributions by name.
886"""
889def entry_points(**params) -> EntryPoints:
890 """Return EntryPoint objects for all installed packages.
892 Pass selection parameters (group or name) to filter the
893 result to entry points matching those properties (see
894 EntryPoints.select()).
896 :return: EntryPoints for all installed packages.
897 """
898 eps = itertools.chain.from_iterable(
899 dist.entry_points for dist in _unique(distributions())
900 )
901 return EntryPoints(eps).select(**params)
904def files(distribution_name):
905 """Return a list of files for the named package.
907 :param distribution_name: The name of the distribution package to query.
908 :return: List of files composing the distribution.
909 """
910 return distribution(distribution_name).files
913def requires(distribution_name):
914 """
915 Return a list of requirements for the named package.
917 :return: An iterator of requirements, suitable for
918 packaging.requirement.Requirement.
919 """
920 return distribution(distribution_name).requires
923def packages_distributions() -> Mapping[str, List[str]]:
924 """
925 Return a mapping of top-level packages to their
926 distributions.
928 >>> import collections.abc
929 >>> pkgs = packages_distributions()
930 >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
931 True
932 """
933 pkg_to_dist = collections.defaultdict(list)
934 for dist in distributions():
935 for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
936 pkg_to_dist[pkg].append(dist.metadata['Name'])
937 return dict(pkg_to_dist)
940def _top_level_declared(dist):
941 return (dist.read_text('top_level.txt') or '').split()
944def _top_level_inferred(dist):
945 opt_names = {
946 f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f)
947 for f in always_iterable(dist.files)
948 }
950 @pass_none
951 def importable_name(name):
952 return '.' not in name
954 return filter(importable_name, opt_names)