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