292e0c6d4a73d5e2b8003394fe316dc3317d9e92
[SubU] /
1 import os
2 import re
3 import abc
4 import csv
5 import sys
6 from .. import zipp
7 import email
8 import pathlib
9 import operator
10 import textwrap
11 import warnings
12 import functools
13 import itertools
14 import posixpath
15 import collections
16
17 from . import _adapters, _meta
18 from ._collections import FreezableDefaultDict, Pair
19 from ._compat import (
20     NullFinder,
21     install,
22     pypy_partial,
23 )
24 from ._functools import method_cache, pass_none
25 from ._itertools import always_iterable, unique_everseen
26 from ._meta import PackageMetadata, SimplePath
27
28 from contextlib import suppress
29 from importlib import import_module
30 from importlib.abc import MetaPathFinder
31 from itertools import starmap
32 from typing import List, Mapping, Optional, Union
33
34
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 ]
49
50
51 class PackageNotFoundError(ModuleNotFoundError):
52     """The package was not found."""
53
54     def __str__(self):
55         return f"No package metadata was found for {self.name}"
56
57     @property
58     def name(self):
59         (name,) = self.args
60         return name
61
62
63 class Sectioned:
64     """
65     A simple entry point config parser for performance
66
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')
73
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     """
91
92     _sample = textwrap.dedent(
93         """
94         [sec1]
95         # comments ignored
96         a = 1
97         b = 2
98
99         [sec2]
100         a = 2
101         """
102     ).lstrip()
103
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         )
111
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)
122
123     @staticmethod
124     def valid(line):
125         return line and not line.startswith('#')
126
127
128 class DeprecatedTuple:
129     """
130     Provide subscript item access for backward compatibility.
131
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     """
141
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     )
148
149     def __getitem__(self, item):
150         self._warn()
151         return self._key()[item]
152
153
154 class EntryPoint(DeprecatedTuple):
155     """An entry point as defined by Python packaging conventions.
156
157     See `the packaging docs on entry points
158     <https://packaging.python.org/specifications/entry-points/>`_
159     for more information.
160     """
161
162     pattern = re.compile(
163         r'(?P<module>[\w.]+)\s*'
164         r'(:\s*(?P<attr>[\w.]+)\s*)?'
165         r'((?P<extras>\[.*\])\s*)?$'
166     )
167     """
168     A regular expression describing the syntax for an entry point,
169     which might look like:
170
171         - module
172         - package.module
173         - package.module:attribute
174         - package.module:object.attribute
175         - package.module:attr [extra1, extra2]
176
177     Other combinations are possible as well.
178
179     The expression is lenient about whitespace around the ':',
180     following the attr, and following any extras.
181     """
182
183     dist: Optional['Distribution'] = None
184
185     def __init__(self, name, value, group):
186         vars(self).update(name=name, value=value, group=group)
187
188     def load(self):
189         """Load the entry point from its definition. If only a module
190         is indicated by the value, return that module. Otherwise,
191         return the named object.
192         """
193         match = self.pattern.match(self.value)
194         module = import_module(match.group('module'))
195         attrs = filter(None, (match.group('attr') or '').split('.'))
196         return functools.reduce(getattr, attrs, module)
197
198     @property
199     def module(self):
200         match = self.pattern.match(self.value)
201         return match.group('module')
202
203     @property
204     def attr(self):
205         match = self.pattern.match(self.value)
206         return match.group('attr')
207
208     @property
209     def extras(self):
210         match = self.pattern.match(self.value)
211         return list(re.finditer(r'\w+', match.group('extras') or ''))
212
213     def _for(self, dist):
214         vars(self).update(dist=dist)
215         return self
216
217     def __iter__(self):
218         """
219         Supply iter so one may construct dicts of EntryPoints by name.
220         """
221         msg = (
222             "Construction of dict of EntryPoints is deprecated in "
223             "favor of EntryPoints."
224         )
225         warnings.warn(msg, DeprecationWarning)
226         return iter((self.name, self))
227
228     def matches(self, **params):
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):
251         return hash(self._key())
252
253
254 class DeprecatedList(list):
255     """
256     Allow an otherwise immutable object to implement mutability
257     for compatibility.
258
259     >>> recwarn = getfixture('recwarn')
260     >>> dl = DeprecatedList(range(3))
261     >>> dl[0] = 1
262     >>> dl.append(3)
263     >>> del dl[3]
264     >>> dl.reverse()
265     >>> dl.sort()
266     >>> dl.extend([4])
267     >>> dl.pop(-1)
268     4
269     >>> dl.remove(1)
270     >>> dl += [5]
271     >>> dl + [6]
272     [1, 2, 5, 6]
273     >>> dl + (6,)
274     [1, 2, 5, 6]
275     >>> dl.insert(0, 0)
276     >>> dl
277     [0, 1, 2, 5]
278     >>> dl == [0, 1, 2, 5]
279     True
280     >>> dl == (0, 1, 2, 5)
281     True
282     >>> len(recwarn)
283     1
284     """
285
286     __slots__ = ()
287
288     _warn = functools.partial(
289         warnings.warn,
290         "EntryPoints list interface is deprecated. Cast to list if needed.",
291         DeprecationWarning,
292         stacklevel=pypy_partial(2),
293     )
294
295     def _wrap_deprecated_method(method_name: str):  # type: ignore
296         def wrapped(self, *args, **kwargs):
297             self._warn()
298             return getattr(super(), method_name)(*args, **kwargs)
299
300         return method_name, wrapped
301
302     locals().update(
303         map(
304             _wrap_deprecated_method,
305             '__setitem__ __delitem__ append reverse extend pop remove '
306             '__iadd__ insert sort'.split(),
307         )
308     )
309
310     def __add__(self, other):
311         if not isinstance(other, tuple):
312             self._warn()
313             other = tuple(other)
314         return self.__class__(tuple(self) + other)
315
316     def __eq__(self, other):
317         if not isinstance(other, tuple):
318             self._warn()
319             other = tuple(other)
320
321         return tuple(self).__eq__(other)
322
323
324 class EntryPoints(DeprecatedList):
325     """
326     An immutable collection of selectable EntryPoint objects.
327     """
328
329     __slots__ = ()
330
331     def __getitem__(self, name):  # -> EntryPoint:
332         """
333         Get the EntryPoint in self matching name.
334         """
335         if isinstance(name, int):
336             warnings.warn(
337                 "Accessing entry points by index is deprecated. "
338                 "Cast to tuple if needed.",
339                 DeprecationWarning,
340                 stacklevel=2,
341             )
342             return super().__getitem__(name)
343         try:
344             return next(iter(self.select(name=name)))
345         except StopIteration:
346             raise KeyError(name)
347
348     def select(self, **params):
349         """
350         Select entry points from self that match the
351         given parameters (typically group and/or name).
352         """
353         return EntryPoints(ep for ep in self if ep.matches(**params))
354
355     @property
356     def names(self):
357         """
358         Return the set of all names of all entry points.
359         """
360         return {ep.name for ep in self}
361
362     @property
363     def groups(self):
364         """
365         Return the set of all groups of all entry points.
366
367         For coverage while SelectableGroups is present.
368         >>> EntryPoints().groups
369         set()
370         """
371         return {ep.group for ep in self}
372
373     @classmethod
374     def _from_text_for(cls, text, dist):
375         return cls(ep._for(dist) for ep in cls._from_text(text))
376
377     @staticmethod
378     def _from_text(text):
379         return (
380             EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
381             for item in Sectioned.section_pairs(text or '')
382         )
383
384
385 class Deprecated:
386     """
387     Compatibility add-in for mapping to indicate that
388     mapping behavior is deprecated.
389
390     >>> recwarn = getfixture('recwarn')
391     >>> class DeprecatedDict(Deprecated, dict): pass
392     >>> dd = DeprecatedDict(foo='bar')
393     >>> dd.get('baz', None)
394     >>> dd['foo']
395     'bar'
396     >>> list(dd)
397     ['foo']
398     >>> list(dd.keys())
399     ['foo']
400     >>> 'foo' in dd
401     True
402     >>> list(dd.values())
403     ['bar']
404     >>> len(recwarn)
405     1
406     """
407
408     _warn = functools.partial(
409         warnings.warn,
410         "SelectableGroups dict interface is deprecated. Use select.",
411         DeprecationWarning,
412         stacklevel=pypy_partial(2),
413     )
414
415     def __getitem__(self, name):
416         self._warn()
417         return super().__getitem__(name)
418
419     def get(self, name, default=None):
420         self._warn()
421         return super().get(name, default)
422
423     def __iter__(self):
424         self._warn()
425         return super().__iter__()
426
427     def __contains__(self, *args):
428         self._warn()
429         return super().__contains__(*args)
430
431     def keys(self):
432         self._warn()
433         return super().keys()
434
435     def values(self):
436         self._warn()
437         return super().values()
438
439
440 class SelectableGroups(Deprecated, dict):
441     """
442     A backward- and forward-compatible result from
443     entry_points that fully implements the dict interface.
444     """
445
446     @classmethod
447     def load(cls, eps):
448         by_group = operator.attrgetter('group')
449         ordered = sorted(eps, key=by_group)
450         grouped = itertools.groupby(ordered, by_group)
451         return cls((group, EntryPoints(eps)) for group, eps in grouped)
452
453     @property
454     def _all(self):
455         """
456         Reconstruct a list of all entrypoints from the groups.
457         """
458         groups = super(Deprecated, self).values()
459         return EntryPoints(itertools.chain.from_iterable(groups))
460
461     @property
462     def groups(self):
463         return self._all.groups
464
465     @property
466     def names(self):
467         """
468         for coverage:
469         >>> SelectableGroups().names
470         set()
471         """
472         return self._all.names
473
474     def select(self, **params):
475         if not params:
476             return self
477         return self._all.select(**params)
478
479
480 class PackagePath(pathlib.PurePosixPath):
481     """A reference to a path in a package"""
482
483     def read_text(self, encoding='utf-8'):
484         with self.locate().open(encoding=encoding) as stream:
485             return stream.read()
486
487     def read_binary(self):
488         with self.locate().open('rb') as stream:
489             return stream.read()
490
491     def locate(self):
492         """Return a path-like object for this path"""
493         return self.dist.locate_file(self)
494
495
496 class FileHash:
497     def __init__(self, spec):
498         self.mode, _, self.value = spec.partition('=')
499
500     def __repr__(self):
501         return f'<FileHash mode: {self.mode} value: {self.value}>'
502
503
504 class Distribution:
505     """A Python distribution package."""
506
507     @abc.abstractmethod
508     def read_text(self, filename):
509         """Attempt to load metadata file given by the name.
510
511         :param filename: The name of the file in the distribution info.
512         :return: The text if found, otherwise None.
513         """
514
515     @abc.abstractmethod
516     def locate_file(self, path):
517         """
518         Given a path to a file in this distribution, return a path
519         to it.
520         """
521
522     @classmethod
523     def from_name(cls, name):
524         """Return the Distribution for the given package name.
525
526         :param name: The name of the distribution package to search for.
527         :return: The Distribution instance (or subclass thereof) for the named
528             package, if found.
529         :raises PackageNotFoundError: When the named package's distribution
530             metadata cannot be found.
531         """
532         for resolver in cls._discover_resolvers():
533             dists = resolver(DistributionFinder.Context(name=name))
534             dist = next(iter(dists), None)
535             if dist is not None:
536                 return dist
537         else:
538             raise PackageNotFoundError(name)
539
540     @classmethod
541     def discover(cls, **kwargs):
542         """Return an iterable of Distribution objects for all packages.
543
544         Pass a ``context`` or pass keyword arguments for constructing
545         a context.
546
547         :context: A ``DistributionFinder.Context`` object.
548         :return: Iterable of Distribution objects for all packages.
549         """
550         context = kwargs.pop('context', None)
551         if context and kwargs:
552             raise ValueError("cannot accept context and kwargs")
553         context = context or DistributionFinder.Context(**kwargs)
554         return itertools.chain.from_iterable(
555             resolver(context) for resolver in cls._discover_resolvers()
556         )
557
558     @staticmethod
559     def at(path):
560         """Return a Distribution for the indicated metadata path
561
562         :param path: a string or path-like object
563         :return: a concrete Distribution instance for the path
564         """
565         return PathDistribution(pathlib.Path(path))
566
567     @staticmethod
568     def _discover_resolvers():
569         """Search the meta_path for resolvers."""
570         declared = (
571             getattr(finder, 'find_distributions', None) for finder in sys.meta_path
572         )
573         return filter(None, declared)
574
575     @property
576     def metadata(self) -> _meta.PackageMetadata:
577         """Return the parsed metadata for this Distribution.
578
579         The returned object will have keys that name the various bits of
580         metadata.  See PEP 566 for details.
581         """
582         text = (
583             self.read_text('METADATA')
584             or self.read_text('PKG-INFO')
585             # This last clause is here to support old egg-info files.  Its
586             # effect is to just end up using the PathDistribution's self._path
587             # (which points to the egg-info file) attribute unchanged.
588             or self.read_text('')
589         )
590         return _adapters.Message(email.message_from_string(text))
591
592     @property
593     def name(self):
594         """Return the 'Name' metadata for the distribution package."""
595         return self.metadata['Name']
596
597     @property
598     def _normalized_name(self):
599         """Return a normalized version of the name."""
600         return Prepared.normalize(self.name)
601
602     @property
603     def version(self):
604         """Return the 'Version' metadata for the distribution package."""
605         return self.metadata['Version']
606
607     @property
608     def entry_points(self):
609         return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
610
611     @property
612     def files(self):
613         """Files in this distribution.
614
615         :return: List of PackagePath for this distribution or None
616
617         Result is `None` if the metadata file that enumerates files
618         (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
619         missing.
620         Result may be empty if the metadata exists but is empty.
621         """
622
623         def make_file(name, hash=None, size_str=None):
624             result = PackagePath(name)
625             result.hash = FileHash(hash) if hash else None
626             result.size = int(size_str) if size_str else None
627             result.dist = self
628             return result
629
630         @pass_none
631         def make_files(lines):
632             return list(starmap(make_file, csv.reader(lines)))
633
634         return make_files(self._read_files_distinfo() or self._read_files_egginfo())
635
636     def _read_files_distinfo(self):
637         """
638         Read the lines of RECORD
639         """
640         text = self.read_text('RECORD')
641         return text and text.splitlines()
642
643     def _read_files_egginfo(self):
644         """
645         SOURCES.txt might contain literal commas, so wrap each line
646         in quotes.
647         """
648         text = self.read_text('SOURCES.txt')
649         return text and map('"{}"'.format, text.splitlines())
650
651     @property
652     def requires(self):
653         """Generated requirements specified for this Distribution"""
654         reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
655         return reqs and list(reqs)
656
657     def _read_dist_info_reqs(self):
658         return self.metadata.get_all('Requires-Dist')
659
660     def _read_egg_info_reqs(self):
661         source = self.read_text('requires.txt')
662         return pass_none(self._deps_from_requires_text)(source)
663
664     @classmethod
665     def _deps_from_requires_text(cls, source):
666         return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
667
668     @staticmethod
669     def _convert_egg_info_reqs_to_simple_reqs(sections):
670         """
671         Historically, setuptools would solicit and store 'extra'
672         requirements, including those with environment markers,
673         in separate sections. More modern tools expect each
674         dependency to be defined separately, with any relevant
675         extras and environment markers attached directly to that
676         requirement. This method converts the former to the
677         latter. See _test_deps_from_requires_text for an example.
678         """
679
680         def make_condition(name):
681             return name and f'extra == "{name}"'
682
683         def quoted_marker(section):
684             section = section or ''
685             extra, sep, markers = section.partition(':')
686             if extra and markers:
687                 markers = f'({markers})'
688             conditions = list(filter(None, [markers, make_condition(extra)]))
689             return '; ' + ' and '.join(conditions) if conditions else ''
690
691         def url_req_space(req):
692             """
693             PEP 508 requires a space between the url_spec and the quoted_marker.
694             Ref python/importlib_metadata#357.
695             """
696             # '@' is uniquely indicative of a url_req.
697             return ' ' * ('@' in req)
698
699         for section in sections:
700             space = url_req_space(section.value)
701             yield section.value + space + quoted_marker(section.name)
702
703
704 class DistributionFinder(MetaPathFinder):
705     """
706     A MetaPathFinder capable of discovering installed distributions.
707     """
708
709     class Context:
710         """
711         Keyword arguments presented by the caller to
712         ``distributions()`` or ``Distribution.discover()``
713         to narrow the scope of a search for distributions
714         in all DistributionFinders.
715
716         Each DistributionFinder may expect any parameters
717         and should attempt to honor the canonical
718         parameters defined below when appropriate.
719         """
720
721         name = None
722         """
723         Specific name for which a distribution finder should match.
724         A name of ``None`` matches all distributions.
725         """
726
727         def __init__(self, **kwargs):
728             vars(self).update(kwargs)
729
730         @property
731         def path(self):
732             """
733             The sequence of directory path that a distribution finder
734             should search.
735
736             Typically refers to Python installed package paths such as
737             "site-packages" directories and defaults to ``sys.path``.
738             """
739             return vars(self).get('path', sys.path)
740
741     @abc.abstractmethod
742     def find_distributions(self, context=Context()):
743         """
744         Find distributions.
745
746         Return an iterable of all Distribution instances capable of
747         loading the metadata for packages matching the ``context``,
748         a DistributionFinder.Context instance.
749         """
750
751
752 class FastPath:
753     """
754     Micro-optimized class for searching a path for
755     children.
756
757     >>> FastPath('').children()
758     ['...']
759     """
760
761     @functools.lru_cache()  # type: ignore
762     def __new__(cls, root):
763         return super().__new__(cls)
764
765     def __init__(self, root):
766         self.root = str(root)
767
768     def joinpath(self, child):
769         return pathlib.Path(self.root, child)
770
771     def children(self):
772         with suppress(Exception):
773             return os.listdir(self.root or '.')
774         with suppress(Exception):
775             return self.zip_children()
776         return []
777
778     def zip_children(self):
779         zip_path = zipp.Path(self.root)
780         names = zip_path.root.namelist()
781         self.joinpath = zip_path.joinpath
782
783         return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
784
785     def search(self, name):
786         return self.lookup(self.mtime).search(name)
787
788     @property
789     def mtime(self):
790         with suppress(OSError):
791             return os.stat(self.root).st_mtime
792         self.lookup.cache_clear()
793
794     @method_cache
795     def lookup(self, mtime):
796         return Lookup(self)
797
798
799 class Lookup:
800     def __init__(self, path: FastPath):
801         base = os.path.basename(path.root).lower()
802         base_is_egg = base.endswith(".egg")
803         self.infos = FreezableDefaultDict(list)
804         self.eggs = FreezableDefaultDict(list)
805
806         for child in path.children():
807             low = child.lower()
808             if low.endswith((".dist-info", ".egg-info")):
809                 # rpartition is faster than splitext and suitable for this purpose.
810                 name = low.rpartition(".")[0].partition("-")[0]
811                 normalized = Prepared.normalize(name)
812                 self.infos[normalized].append(path.joinpath(child))
813             elif base_is_egg and low == "egg-info":
814                 name = base.rpartition(".")[0].partition("-")[0]
815                 legacy_normalized = Prepared.legacy_normalize(name)
816                 self.eggs[legacy_normalized].append(path.joinpath(child))
817
818         self.infos.freeze()
819         self.eggs.freeze()
820
821     def search(self, prepared):
822         infos = (
823             self.infos[prepared.normalized]
824             if prepared
825             else itertools.chain.from_iterable(self.infos.values())
826         )
827         eggs = (
828             self.eggs[prepared.legacy_normalized]
829             if prepared
830             else itertools.chain.from_iterable(self.eggs.values())
831         )
832         return itertools.chain(infos, eggs)
833
834
835 class Prepared:
836     """
837     A prepared search for metadata on a possibly-named package.
838     """
839
840     normalized = None
841     legacy_normalized = None
842
843     def __init__(self, name):
844         self.name = name
845         if name is None:
846             return
847         self.normalized = self.normalize(name)
848         self.legacy_normalized = self.legacy_normalize(name)
849
850     @staticmethod
851     def normalize(name):
852         """
853         PEP 503 normalization plus dashes as underscores.
854         """
855         return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
856
857     @staticmethod
858     def legacy_normalize(name):
859         """
860         Normalize the package name as found in the convention in
861         older packaging tools versions and specs.
862         """
863         return name.lower().replace('-', '_')
864
865     def __bool__(self):
866         return bool(self.name)
867
868
869 @install
870 class MetadataPathFinder(NullFinder, DistributionFinder):
871     """A degenerate finder for distribution packages on the file system.
872
873     This finder supplies only a find_distributions() method for versions
874     of Python that do not have a PathFinder find_distributions().
875     """
876
877     def find_distributions(self, context=DistributionFinder.Context()):
878         """
879         Find distributions.
880
881         Return an iterable of all Distribution instances capable of
882         loading the metadata for packages matching ``context.name``
883         (or all names if ``None`` indicated) along the paths in the list
884         of directories ``context.path``.
885         """
886         found = self._search_paths(context.name, context.path)
887         return map(PathDistribution, found)
888
889     @classmethod
890     def _search_paths(cls, name, paths):
891         """Find metadata directories in paths heuristically."""
892         prepared = Prepared(name)
893         return itertools.chain.from_iterable(
894             path.search(prepared) for path in map(FastPath, paths)
895         )
896
897     def invalidate_caches(cls):
898         FastPath.__new__.cache_clear()
899
900
901 class PathDistribution(Distribution):
902     def __init__(self, path: SimplePath):
903         """Construct a distribution.
904
905         :param path: SimplePath indicating the metadata directory.
906         """
907         self._path = path
908
909     def read_text(self, filename):
910         with suppress(
911             FileNotFoundError,
912             IsADirectoryError,
913             KeyError,
914             NotADirectoryError,
915             PermissionError,
916         ):
917             return self._path.joinpath(filename).read_text(encoding='utf-8')
918
919     read_text.__doc__ = Distribution.read_text.__doc__
920
921     def locate_file(self, path):
922         return self._path.parent / path
923
924     @property
925     def _normalized_name(self):
926         """
927         Performance optimization: where possible, resolve the
928         normalized name from the file system path.
929         """
930         stem = os.path.basename(str(self._path))
931         return self._name_from_stem(stem) or super()._normalized_name
932
933     def _name_from_stem(self, stem):
934         name, ext = os.path.splitext(stem)
935         if ext not in ('.dist-info', '.egg-info'):
936             return
937         name, sep, rest = stem.partition('-')
938         return name
939
940
941 def distribution(distribution_name):
942     """Get the ``Distribution`` instance for the named package.
943
944     :param distribution_name: The name of the distribution package as a string.
945     :return: A ``Distribution`` instance (or subclass thereof).
946     """
947     return Distribution.from_name(distribution_name)
948
949
950 def distributions(**kwargs):
951     """Get all ``Distribution`` instances in the current environment.
952
953     :return: An iterable of ``Distribution`` instances.
954     """
955     return Distribution.discover(**kwargs)
956
957
958 def metadata(distribution_name) -> _meta.PackageMetadata:
959     """Get the metadata for the named package.
960
961     :param distribution_name: The name of the distribution package to query.
962     :return: A PackageMetadata containing the parsed metadata.
963     """
964     return Distribution.from_name(distribution_name).metadata
965
966
967 def version(distribution_name):
968     """Get the version string for the named package.
969
970     :param distribution_name: The name of the distribution package to query.
971     :return: The version string for the package as defined in the package's
972         "Version" metadata key.
973     """
974     return distribution(distribution_name).version
975
976
977 def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
978     """Return EntryPoint objects for all installed packages.
979
980     Pass selection parameters (group or name) to filter the
981     result to entry points matching those properties (see
982     EntryPoints.select()).
983
984     For compatibility, returns ``SelectableGroups`` object unless
985     selection parameters are supplied. In the future, this function
986     will return ``EntryPoints`` instead of ``SelectableGroups``
987     even when no selection parameters are supplied.
988
989     For maximum future compatibility, pass selection parameters
990     or invoke ``.select`` with parameters on the result.
991
992     :return: EntryPoints or SelectableGroups for all installed packages.
993     """
994     norm_name = operator.attrgetter('_normalized_name')
995     unique = functools.partial(unique_everseen, key=norm_name)
996     eps = itertools.chain.from_iterable(
997         dist.entry_points for dist in unique(distributions())
998     )
999     return SelectableGroups.load(eps).select(**params)
1000
1001
1002 def files(distribution_name):
1003     """Return a list of files for the named package.
1004
1005     :param distribution_name: The name of the distribution package to query.
1006     :return: List of files composing the distribution.
1007     """
1008     return distribution(distribution_name).files
1009
1010
1011 def requires(distribution_name):
1012     """
1013     Return a list of requirements for the named package.
1014
1015     :return: An iterator of requirements, suitable for
1016         packaging.requirement.Requirement.
1017     """
1018     return distribution(distribution_name).requires
1019
1020
1021 def packages_distributions() -> Mapping[str, List[str]]:
1022     """
1023     Return a mapping of top-level packages to their
1024     distributions.
1025
1026     >>> import collections.abc
1027     >>> pkgs = packages_distributions()
1028     >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
1029     True
1030     """
1031     pkg_to_dist = collections.defaultdict(list)
1032     for dist in distributions():
1033         for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
1034             pkg_to_dist[pkg].append(dist.metadata['Name'])
1035     return dict(pkg_to_dist)
1036
1037
1038 def _top_level_declared(dist):
1039     return (dist.read_text('top_level.txt') or '').split()
1040
1041
1042 def _top_level_inferred(dist):
1043     return {
1044         f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
1045         for f in always_iterable(dist.files)
1046         if f.suffix == ".py"
1047     }