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
19
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
30
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
36
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]
51
52
53class PackageNotFoundError(ModuleNotFoundError):
54 """The package was not found."""
55
56 def __str__(self) -> str:
57 return f"No package metadata was found for {self.name}"
58
59 @property
60 def name(self) -> str: # type: ignore[override]
61 (name,) = self.args
62 return name
63
64
65class Sectioned:
66 """
67 A simple entry point config parser for performance
68
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')
75
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 """
93
94 _sample = textwrap.dedent(
95 """
96 [sec1]
97 # comments ignored
98 a = 1
99 b = 2
100
101 [sec2]
102 a = 2
103 """
104 ).lstrip()
105
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 )
113
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)
124
125 @staticmethod
126 def valid(line: str):
127 return line and not line.startswith('#')
128
129
130class EntryPoint:
131 """An entry point as defined by Python packaging conventions.
132
133 See `the packaging docs on entry points
134 <https://packaging.python.org/specifications/entry-points/>`_
135 for more information.
136
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 """
146
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:
155
156 - module
157 - package.module
158 - package.module:attribute
159 - package.module:object.attribute
160 - package.module:attr [extra1, extra2]
161
162 Other combinations are possible as well.
163
164 The expression is lenient about whitespace around the ':',
165 following the attr, and following any extras.
166 """
167
168 name: str
169 value: str
170 group: str
171
172 dist: Optional['Distribution'] = None
173
174 def __init__(self, name: str, value: str, group: str) -> None:
175 vars(self).update(name=name, value=value, group=group)
176
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)
186
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')
192
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')
198
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 '')
204
205 def _for(self, dist):
206 vars(self).update(dist=dist)
207 return self
208
209 def matches(self, **params):
210 """
211 EntryPoint matches the given parameters.
212
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))
231
232 def _key(self):
233 return self.name, self.value, self.group
234
235 def __lt__(self, other):
236 return self._key() < other._key()
237
238 def __eq__(self, other):
239 return self._key() == other._key()
240
241 def __setattr__(self, name, value):
242 raise AttributeError("EntryPoint objects are immutable.")
243
244 def __repr__(self):
245 return (
246 f'EntryPoint(name={self.name!r}, value={self.value!r}, '
247 f'group={self.group!r})'
248 )
249
250 def __hash__(self) -> int:
251 return hash(self._key())
252
253
254class EntryPoints(tuple):
255 """
256 An immutable collection of selectable EntryPoint objects.
257 """
258
259 __slots__ = ()
260
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)
269
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))
276
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))
283
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}
290
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}
297
298 @classmethod
299 def _from_text_for(cls, text, dist):
300 return cls(ep._for(dist) for ep in cls._from_text(text))
301
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 )
308
309
310class PackagePath(pathlib.PurePosixPath):
311 """A reference to a path in a package"""
312
313 hash: Optional["FileHash"]
314 size: int
315 dist: "Distribution"
316
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()
320
321 def read_binary(self) -> bytes:
322 with self.locate().open('rb') as stream:
323 return stream.read()
324
325 def locate(self) -> pathlib.Path:
326 """Return a path-like object for this path"""
327 return self.dist.locate_file(self)
328
329
330class FileHash:
331 def __init__(self, spec: str) -> None:
332 self.mode, _, self.value = spec.partition('=')
333
334 def __repr__(self) -> str:
335 return f'<FileHash mode: {self.mode} value: {self.value}>'
336
337
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)
355
356
357class Distribution(DeprecatedNonAbstract):
358 """A Python distribution package."""
359
360 @abc.abstractmethod
361 def read_text(self, filename) -> Optional[str]:
362 """Attempt to load metadata file given by the name.
363
364 :param filename: The name of the file in the distribution info.
365 :return: The text if found, otherwise None.
366 """
367
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 """
374
375 @classmethod
376 def from_name(cls, name: str) -> "Distribution":
377 """Return the Distribution for the given package name.
378
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)
392
393 @classmethod
394 def discover(cls, **kwargs) -> Iterable["Distribution"]:
395 """Return an iterable of Distribution objects for all packages.
396
397 Pass a ``context`` or pass keyword arguments for constructing
398 a context.
399
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 )
410
411 @staticmethod
412 def at(path: StrPath) -> "Distribution":
413 """Return a Distribution for the indicated metadata path
414
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))
419
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)
427
428 @property
429 def metadata(self) -> _meta.PackageMetadata:
430 """Return the parsed metadata for this Distribution.
431
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))
445
446 @property
447 def name(self) -> str:
448 """Return the 'Name' metadata for the distribution package."""
449 return self.metadata['Name']
450
451 @property
452 def _normalized_name(self):
453 """Return a normalized version of the name."""
454 return Prepared.normalize(self.name)
455
456 @property
457 def version(self) -> str:
458 """Return the 'Version' metadata for the distribution package."""
459 return self.metadata['Version']
460
461 @property
462 def entry_points(self) -> EntryPoints:
463 return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
464
465 @property
466 def files(self) -> Optional[List[PackagePath]]:
467 """Files in this distribution.
468
469 :return: List of PackagePath for this distribution or None
470
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 """
476
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
483
484 @pass_none
485 def make_files(lines):
486 return starmap(make_file, csv.reader(lines))
487
488 @pass_none
489 def skip_missing_files(package_paths):
490 return list(filter(lambda path: path.locate().exists(), package_paths))
491
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 )
499
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()
506
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).
513
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
525
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)
534
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).
540
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())
549
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)
555
556 def _read_dist_info_reqs(self):
557 return self.metadata.get_all('Requires-Dist')
558
559 def _read_egg_info_reqs(self):
560 source = self.read_text('requires.txt')
561 return pass_none(self._deps_from_requires_text)(source)
562
563 @classmethod
564 def _deps_from_requires_text(cls, source):
565 return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
566
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 """
578
579 def make_condition(name):
580 return name and f'extra == "{name}"'
581
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 ''
589
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)
597
598 for section in sections:
599 space = url_req_space(section.value)
600 yield section.value + space + quoted_marker(section.name)
601
602 @property
603 def origin(self):
604 return self._load_json('direct_url.json')
605
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 )
611
612
613class DistributionFinder(MetaPathFinder):
614 """
615 A MetaPathFinder capable of discovering installed distributions.
616 """
617
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.
624
625 Each DistributionFinder may expect any parameters
626 and should attempt to honor the canonical
627 parameters defined below when appropriate.
628 """
629
630 name = None
631 """
632 Specific name for which a distribution finder should match.
633 A name of ``None`` matches all distributions.
634 """
635
636 def __init__(self, **kwargs):
637 vars(self).update(kwargs)
638
639 @property
640 def path(self) -> List[str]:
641 """
642 The sequence of directory path that a distribution finder
643 should search.
644
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)
649
650 @abc.abstractmethod
651 def find_distributions(self, context=Context()) -> Iterable[Distribution]:
652 """
653 Find distributions.
654
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 """
659
660
661class FastPath:
662 """
663 Micro-optimized class for searching a path for
664 children.
665
666 >>> FastPath('').children()
667 ['...']
668 """
669
670 @functools.lru_cache() # type: ignore
671 def __new__(cls, root):
672 return super().__new__(cls)
673
674 def __init__(self, root):
675 self.root = root
676
677 def joinpath(self, child):
678 return pathlib.Path(self.root, child)
679
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 []
686
687 def zip_children(self):
688 zip_path = zipp.Path(self.root)
689 names = zip_path.root.namelist()
690 self.joinpath = zip_path.joinpath
691
692 return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
693
694 def search(self, name):
695 return self.lookup(self.mtime).search(name)
696
697 @property
698 def mtime(self):
699 with suppress(OSError):
700 return os.stat(self.root).st_mtime
701 self.lookup.cache_clear()
702
703 @method_cache
704 def lookup(self, mtime):
705 return Lookup(self)
706
707
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)
714
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))
726
727 self.infos.freeze()
728 self.eggs.freeze()
729
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)
742
743
744class Prepared:
745 """
746 A prepared search for metadata on a possibly-named package.
747 """
748
749 normalized = None
750 legacy_normalized = None
751
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)
758
759 @staticmethod
760 def normalize(name):
761 """
762 PEP 503 normalization plus dashes as underscores.
763 """
764 return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
765
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('-', '_')
773
774 def __bool__(self):
775 return bool(self.name)
776
777
778@install
779class MetadataPathFinder(NullFinder, DistributionFinder):
780 """A degenerate finder for distribution packages on the file system.
781
782 This finder supplies only a find_distributions() method for versions
783 of Python that do not have a PathFinder find_distributions().
784 """
785
786 def find_distributions(
787 self, context=DistributionFinder.Context()
788 ) -> Iterable["PathDistribution"]:
789 """
790 Find distributions.
791
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)
799
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 )
807
808 def invalidate_caches(cls) -> None:
809 FastPath.__new__.cache_clear()
810
811
812class PathDistribution(Distribution):
813 def __init__(self, path: SimplePath) -> None:
814 """Construct a distribution.
815
816 :param path: SimplePath indicating the metadata directory.
817 """
818 self._path = path
819
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')
829
830 return None
831
832 read_text.__doc__ = Distribution.read_text.__doc__
833
834 def locate_file(self, path: StrPath) -> pathlib.Path:
835 return self._path.parent / path
836
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 )
848
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
865
866
867def distribution(distribution_name: str) -> Distribution:
868 """Get the ``Distribution`` instance for the named package.
869
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)
874
875
876def distributions(**kwargs) -> Iterable[Distribution]:
877 """Get all ``Distribution`` instances in the current environment.
878
879 :return: An iterable of ``Distribution`` instances.
880 """
881 return Distribution.discover(**kwargs)
882
883
884def metadata(distribution_name: str) -> _meta.PackageMetadata:
885 """Get the metadata for the named package.
886
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
891
892
893def version(distribution_name: str) -> str:
894 """Get the version string for the named package.
895
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
901
902
903_unique = functools.partial(
904 unique_everseen,
905 key=_py39compat.normalized_name,
906)
907"""
908Wrapper for ``distributions`` to return unique distributions by name.
909"""
910
911
912def entry_points(**params) -> EntryPoints:
913 """Return EntryPoint objects for all installed packages.
914
915 Pass selection parameters (group or name) to filter the
916 result to entry points matching those properties (see
917 EntryPoints.select()).
918
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)
925
926
927def files(distribution_name: str) -> Optional[List[PackagePath]]:
928 """Return a list of files for the named package.
929
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
934
935
936def requires(distribution_name: str) -> Optional[List[str]]:
937 """
938 Return a list of requirements for the named package.
939
940 :return: An iterable of requirements, suitable for
941 packaging.requirement.Requirement.
942 """
943 return distribution(distribution_name).requires
944
945
946def packages_distributions() -> Mapping[str, List[str]]:
947 """
948 Return a mapping of top-level packages to their
949 distributions.
950
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)
961
962
963def _top_level_declared(dist):
964 return (dist.read_text('top_level.txt') or '').split()
965
966
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
973
974
975def _get_toplevel_name(name: PackagePath) -> str:
976 """
977 Infer a possibly importable module name from a name presumed on
978 sys.path.
979
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 )
998
999
1000def _top_level_inferred(dist):
1001 opt_names = set(map(_get_toplevel_name, always_iterable(dist.files)))
1002
1003 def importable_name(name):
1004 return '.' not in name
1005
1006 return filter(importable_name, opt_names)