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