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