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