Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/importlib_metadata/__init__.py: 48%
427 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +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 collections
17from . import _adapters, _meta, _py39compat
18from ._collections import FreezableDefaultDict, Pair
19from ._compat import (
20 NullFinder,
21 install,
22 pypy_partial,
23)
24from ._functools import method_cache, pass_none
25from ._itertools import always_iterable, unique_everseen
26from ._meta import PackageMetadata, SimplePath
28from contextlib import suppress
29from importlib import import_module
30from importlib.abc import MetaPathFinder
31from itertools import starmap
32from typing import List, Mapping, Optional, Union
35__all__ = [
36 'Distribution',
37 'DistributionFinder',
38 'PackageMetadata',
39 'PackageNotFoundError',
40 'distribution',
41 'distributions',
42 'entry_points',
43 'files',
44 'metadata',
45 'packages_distributions',
46 'requires',
47 'version',
48]
51class PackageNotFoundError(ModuleNotFoundError):
52 """The package was not found."""
54 def __str__(self):
55 return f"No package metadata was found for {self.name}"
57 @property
58 def name(self):
59 (name,) = self.args
60 return name
63class Sectioned:
64 """
65 A simple entry point config parser for performance
67 >>> for item in Sectioned.read(Sectioned._sample):
68 ... print(item)
69 Pair(name='sec1', value='# comments ignored')
70 Pair(name='sec1', value='a = 1')
71 Pair(name='sec1', value='b = 2')
72 Pair(name='sec2', value='a = 2')
74 >>> res = Sectioned.section_pairs(Sectioned._sample)
75 >>> item = next(res)
76 >>> item.name
77 'sec1'
78 >>> item.value
79 Pair(name='a', value='1')
80 >>> item = next(res)
81 >>> item.value
82 Pair(name='b', value='2')
83 >>> item = next(res)
84 >>> item.name
85 'sec2'
86 >>> item.value
87 Pair(name='a', value='2')
88 >>> list(res)
89 []
90 """
92 _sample = textwrap.dedent(
93 """
94 [sec1]
95 # comments ignored
96 a = 1
97 b = 2
99 [sec2]
100 a = 2
101 """
102 ).lstrip()
104 @classmethod
105 def section_pairs(cls, text):
106 return (
107 section._replace(value=Pair.parse(section.value))
108 for section in cls.read(text, filter_=cls.valid)
109 if section.name is not None
110 )
112 @staticmethod
113 def read(text, filter_=None):
114 lines = filter(filter_, map(str.strip, text.splitlines()))
115 name = None
116 for value in lines:
117 section_match = value.startswith('[') and value.endswith(']')
118 if section_match:
119 name = value.strip('[]')
120 continue
121 yield Pair(name, value)
123 @staticmethod
124 def valid(line):
125 return line and not line.startswith('#')
128class DeprecatedTuple:
129 """
130 Provide subscript item access for backward compatibility.
132 >>> recwarn = getfixture('recwarn')
133 >>> ep = EntryPoint(name='name', value='value', group='group')
134 >>> ep[:]
135 ('name', 'value', 'group')
136 >>> ep[0]
137 'name'
138 >>> len(recwarn)
139 1
140 """
142 _warn = functools.partial(
143 warnings.warn,
144 "EntryPoint tuple interface is deprecated. Access members by name.",
145 DeprecationWarning,
146 stacklevel=pypy_partial(2),
147 )
149 def __getitem__(self, item):
150 self._warn()
151 return self._key()[item]
154class EntryPoint(DeprecatedTuple):
155 """An entry point as defined by Python packaging conventions.
157 See `the packaging docs on entry points
158 <https://packaging.python.org/specifications/entry-points/>`_
159 for more information.
161 >>> ep = EntryPoint(
162 ... name=None, group=None, value='package.module:attr [extra1, extra2]')
163 >>> ep.module
164 'package.module'
165 >>> ep.attr
166 'attr'
167 >>> ep.extras
168 ['extra1', 'extra2']
169 """
171 pattern = re.compile(
172 r'(?P<module>[\w.]+)\s*'
173 r'(:\s*(?P<attr>[\w.]+)\s*)?'
174 r'((?P<extras>\[.*\])\s*)?$'
175 )
176 """
177 A regular expression describing the syntax for an entry point,
178 which might look like:
180 - module
181 - package.module
182 - package.module:attribute
183 - package.module:object.attribute
184 - package.module:attr [extra1, extra2]
186 Other combinations are possible as well.
188 The expression is lenient about whitespace around the ':',
189 following the attr, and following any extras.
190 """
192 name: str
193 value: str
194 group: str
196 dist: Optional['Distribution'] = None
198 def __init__(self, name, value, group):
199 vars(self).update(name=name, value=value, group=group)
201 def load(self):
202 """Load the entry point from its definition. If only a module
203 is indicated by the value, return that module. Otherwise,
204 return the named object.
205 """
206 match = self.pattern.match(self.value)
207 module = import_module(match.group('module'))
208 attrs = filter(None, (match.group('attr') or '').split('.'))
209 return functools.reduce(getattr, attrs, module)
211 @property
212 def module(self):
213 match = self.pattern.match(self.value)
214 return match.group('module')
216 @property
217 def attr(self):
218 match = self.pattern.match(self.value)
219 return match.group('attr')
221 @property
222 def extras(self):
223 match = self.pattern.match(self.value)
224 return re.findall(r'\w+', match.group('extras') or '')
226 def _for(self, dist):
227 vars(self).update(dist=dist)
228 return self
230 def __iter__(self):
231 """
232 Supply iter so one may construct dicts of EntryPoints by name.
233 """
234 msg = (
235 "Construction of dict of EntryPoints is deprecated in "
236 "favor of EntryPoints."
237 )
238 warnings.warn(msg, DeprecationWarning)
239 return iter((self.name, self))
241 def matches(self, **params):
242 """
243 EntryPoint matches the given parameters.
245 >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]')
246 >>> ep.matches(group='foo')
247 True
248 >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]')
249 True
250 >>> ep.matches(group='foo', name='other')
251 False
252 >>> ep.matches()
253 True
254 >>> ep.matches(extras=['extra1', 'extra2'])
255 True
256 >>> ep.matches(module='bing')
257 True
258 >>> ep.matches(attr='bong')
259 True
260 """
261 attrs = (getattr(self, param) for param in params)
262 return all(map(operator.eq, params.values(), attrs))
264 def _key(self):
265 return self.name, self.value, self.group
267 def __lt__(self, other):
268 return self._key() < other._key()
270 def __eq__(self, other):
271 return self._key() == other._key()
273 def __setattr__(self, name, value):
274 raise AttributeError("EntryPoint objects are immutable.")
276 def __repr__(self):
277 return (
278 f'EntryPoint(name={self.name!r}, value={self.value!r}, '
279 f'group={self.group!r})'
280 )
282 def __hash__(self):
283 return hash(self._key())
286class DeprecatedList(list):
287 """
288 Allow an otherwise immutable object to implement mutability
289 for compatibility.
291 >>> recwarn = getfixture('recwarn')
292 >>> dl = DeprecatedList(range(3))
293 >>> dl[0] = 1
294 >>> dl.append(3)
295 >>> del dl[3]
296 >>> dl.reverse()
297 >>> dl.sort()
298 >>> dl.extend([4])
299 >>> dl.pop(-1)
300 4
301 >>> dl.remove(1)
302 >>> dl += [5]
303 >>> dl + [6]
304 [1, 2, 5, 6]
305 >>> dl + (6,)
306 [1, 2, 5, 6]
307 >>> dl.insert(0, 0)
308 >>> dl
309 [0, 1, 2, 5]
310 >>> dl == [0, 1, 2, 5]
311 True
312 >>> dl == (0, 1, 2, 5)
313 True
314 >>> len(recwarn)
315 1
316 """
318 __slots__ = ()
320 _warn = functools.partial(
321 warnings.warn,
322 "EntryPoints list interface is deprecated. Cast to list if needed.",
323 DeprecationWarning,
324 stacklevel=pypy_partial(2),
325 )
327 def _wrap_deprecated_method(method_name: str): # type: ignore
328 def wrapped(self, *args, **kwargs):
329 self._warn()
330 return getattr(super(), method_name)(*args, **kwargs)
332 return method_name, wrapped
334 locals().update(
335 map(
336 _wrap_deprecated_method,
337 '__setitem__ __delitem__ append reverse extend pop remove '
338 '__iadd__ insert sort'.split(),
339 )
340 )
342 def __add__(self, other):
343 if not isinstance(other, tuple):
344 self._warn()
345 other = tuple(other)
346 return self.__class__(tuple(self) + other)
348 def __eq__(self, other):
349 if not isinstance(other, tuple):
350 self._warn()
351 other = tuple(other)
353 return tuple(self).__eq__(other)
356class EntryPoints(DeprecatedList):
357 """
358 An immutable collection of selectable EntryPoint objects.
359 """
361 __slots__ = ()
363 def __getitem__(self, name): # -> EntryPoint:
364 """
365 Get the EntryPoint in self matching name.
366 """
367 if isinstance(name, int):
368 warnings.warn(
369 "Accessing entry points by index is deprecated. "
370 "Cast to tuple if needed.",
371 DeprecationWarning,
372 stacklevel=2,
373 )
374 return super().__getitem__(name)
375 try:
376 return next(iter(self.select(name=name)))
377 except StopIteration:
378 raise KeyError(name)
380 def select(self, **params):
381 """
382 Select entry points from self that match the
383 given parameters (typically group and/or name).
384 """
385 candidates = (_py39compat.ep_matches(ep, **params) for ep in self)
386 return EntryPoints(ep for ep, predicate in candidates if predicate)
388 @property
389 def names(self):
390 """
391 Return the set of all names of all entry points.
392 """
393 return {ep.name for ep in self}
395 @property
396 def groups(self):
397 """
398 Return the set of all groups of all entry points.
400 For coverage while SelectableGroups is present.
401 >>> EntryPoints().groups
402 set()
403 """
404 return {ep.group for ep in self}
406 @classmethod
407 def _from_text_for(cls, text, dist):
408 return cls(ep._for(dist) for ep in cls._from_text(text))
410 @staticmethod
411 def _from_text(text):
412 return (
413 EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
414 for item in Sectioned.section_pairs(text or '')
415 )
418class Deprecated:
419 """
420 Compatibility add-in for mapping to indicate that
421 mapping behavior is deprecated.
423 >>> recwarn = getfixture('recwarn')
424 >>> class DeprecatedDict(Deprecated, dict): pass
425 >>> dd = DeprecatedDict(foo='bar')
426 >>> dd.get('baz', None)
427 >>> dd['foo']
428 'bar'
429 >>> list(dd)
430 ['foo']
431 >>> list(dd.keys())
432 ['foo']
433 >>> 'foo' in dd
434 True
435 >>> list(dd.values())
436 ['bar']
437 >>> len(recwarn)
438 1
439 """
441 _warn = functools.partial(
442 warnings.warn,
443 "SelectableGroups dict interface is deprecated. Use select.",
444 DeprecationWarning,
445 stacklevel=pypy_partial(2),
446 )
448 def __getitem__(self, name):
449 self._warn()
450 return super().__getitem__(name)
452 def get(self, name, default=None):
453 self._warn()
454 return super().get(name, default)
456 def __iter__(self):
457 self._warn()
458 return super().__iter__()
460 def __contains__(self, *args):
461 self._warn()
462 return super().__contains__(*args)
464 def keys(self):
465 self._warn()
466 return super().keys()
468 def values(self):
469 self._warn()
470 return super().values()
473class SelectableGroups(Deprecated, dict):
474 """
475 A backward- and forward-compatible result from
476 entry_points that fully implements the dict interface.
477 """
479 @classmethod
480 def load(cls, eps):
481 by_group = operator.attrgetter('group')
482 ordered = sorted(eps, key=by_group)
483 grouped = itertools.groupby(ordered, by_group)
484 return cls((group, EntryPoints(eps)) for group, eps in grouped)
486 @property
487 def _all(self):
488 """
489 Reconstruct a list of all entrypoints from the groups.
490 """
491 groups = super(Deprecated, self).values()
492 return EntryPoints(itertools.chain.from_iterable(groups))
494 @property
495 def groups(self):
496 return self._all.groups
498 @property
499 def names(self):
500 """
501 for coverage:
502 >>> SelectableGroups().names
503 set()
504 """
505 return self._all.names
507 def select(self, **params):
508 if not params:
509 return self
510 return self._all.select(**params)
513class PackagePath(pathlib.PurePosixPath):
514 """A reference to a path in a package"""
516 def read_text(self, encoding='utf-8'):
517 with self.locate().open(encoding=encoding) as stream:
518 return stream.read()
520 def read_binary(self):
521 with self.locate().open('rb') as stream:
522 return stream.read()
524 def locate(self):
525 """Return a path-like object for this path"""
526 return self.dist.locate_file(self)
529class FileHash:
530 def __init__(self, spec):
531 self.mode, _, self.value = spec.partition('=')
533 def __repr__(self):
534 return f'<FileHash mode: {self.mode} value: {self.value}>'
537class Distribution:
538 """A Python distribution package."""
540 @abc.abstractmethod
541 def read_text(self, filename):
542 """Attempt to load metadata file given by the name.
544 :param filename: The name of the file in the distribution info.
545 :return: The text if found, otherwise None.
546 """
548 @abc.abstractmethod
549 def locate_file(self, path):
550 """
551 Given a path to a file in this distribution, return a path
552 to it.
553 """
555 @classmethod
556 def from_name(cls, name: str):
557 """Return the Distribution for the given package name.
559 :param name: The name of the distribution package to search for.
560 :return: The Distribution instance (or subclass thereof) for the named
561 package, if found.
562 :raises PackageNotFoundError: When the named package's distribution
563 metadata cannot be found.
564 :raises ValueError: When an invalid value is supplied for name.
565 """
566 if not name:
567 raise ValueError("A distribution name is required.")
568 try:
569 return next(cls.discover(name=name))
570 except StopIteration:
571 raise PackageNotFoundError(name)
573 @classmethod
574 def discover(cls, **kwargs):
575 """Return an iterable of Distribution objects for all packages.
577 Pass a ``context`` or pass keyword arguments for constructing
578 a context.
580 :context: A ``DistributionFinder.Context`` object.
581 :return: Iterable of Distribution objects for all packages.
582 """
583 context = kwargs.pop('context', None)
584 if context and kwargs:
585 raise ValueError("cannot accept context and kwargs")
586 context = context or DistributionFinder.Context(**kwargs)
587 return itertools.chain.from_iterable(
588 resolver(context) for resolver in cls._discover_resolvers()
589 )
591 @staticmethod
592 def at(path):
593 """Return a Distribution for the indicated metadata path
595 :param path: a string or path-like object
596 :return: a concrete Distribution instance for the path
597 """
598 return PathDistribution(pathlib.Path(path))
600 @staticmethod
601 def _discover_resolvers():
602 """Search the meta_path for resolvers."""
603 declared = (
604 getattr(finder, 'find_distributions', None) for finder in sys.meta_path
605 )
606 return filter(None, declared)
608 @property
609 def metadata(self) -> _meta.PackageMetadata:
610 """Return the parsed metadata for this Distribution.
612 The returned object will have keys that name the various bits of
613 metadata. See PEP 566 for details.
614 """
615 text = (
616 self.read_text('METADATA')
617 or self.read_text('PKG-INFO')
618 # This last clause is here to support old egg-info files. Its
619 # effect is to just end up using the PathDistribution's self._path
620 # (which points to the egg-info file) attribute unchanged.
621 or self.read_text('')
622 )
623 return _adapters.Message(email.message_from_string(text))
625 @property
626 def name(self):
627 """Return the 'Name' metadata for the distribution package."""
628 return self.metadata['Name']
630 @property
631 def _normalized_name(self):
632 """Return a normalized version of the name."""
633 return Prepared.normalize(self.name)
635 @property
636 def version(self):
637 """Return the 'Version' metadata for the distribution package."""
638 return self.metadata['Version']
640 @property
641 def entry_points(self):
642 return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
644 @property
645 def files(self):
646 """Files in this distribution.
648 :return: List of PackagePath for this distribution or None
650 Result is `None` if the metadata file that enumerates files
651 (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
652 missing.
653 Result may be empty if the metadata exists but is empty.
654 """
656 def make_file(name, hash=None, size_str=None):
657 result = PackagePath(name)
658 result.hash = FileHash(hash) if hash else None
659 result.size = int(size_str) if size_str else None
660 result.dist = self
661 return result
663 @pass_none
664 def make_files(lines):
665 return list(starmap(make_file, csv.reader(lines)))
667 return make_files(self._read_files_distinfo() or self._read_files_egginfo())
669 def _read_files_distinfo(self):
670 """
671 Read the lines of RECORD
672 """
673 text = self.read_text('RECORD')
674 return text and text.splitlines()
676 def _read_files_egginfo(self):
677 """
678 SOURCES.txt might contain literal commas, so wrap each line
679 in quotes.
680 """
681 text = self.read_text('SOURCES.txt')
682 return text and map('"{}"'.format, text.splitlines())
684 @property
685 def requires(self):
686 """Generated requirements specified for this Distribution"""
687 reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
688 return reqs and list(reqs)
690 def _read_dist_info_reqs(self):
691 return self.metadata.get_all('Requires-Dist')
693 def _read_egg_info_reqs(self):
694 source = self.read_text('requires.txt')
695 return pass_none(self._deps_from_requires_text)(source)
697 @classmethod
698 def _deps_from_requires_text(cls, source):
699 return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
701 @staticmethod
702 def _convert_egg_info_reqs_to_simple_reqs(sections):
703 """
704 Historically, setuptools would solicit and store 'extra'
705 requirements, including those with environment markers,
706 in separate sections. More modern tools expect each
707 dependency to be defined separately, with any relevant
708 extras and environment markers attached directly to that
709 requirement. This method converts the former to the
710 latter. See _test_deps_from_requires_text for an example.
711 """
713 def make_condition(name):
714 return name and f'extra == "{name}"'
716 def quoted_marker(section):
717 section = section or ''
718 extra, sep, markers = section.partition(':')
719 if extra and markers:
720 markers = f'({markers})'
721 conditions = list(filter(None, [markers, make_condition(extra)]))
722 return '; ' + ' and '.join(conditions) if conditions else ''
724 def url_req_space(req):
725 """
726 PEP 508 requires a space between the url_spec and the quoted_marker.
727 Ref python/importlib_metadata#357.
728 """
729 # '@' is uniquely indicative of a url_req.
730 return ' ' * ('@' in req)
732 for section in sections:
733 space = url_req_space(section.value)
734 yield section.value + space + quoted_marker(section.name)
737class DistributionFinder(MetaPathFinder):
738 """
739 A MetaPathFinder capable of discovering installed distributions.
740 """
742 class Context:
743 """
744 Keyword arguments presented by the caller to
745 ``distributions()`` or ``Distribution.discover()``
746 to narrow the scope of a search for distributions
747 in all DistributionFinders.
749 Each DistributionFinder may expect any parameters
750 and should attempt to honor the canonical
751 parameters defined below when appropriate.
752 """
754 name = None
755 """
756 Specific name for which a distribution finder should match.
757 A name of ``None`` matches all distributions.
758 """
760 def __init__(self, **kwargs):
761 vars(self).update(kwargs)
763 @property
764 def path(self):
765 """
766 The sequence of directory path that a distribution finder
767 should search.
769 Typically refers to Python installed package paths such as
770 "site-packages" directories and defaults to ``sys.path``.
771 """
772 return vars(self).get('path', sys.path)
774 @abc.abstractmethod
775 def find_distributions(self, context=Context()):
776 """
777 Find distributions.
779 Return an iterable of all Distribution instances capable of
780 loading the metadata for packages matching the ``context``,
781 a DistributionFinder.Context instance.
782 """
785class FastPath:
786 """
787 Micro-optimized class for searching a path for
788 children.
790 >>> FastPath('').children()
791 ['...']
792 """
794 @functools.lru_cache() # type: ignore
795 def __new__(cls, root):
796 return super().__new__(cls)
798 def __init__(self, root):
799 self.root = root
801 def joinpath(self, child):
802 return pathlib.Path(self.root, child)
804 def children(self):
805 with suppress(Exception):
806 return os.listdir(self.root or '.')
807 with suppress(Exception):
808 return self.zip_children()
809 return []
811 def zip_children(self):
812 zip_path = zipp.Path(self.root)
813 names = zip_path.root.namelist()
814 self.joinpath = zip_path.joinpath
816 return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
818 def search(self, name):
819 return self.lookup(self.mtime).search(name)
821 @property
822 def mtime(self):
823 with suppress(OSError):
824 return os.stat(self.root).st_mtime
825 self.lookup.cache_clear()
827 @method_cache
828 def lookup(self, mtime):
829 return Lookup(self)
832class Lookup:
833 def __init__(self, path: FastPath):
834 base = os.path.basename(path.root).lower()
835 base_is_egg = base.endswith(".egg")
836 self.infos = FreezableDefaultDict(list)
837 self.eggs = FreezableDefaultDict(list)
839 for child in path.children():
840 low = child.lower()
841 if low.endswith((".dist-info", ".egg-info")):
842 # rpartition is faster than splitext and suitable for this purpose.
843 name = low.rpartition(".")[0].partition("-")[0]
844 normalized = Prepared.normalize(name)
845 self.infos[normalized].append(path.joinpath(child))
846 elif base_is_egg and low == "egg-info":
847 name = base.rpartition(".")[0].partition("-")[0]
848 legacy_normalized = Prepared.legacy_normalize(name)
849 self.eggs[legacy_normalized].append(path.joinpath(child))
851 self.infos.freeze()
852 self.eggs.freeze()
854 def search(self, prepared):
855 infos = (
856 self.infos[prepared.normalized]
857 if prepared
858 else itertools.chain.from_iterable(self.infos.values())
859 )
860 eggs = (
861 self.eggs[prepared.legacy_normalized]
862 if prepared
863 else itertools.chain.from_iterable(self.eggs.values())
864 )
865 return itertools.chain(infos, eggs)
868class Prepared:
869 """
870 A prepared search for metadata on a possibly-named package.
871 """
873 normalized = None
874 legacy_normalized = None
876 def __init__(self, name):
877 self.name = name
878 if name is None:
879 return
880 self.normalized = self.normalize(name)
881 self.legacy_normalized = self.legacy_normalize(name)
883 @staticmethod
884 def normalize(name):
885 """
886 PEP 503 normalization plus dashes as underscores.
887 """
888 return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
890 @staticmethod
891 def legacy_normalize(name):
892 """
893 Normalize the package name as found in the convention in
894 older packaging tools versions and specs.
895 """
896 return name.lower().replace('-', '_')
898 def __bool__(self):
899 return bool(self.name)
902@install
903class MetadataPathFinder(NullFinder, DistributionFinder):
904 """A degenerate finder for distribution packages on the file system.
906 This finder supplies only a find_distributions() method for versions
907 of Python that do not have a PathFinder find_distributions().
908 """
910 def find_distributions(self, context=DistributionFinder.Context()):
911 """
912 Find distributions.
914 Return an iterable of all Distribution instances capable of
915 loading the metadata for packages matching ``context.name``
916 (or all names if ``None`` indicated) along the paths in the list
917 of directories ``context.path``.
918 """
919 found = self._search_paths(context.name, context.path)
920 return map(PathDistribution, found)
922 @classmethod
923 def _search_paths(cls, name, paths):
924 """Find metadata directories in paths heuristically."""
925 prepared = Prepared(name)
926 return itertools.chain.from_iterable(
927 path.search(prepared) for path in map(FastPath, paths)
928 )
930 def invalidate_caches(cls):
931 FastPath.__new__.cache_clear()
934class PathDistribution(Distribution):
935 def __init__(self, path: SimplePath):
936 """Construct a distribution.
938 :param path: SimplePath indicating the metadata directory.
939 """
940 self._path = path
942 def read_text(self, filename):
943 with suppress(
944 FileNotFoundError,
945 IsADirectoryError,
946 KeyError,
947 NotADirectoryError,
948 PermissionError,
949 ):
950 return self._path.joinpath(filename).read_text(encoding='utf-8')
952 read_text.__doc__ = Distribution.read_text.__doc__
954 def locate_file(self, path):
955 return self._path.parent / path
957 @property
958 def _normalized_name(self):
959 """
960 Performance optimization: where possible, resolve the
961 normalized name from the file system path.
962 """
963 stem = os.path.basename(str(self._path))
964 return (
965 pass_none(Prepared.normalize)(self._name_from_stem(stem))
966 or super()._normalized_name
967 )
969 @staticmethod
970 def _name_from_stem(stem):
971 """
972 >>> PathDistribution._name_from_stem('foo-3.0.egg-info')
973 'foo'
974 >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
975 'CherryPy'
976 >>> PathDistribution._name_from_stem('face.egg-info')
977 'face'
978 >>> PathDistribution._name_from_stem('foo.bar')
979 """
980 filename, ext = os.path.splitext(stem)
981 if ext not in ('.dist-info', '.egg-info'):
982 return
983 name, sep, rest = filename.partition('-')
984 return name
987def distribution(distribution_name):
988 """Get the ``Distribution`` instance for the named package.
990 :param distribution_name: The name of the distribution package as a string.
991 :return: A ``Distribution`` instance (or subclass thereof).
992 """
993 return Distribution.from_name(distribution_name)
996def distributions(**kwargs):
997 """Get all ``Distribution`` instances in the current environment.
999 :return: An iterable of ``Distribution`` instances.
1000 """
1001 return Distribution.discover(**kwargs)
1004def metadata(distribution_name) -> _meta.PackageMetadata:
1005 """Get the metadata for the named package.
1007 :param distribution_name: The name of the distribution package to query.
1008 :return: A PackageMetadata containing the parsed metadata.
1009 """
1010 return Distribution.from_name(distribution_name).metadata
1013def version(distribution_name):
1014 """Get the version string for the named package.
1016 :param distribution_name: The name of the distribution package to query.
1017 :return: The version string for the package as defined in the package's
1018 "Version" metadata key.
1019 """
1020 return distribution(distribution_name).version
1023_unique = functools.partial(
1024 unique_everseen,
1025 key=_py39compat.normalized_name,
1026)
1027"""
1028Wrapper for ``distributions`` to return unique distributions by name.
1029"""
1032def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
1033 """Return EntryPoint objects for all installed packages.
1035 Pass selection parameters (group or name) to filter the
1036 result to entry points matching those properties (see
1037 EntryPoints.select()).
1039 For compatibility, returns ``SelectableGroups`` object unless
1040 selection parameters are supplied. In the future, this function
1041 will return ``EntryPoints`` instead of ``SelectableGroups``
1042 even when no selection parameters are supplied.
1044 For maximum future compatibility, pass selection parameters
1045 or invoke ``.select`` with parameters on the result.
1047 :return: EntryPoints or SelectableGroups for all installed packages.
1048 """
1049 eps = itertools.chain.from_iterable(
1050 dist.entry_points for dist in _unique(distributions())
1051 )
1052 return SelectableGroups.load(eps).select(**params)
1055def files(distribution_name):
1056 """Return a list of files for the named package.
1058 :param distribution_name: The name of the distribution package to query.
1059 :return: List of files composing the distribution.
1060 """
1061 return distribution(distribution_name).files
1064def requires(distribution_name):
1065 """
1066 Return a list of requirements for the named package.
1068 :return: An iterator of requirements, suitable for
1069 packaging.requirement.Requirement.
1070 """
1071 return distribution(distribution_name).requires
1074def packages_distributions() -> Mapping[str, List[str]]:
1075 """
1076 Return a mapping of top-level packages to their
1077 distributions.
1079 >>> import collections.abc
1080 >>> pkgs = packages_distributions()
1081 >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
1082 True
1083 """
1084 pkg_to_dist = collections.defaultdict(list)
1085 for dist in distributions():
1086 for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
1087 pkg_to_dist[pkg].append(dist.metadata['Name'])
1088 return dict(pkg_to_dist)
1091def _top_level_declared(dist):
1092 return (dist.read_text('top_level.txt') or '').split()
1095def _top_level_inferred(dist):
1096 return {
1097 f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
1098 for f in always_iterable(dist.files)
1099 if f.suffix == ".py"
1100 }