python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
pip/
_vendor/
distlib/
database.py
       1  # -*- coding: utf-8 -*-
       2  #
       3  # Copyright (C) 2012-2017 The Python Software Foundation.
       4  # See LICENSE.txt and CONTRIBUTORS.txt.
       5  #
       6  """PEP 376 implementation."""
       7  
       8  from __future__ import unicode_literals
       9  
      10  import base64
      11  import codecs
      12  import contextlib
      13  import hashlib
      14  import logging
      15  import os
      16  import posixpath
      17  import sys
      18  import zipimport
      19  
      20  from . import DistlibException, resources
      21  from .compat import StringIO
      22  from .version import get_scheme, UnsupportedVersionError
      23  from .metadata import (Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME,
      24                         LEGACY_METADATA_FILENAME)
      25  from .util import (parse_requirement, cached_property, parse_name_and_version,
      26                     read_exports, write_exports, CSVReader, CSVWriter)
      27  
      28  
      29  __all__ = ['Distribution', 'BaseInstalledDistribution',
      30             'InstalledDistribution', 'EggInfoDistribution',
      31             'DistributionPath']
      32  
      33  
      34  logger = logging.getLogger(__name__)
      35  
      36  EXPORTS_FILENAME = 'pydist-exports.json'
      37  COMMANDS_FILENAME = 'pydist-commands.json'
      38  
      39  DIST_FILES = ('INSTALLER', METADATA_FILENAME, 'RECORD', 'REQUESTED',
      40                'RESOURCES', EXPORTS_FILENAME, 'SHARED')
      41  
      42  DISTINFO_EXT = '.dist-info'
      43  
      44  
      45  class ESC[4;38;5;81m_Cache(ESC[4;38;5;149mobject):
      46      """
      47      A simple cache mapping names and .dist-info paths to distributions
      48      """
      49      def __init__(self):
      50          """
      51          Initialise an instance. There is normally one for each DistributionPath.
      52          """
      53          self.name = {}
      54          self.path = {}
      55          self.generated = False
      56  
      57      def clear(self):
      58          """
      59          Clear the cache, setting it to its initial state.
      60          """
      61          self.name.clear()
      62          self.path.clear()
      63          self.generated = False
      64  
      65      def add(self, dist):
      66          """
      67          Add a distribution to the cache.
      68          :param dist: The distribution to add.
      69          """
      70          if dist.path not in self.path:
      71              self.path[dist.path] = dist
      72              self.name.setdefault(dist.key, []).append(dist)
      73  
      74  
      75  class ESC[4;38;5;81mDistributionPath(ESC[4;38;5;149mobject):
      76      """
      77      Represents a set of distributions installed on a path (typically sys.path).
      78      """
      79      def __init__(self, path=None, include_egg=False):
      80          """
      81          Create an instance from a path, optionally including legacy (distutils/
      82          setuptools/distribute) distributions.
      83          :param path: The path to use, as a list of directories. If not specified,
      84                       sys.path is used.
      85          :param include_egg: If True, this instance will look for and return legacy
      86                              distributions as well as those based on PEP 376.
      87          """
      88          if path is None:
      89              path = sys.path
      90          self.path = path
      91          self._include_dist = True
      92          self._include_egg = include_egg
      93  
      94          self._cache = _Cache()
      95          self._cache_egg = _Cache()
      96          self._cache_enabled = True
      97          self._scheme = get_scheme('default')
      98  
      99      def _get_cache_enabled(self):
     100          return self._cache_enabled
     101  
     102      def _set_cache_enabled(self, value):
     103          self._cache_enabled = value
     104  
     105      cache_enabled = property(_get_cache_enabled, _set_cache_enabled)
     106  
     107      def clear_cache(self):
     108          """
     109          Clears the internal cache.
     110          """
     111          self._cache.clear()
     112          self._cache_egg.clear()
     113  
     114  
     115      def _yield_distributions(self):
     116          """
     117          Yield .dist-info and/or .egg(-info) distributions.
     118          """
     119          # We need to check if we've seen some resources already, because on
     120          # some Linux systems (e.g. some Debian/Ubuntu variants) there are
     121          # symlinks which alias other files in the environment.
     122          seen = set()
     123          for path in self.path:
     124              finder = resources.finder_for_path(path)
     125              if finder is None:
     126                  continue
     127              r = finder.find('')
     128              if not r or not r.is_container:
     129                  continue
     130              rset = sorted(r.resources)
     131              for entry in rset:
     132                  r = finder.find(entry)
     133                  if not r or r.path in seen:
     134                      continue
     135                  try:
     136                      if self._include_dist and entry.endswith(DISTINFO_EXT):
     137                          possible_filenames = [METADATA_FILENAME,
     138                                                WHEEL_METADATA_FILENAME,
     139                                                LEGACY_METADATA_FILENAME]
     140                          for metadata_filename in possible_filenames:
     141                              metadata_path = posixpath.join(entry, metadata_filename)
     142                              pydist = finder.find(metadata_path)
     143                              if pydist:
     144                                  break
     145                          else:
     146                              continue
     147  
     148                          with contextlib.closing(pydist.as_stream()) as stream:
     149                              metadata = Metadata(fileobj=stream, scheme='legacy')
     150                          logger.debug('Found %s', r.path)
     151                          seen.add(r.path)
     152                          yield new_dist_class(r.path, metadata=metadata,
     153                                               env=self)
     154                      elif self._include_egg and entry.endswith(('.egg-info',
     155                                                                '.egg')):
     156                          logger.debug('Found %s', r.path)
     157                          seen.add(r.path)
     158                          yield old_dist_class(r.path, self)
     159                  except Exception as e:
     160                      msg = 'Unable to read distribution at %s, perhaps due to bad metadata: %s'
     161                      logger.warning(msg, r.path, e)
     162                      import warnings
     163                      warnings.warn(msg % (r.path, e), stacklevel=2)
     164  
     165      def _generate_cache(self):
     166          """
     167          Scan the path for distributions and populate the cache with
     168          those that are found.
     169          """
     170          gen_dist = not self._cache.generated
     171          gen_egg = self._include_egg and not self._cache_egg.generated
     172          if gen_dist or gen_egg:
     173              for dist in self._yield_distributions():
     174                  if isinstance(dist, InstalledDistribution):
     175                      self._cache.add(dist)
     176                  else:
     177                      self._cache_egg.add(dist)
     178  
     179              if gen_dist:
     180                  self._cache.generated = True
     181              if gen_egg:
     182                  self._cache_egg.generated = True
     183  
     184      @classmethod
     185      def distinfo_dirname(cls, name, version):
     186          """
     187          The *name* and *version* parameters are converted into their
     188          filename-escaped form, i.e. any ``'-'`` characters are replaced
     189          with ``'_'`` other than the one in ``'dist-info'`` and the one
     190          separating the name from the version number.
     191  
     192          :parameter name: is converted to a standard distribution name by replacing
     193                           any runs of non- alphanumeric characters with a single
     194                           ``'-'``.
     195          :type name: string
     196          :parameter version: is converted to a standard version string. Spaces
     197                              become dots, and all other non-alphanumeric characters
     198                              (except dots) become dashes, with runs of multiple
     199                              dashes condensed to a single dash.
     200          :type version: string
     201          :returns: directory name
     202          :rtype: string"""
     203          name = name.replace('-', '_')
     204          return '-'.join([name, version]) + DISTINFO_EXT
     205  
     206      def get_distributions(self):
     207          """
     208          Provides an iterator that looks for distributions and returns
     209          :class:`InstalledDistribution` or
     210          :class:`EggInfoDistribution` instances for each one of them.
     211  
     212          :rtype: iterator of :class:`InstalledDistribution` and
     213                  :class:`EggInfoDistribution` instances
     214          """
     215          if not self._cache_enabled:
     216              for dist in self._yield_distributions():
     217                  yield dist
     218          else:
     219              self._generate_cache()
     220  
     221              for dist in self._cache.path.values():
     222                  yield dist
     223  
     224              if self._include_egg:
     225                  for dist in self._cache_egg.path.values():
     226                      yield dist
     227  
     228      def get_distribution(self, name):
     229          """
     230          Looks for a named distribution on the path.
     231  
     232          This function only returns the first result found, as no more than one
     233          value is expected. If nothing is found, ``None`` is returned.
     234  
     235          :rtype: :class:`InstalledDistribution`, :class:`EggInfoDistribution`
     236                  or ``None``
     237          """
     238          result = None
     239          name = name.lower()
     240          if not self._cache_enabled:
     241              for dist in self._yield_distributions():
     242                  if dist.key == name:
     243                      result = dist
     244                      break
     245          else:
     246              self._generate_cache()
     247  
     248              if name in self._cache.name:
     249                  result = self._cache.name[name][0]
     250              elif self._include_egg and name in self._cache_egg.name:
     251                  result = self._cache_egg.name[name][0]
     252          return result
     253  
     254      def provides_distribution(self, name, version=None):
     255          """
     256          Iterates over all distributions to find which distributions provide *name*.
     257          If a *version* is provided, it will be used to filter the results.
     258  
     259          This function only returns the first result found, since no more than
     260          one values are expected. If the directory is not found, returns ``None``.
     261  
     262          :parameter version: a version specifier that indicates the version
     263                              required, conforming to the format in ``PEP-345``
     264  
     265          :type name: string
     266          :type version: string
     267          """
     268          matcher = None
     269          if version is not None:
     270              try:
     271                  matcher = self._scheme.matcher('%s (%s)' % (name, version))
     272              except ValueError:
     273                  raise DistlibException('invalid name or version: %r, %r' %
     274                                        (name, version))
     275  
     276          for dist in self.get_distributions():
     277              # We hit a problem on Travis where enum34 was installed and doesn't
     278              # have a provides attribute ...
     279              if not hasattr(dist, 'provides'):
     280                  logger.debug('No "provides": %s', dist)
     281              else:
     282                  provided = dist.provides
     283  
     284                  for p in provided:
     285                      p_name, p_ver = parse_name_and_version(p)
     286                      if matcher is None:
     287                          if p_name == name:
     288                              yield dist
     289                              break
     290                      else:
     291                          if p_name == name and matcher.match(p_ver):
     292                              yield dist
     293                              break
     294  
     295      def get_file_path(self, name, relative_path):
     296          """
     297          Return the path to a resource file.
     298          """
     299          dist = self.get_distribution(name)
     300          if dist is None:
     301              raise LookupError('no distribution named %r found' % name)
     302          return dist.get_resource_path(relative_path)
     303  
     304      def get_exported_entries(self, category, name=None):
     305          """
     306          Return all of the exported entries in a particular category.
     307  
     308          :param category: The category to search for entries.
     309          :param name: If specified, only entries with that name are returned.
     310          """
     311          for dist in self.get_distributions():
     312              r = dist.exports
     313              if category in r:
     314                  d = r[category]
     315                  if name is not None:
     316                      if name in d:
     317                          yield d[name]
     318                  else:
     319                      for v in d.values():
     320                          yield v
     321  
     322  
     323  class ESC[4;38;5;81mDistribution(ESC[4;38;5;149mobject):
     324      """
     325      A base class for distributions, whether installed or from indexes.
     326      Either way, it must have some metadata, so that's all that's needed
     327      for construction.
     328      """
     329  
     330      build_time_dependency = False
     331      """
     332      Set to True if it's known to be only a build-time dependency (i.e.
     333      not needed after installation).
     334      """
     335  
     336      requested = False
     337      """A boolean that indicates whether the ``REQUESTED`` metadata file is
     338      present (in other words, whether the package was installed by user
     339      request or it was installed as a dependency)."""
     340  
     341      def __init__(self, metadata):
     342          """
     343          Initialise an instance.
     344          :param metadata: The instance of :class:`Metadata` describing this
     345          distribution.
     346          """
     347          self.metadata = metadata
     348          self.name = metadata.name
     349          self.key = self.name.lower()    # for case-insensitive comparisons
     350          self.version = metadata.version
     351          self.locator = None
     352          self.digest = None
     353          self.extras = None      # additional features requested
     354          self.context = None     # environment marker overrides
     355          self.download_urls = set()
     356          self.digests = {}
     357  
     358      @property
     359      def source_url(self):
     360          """
     361          The source archive download URL for this distribution.
     362          """
     363          return self.metadata.source_url
     364  
     365      download_url = source_url   # Backward compatibility
     366  
     367      @property
     368      def name_and_version(self):
     369          """
     370          A utility property which displays the name and version in parentheses.
     371          """
     372          return '%s (%s)' % (self.name, self.version)
     373  
     374      @property
     375      def provides(self):
     376          """
     377          A set of distribution names and versions provided by this distribution.
     378          :return: A set of "name (version)" strings.
     379          """
     380          plist = self.metadata.provides
     381          s = '%s (%s)' % (self.name, self.version)
     382          if s not in plist:
     383              plist.append(s)
     384          return plist
     385  
     386      def _get_requirements(self, req_attr):
     387          md = self.metadata
     388          reqts = getattr(md, req_attr)
     389          logger.debug('%s: got requirements %r from metadata: %r', self.name, req_attr,
     390                       reqts)
     391          return set(md.get_requirements(reqts, extras=self.extras,
     392                                         env=self.context))
     393  
     394      @property
     395      def run_requires(self):
     396          return self._get_requirements('run_requires')
     397  
     398      @property
     399      def meta_requires(self):
     400          return self._get_requirements('meta_requires')
     401  
     402      @property
     403      def build_requires(self):
     404          return self._get_requirements('build_requires')
     405  
     406      @property
     407      def test_requires(self):
     408          return self._get_requirements('test_requires')
     409  
     410      @property
     411      def dev_requires(self):
     412          return self._get_requirements('dev_requires')
     413  
     414      def matches_requirement(self, req):
     415          """
     416          Say if this instance matches (fulfills) a requirement.
     417          :param req: The requirement to match.
     418          :rtype req: str
     419          :return: True if it matches, else False.
     420          """
     421          # Requirement may contain extras - parse to lose those
     422          # from what's passed to the matcher
     423          r = parse_requirement(req)
     424          scheme = get_scheme(self.metadata.scheme)
     425          try:
     426              matcher = scheme.matcher(r.requirement)
     427          except UnsupportedVersionError:
     428              # XXX compat-mode if cannot read the version
     429              logger.warning('could not read version %r - using name only',
     430                             req)
     431              name = req.split()[0]
     432              matcher = scheme.matcher(name)
     433  
     434          name = matcher.key   # case-insensitive
     435  
     436          result = False
     437          for p in self.provides:
     438              p_name, p_ver = parse_name_and_version(p)
     439              if p_name != name:
     440                  continue
     441              try:
     442                  result = matcher.match(p_ver)
     443                  break
     444              except UnsupportedVersionError:
     445                  pass
     446          return result
     447  
     448      def __repr__(self):
     449          """
     450          Return a textual representation of this instance,
     451          """
     452          if self.source_url:
     453              suffix = ' [%s]' % self.source_url
     454          else:
     455              suffix = ''
     456          return '<Distribution %s (%s)%s>' % (self.name, self.version, suffix)
     457  
     458      def __eq__(self, other):
     459          """
     460          See if this distribution is the same as another.
     461          :param other: The distribution to compare with. To be equal to one
     462                        another. distributions must have the same type, name,
     463                        version and source_url.
     464          :return: True if it is the same, else False.
     465          """
     466          if type(other) is not type(self):
     467              result = False
     468          else:
     469              result = (self.name == other.name and
     470                        self.version == other.version and
     471                        self.source_url == other.source_url)
     472          return result
     473  
     474      def __hash__(self):
     475          """
     476          Compute hash in a way which matches the equality test.
     477          """
     478          return hash(self.name) + hash(self.version) + hash(self.source_url)
     479  
     480  
     481  class ESC[4;38;5;81mBaseInstalledDistribution(ESC[4;38;5;149mDistribution):
     482      """
     483      This is the base class for installed distributions (whether PEP 376 or
     484      legacy).
     485      """
     486  
     487      hasher = None
     488  
     489      def __init__(self, metadata, path, env=None):
     490          """
     491          Initialise an instance.
     492          :param metadata: An instance of :class:`Metadata` which describes the
     493                           distribution. This will normally have been initialised
     494                           from a metadata file in the ``path``.
     495          :param path:     The path of the ``.dist-info`` or ``.egg-info``
     496                           directory for the distribution.
     497          :param env:      This is normally the :class:`DistributionPath`
     498                           instance where this distribution was found.
     499          """
     500          super(BaseInstalledDistribution, self).__init__(metadata)
     501          self.path = path
     502          self.dist_path = env
     503  
     504      def get_hash(self, data, hasher=None):
     505          """
     506          Get the hash of some data, using a particular hash algorithm, if
     507          specified.
     508  
     509          :param data: The data to be hashed.
     510          :type data: bytes
     511          :param hasher: The name of a hash implementation, supported by hashlib,
     512                         or ``None``. Examples of valid values are ``'sha1'``,
     513                         ``'sha224'``, ``'sha384'``, '``sha256'``, ``'md5'`` and
     514                         ``'sha512'``. If no hasher is specified, the ``hasher``
     515                         attribute of the :class:`InstalledDistribution` instance
     516                         is used. If the hasher is determined to be ``None``, MD5
     517                         is used as the hashing algorithm.
     518          :returns: The hash of the data. If a hasher was explicitly specified,
     519                    the returned hash will be prefixed with the specified hasher
     520                    followed by '='.
     521          :rtype: str
     522          """
     523          if hasher is None:
     524              hasher = self.hasher
     525          if hasher is None:
     526              hasher = hashlib.md5
     527              prefix = ''
     528          else:
     529              hasher = getattr(hashlib, hasher)
     530              prefix = '%s=' % self.hasher
     531          digest = hasher(data).digest()
     532          digest = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
     533          return '%s%s' % (prefix, digest)
     534  
     535  
     536  class ESC[4;38;5;81mInstalledDistribution(ESC[4;38;5;149mBaseInstalledDistribution):
     537      """
     538      Created with the *path* of the ``.dist-info`` directory provided to the
     539      constructor. It reads the metadata contained in ``pydist.json`` when it is
     540      instantiated., or uses a passed in Metadata instance (useful for when
     541      dry-run mode is being used).
     542      """
     543  
     544      hasher = 'sha256'
     545  
     546      def __init__(self, path, metadata=None, env=None):
     547          self.modules = []
     548          self.finder = finder = resources.finder_for_path(path)
     549          if finder is None:
     550              raise ValueError('finder unavailable for %s' % path)
     551          if env and env._cache_enabled and path in env._cache.path:
     552              metadata = env._cache.path[path].metadata
     553          elif metadata is None:
     554              r = finder.find(METADATA_FILENAME)
     555              # Temporary - for Wheel 0.23 support
     556              if r is None:
     557                  r = finder.find(WHEEL_METADATA_FILENAME)
     558              # Temporary - for legacy support
     559              if r is None:
     560                  r = finder.find(LEGACY_METADATA_FILENAME)
     561              if r is None:
     562                  raise ValueError('no %s found in %s' % (METADATA_FILENAME,
     563                                                          path))
     564              with contextlib.closing(r.as_stream()) as stream:
     565                  metadata = Metadata(fileobj=stream, scheme='legacy')
     566  
     567          super(InstalledDistribution, self).__init__(metadata, path, env)
     568  
     569          if env and env._cache_enabled:
     570              env._cache.add(self)
     571  
     572          r = finder.find('REQUESTED')
     573          self.requested = r is not None
     574          p  = os.path.join(path, 'top_level.txt')
     575          if os.path.exists(p):
     576              with open(p, 'rb') as f:
     577                  data = f.read().decode('utf-8')
     578              self.modules = data.splitlines()
     579  
     580      def __repr__(self):
     581          return '<InstalledDistribution %r %s at %r>' % (
     582              self.name, self.version, self.path)
     583  
     584      def __str__(self):
     585          return "%s %s" % (self.name, self.version)
     586  
     587      def _get_records(self):
     588          """
     589          Get the list of installed files for the distribution
     590          :return: A list of tuples of path, hash and size. Note that hash and
     591                   size might be ``None`` for some entries. The path is exactly
     592                   as stored in the file (which is as in PEP 376).
     593          """
     594          results = []
     595          r = self.get_distinfo_resource('RECORD')
     596          with contextlib.closing(r.as_stream()) as stream:
     597              with CSVReader(stream=stream) as record_reader:
     598                  # Base location is parent dir of .dist-info dir
     599                  #base_location = os.path.dirname(self.path)
     600                  #base_location = os.path.abspath(base_location)
     601                  for row in record_reader:
     602                      missing = [None for i in range(len(row), 3)]
     603                      path, checksum, size = row + missing
     604                      #if not os.path.isabs(path):
     605                      #    path = path.replace('/', os.sep)
     606                      #    path = os.path.join(base_location, path)
     607                      results.append((path, checksum, size))
     608          return results
     609  
     610      @cached_property
     611      def exports(self):
     612          """
     613          Return the information exported by this distribution.
     614          :return: A dictionary of exports, mapping an export category to a dict
     615                   of :class:`ExportEntry` instances describing the individual
     616                   export entries, and keyed by name.
     617          """
     618          result = {}
     619          r = self.get_distinfo_resource(EXPORTS_FILENAME)
     620          if r:
     621              result = self.read_exports()
     622          return result
     623  
     624      def read_exports(self):
     625          """
     626          Read exports data from a file in .ini format.
     627  
     628          :return: A dictionary of exports, mapping an export category to a list
     629                   of :class:`ExportEntry` instances describing the individual
     630                   export entries.
     631          """
     632          result = {}
     633          r = self.get_distinfo_resource(EXPORTS_FILENAME)
     634          if r:
     635              with contextlib.closing(r.as_stream()) as stream:
     636                  result = read_exports(stream)
     637          return result
     638  
     639      def write_exports(self, exports):
     640          """
     641          Write a dictionary of exports to a file in .ini format.
     642          :param exports: A dictionary of exports, mapping an export category to
     643                          a list of :class:`ExportEntry` instances describing the
     644                          individual export entries.
     645          """
     646          rf = self.get_distinfo_file(EXPORTS_FILENAME)
     647          with open(rf, 'w') as f:
     648              write_exports(exports, f)
     649  
     650      def get_resource_path(self, relative_path):
     651          """
     652          NOTE: This API may change in the future.
     653  
     654          Return the absolute path to a resource file with the given relative
     655          path.
     656  
     657          :param relative_path: The path, relative to .dist-info, of the resource
     658                                of interest.
     659          :return: The absolute path where the resource is to be found.
     660          """
     661          r = self.get_distinfo_resource('RESOURCES')
     662          with contextlib.closing(r.as_stream()) as stream:
     663              with CSVReader(stream=stream) as resources_reader:
     664                  for relative, destination in resources_reader:
     665                      if relative == relative_path:
     666                          return destination
     667          raise KeyError('no resource file with relative path %r '
     668                         'is installed' % relative_path)
     669  
     670      def list_installed_files(self):
     671          """
     672          Iterates over the ``RECORD`` entries and returns a tuple
     673          ``(path, hash, size)`` for each line.
     674  
     675          :returns: iterator of (path, hash, size)
     676          """
     677          for result in self._get_records():
     678              yield result
     679  
     680      def write_installed_files(self, paths, prefix, dry_run=False):
     681          """
     682          Writes the ``RECORD`` file, using the ``paths`` iterable passed in. Any
     683          existing ``RECORD`` file is silently overwritten.
     684  
     685          prefix is used to determine when to write absolute paths.
     686          """
     687          prefix = os.path.join(prefix, '')
     688          base = os.path.dirname(self.path)
     689          base_under_prefix = base.startswith(prefix)
     690          base = os.path.join(base, '')
     691          record_path = self.get_distinfo_file('RECORD')
     692          logger.info('creating %s', record_path)
     693          if dry_run:
     694              return None
     695          with CSVWriter(record_path) as writer:
     696              for path in paths:
     697                  if os.path.isdir(path) or path.endswith(('.pyc', '.pyo')):
     698                      # do not put size and hash, as in PEP-376
     699                      hash_value = size = ''
     700                  else:
     701                      size = '%d' % os.path.getsize(path)
     702                      with open(path, 'rb') as fp:
     703                          hash_value = self.get_hash(fp.read())
     704                  if path.startswith(base) or (base_under_prefix and
     705                                               path.startswith(prefix)):
     706                      path = os.path.relpath(path, base)
     707                  writer.writerow((path, hash_value, size))
     708  
     709              # add the RECORD file itself
     710              if record_path.startswith(base):
     711                  record_path = os.path.relpath(record_path, base)
     712              writer.writerow((record_path, '', ''))
     713          return record_path
     714  
     715      def check_installed_files(self):
     716          """
     717          Checks that the hashes and sizes of the files in ``RECORD`` are
     718          matched by the files themselves. Returns a (possibly empty) list of
     719          mismatches. Each entry in the mismatch list will be a tuple consisting
     720          of the path, 'exists', 'size' or 'hash' according to what didn't match
     721          (existence is checked first, then size, then hash), the expected
     722          value and the actual value.
     723          """
     724          mismatches = []
     725          base = os.path.dirname(self.path)
     726          record_path = self.get_distinfo_file('RECORD')
     727          for path, hash_value, size in self.list_installed_files():
     728              if not os.path.isabs(path):
     729                  path = os.path.join(base, path)
     730              if path == record_path:
     731                  continue
     732              if not os.path.exists(path):
     733                  mismatches.append((path, 'exists', True, False))
     734              elif os.path.isfile(path):
     735                  actual_size = str(os.path.getsize(path))
     736                  if size and actual_size != size:
     737                      mismatches.append((path, 'size', size, actual_size))
     738                  elif hash_value:
     739                      if '=' in hash_value:
     740                          hasher = hash_value.split('=', 1)[0]
     741                      else:
     742                          hasher = None
     743  
     744                      with open(path, 'rb') as f:
     745                          actual_hash = self.get_hash(f.read(), hasher)
     746                          if actual_hash != hash_value:
     747                              mismatches.append((path, 'hash', hash_value, actual_hash))
     748          return mismatches
     749  
     750      @cached_property
     751      def shared_locations(self):
     752          """
     753          A dictionary of shared locations whose keys are in the set 'prefix',
     754          'purelib', 'platlib', 'scripts', 'headers', 'data' and 'namespace'.
     755          The corresponding value is the absolute path of that category for
     756          this distribution, and takes into account any paths selected by the
     757          user at installation time (e.g. via command-line arguments). In the
     758          case of the 'namespace' key, this would be a list of absolute paths
     759          for the roots of namespace packages in this distribution.
     760  
     761          The first time this property is accessed, the relevant information is
     762          read from the SHARED file in the .dist-info directory.
     763          """
     764          result = {}
     765          shared_path = os.path.join(self.path, 'SHARED')
     766          if os.path.isfile(shared_path):
     767              with codecs.open(shared_path, 'r', encoding='utf-8') as f:
     768                  lines = f.read().splitlines()
     769              for line in lines:
     770                  key, value = line.split('=', 1)
     771                  if key == 'namespace':
     772                      result.setdefault(key, []).append(value)
     773                  else:
     774                      result[key] = value
     775          return result
     776  
     777      def write_shared_locations(self, paths, dry_run=False):
     778          """
     779          Write shared location information to the SHARED file in .dist-info.
     780          :param paths: A dictionary as described in the documentation for
     781          :meth:`shared_locations`.
     782          :param dry_run: If True, the action is logged but no file is actually
     783                          written.
     784          :return: The path of the file written to.
     785          """
     786          shared_path = os.path.join(self.path, 'SHARED')
     787          logger.info('creating %s', shared_path)
     788          if dry_run:
     789              return None
     790          lines = []
     791          for key in ('prefix', 'lib', 'headers', 'scripts', 'data'):
     792              path = paths[key]
     793              if os.path.isdir(paths[key]):
     794                  lines.append('%s=%s' % (key,  path))
     795          for ns in paths.get('namespace', ()):
     796              lines.append('namespace=%s' % ns)
     797  
     798          with codecs.open(shared_path, 'w', encoding='utf-8') as f:
     799              f.write('\n'.join(lines))
     800          return shared_path
     801  
     802      def get_distinfo_resource(self, path):
     803          if path not in DIST_FILES:
     804              raise DistlibException('invalid path for a dist-info file: '
     805                                     '%r at %r' % (path, self.path))
     806          finder = resources.finder_for_path(self.path)
     807          if finder is None:
     808              raise DistlibException('Unable to get a finder for %s' % self.path)
     809          return finder.find(path)
     810  
     811      def get_distinfo_file(self, path):
     812          """
     813          Returns a path located under the ``.dist-info`` directory. Returns a
     814          string representing the path.
     815  
     816          :parameter path: a ``'/'``-separated path relative to the
     817                           ``.dist-info`` directory or an absolute path;
     818                           If *path* is an absolute path and doesn't start
     819                           with the ``.dist-info`` directory path,
     820                           a :class:`DistlibException` is raised
     821          :type path: str
     822          :rtype: str
     823          """
     824          # Check if it is an absolute path  # XXX use relpath, add tests
     825          if path.find(os.sep) >= 0:
     826              # it's an absolute path?
     827              distinfo_dirname, path = path.split(os.sep)[-2:]
     828              if distinfo_dirname != self.path.split(os.sep)[-1]:
     829                  raise DistlibException(
     830                      'dist-info file %r does not belong to the %r %s '
     831                      'distribution' % (path, self.name, self.version))
     832  
     833          # The file must be relative
     834          if path not in DIST_FILES:
     835              raise DistlibException('invalid path for a dist-info file: '
     836                                     '%r at %r' % (path, self.path))
     837  
     838          return os.path.join(self.path, path)
     839  
     840      def list_distinfo_files(self):
     841          """
     842          Iterates over the ``RECORD`` entries and returns paths for each line if
     843          the path is pointing to a file located in the ``.dist-info`` directory
     844          or one of its subdirectories.
     845  
     846          :returns: iterator of paths
     847          """
     848          base = os.path.dirname(self.path)
     849          for path, checksum, size in self._get_records():
     850              # XXX add separator or use real relpath algo
     851              if not os.path.isabs(path):
     852                  path = os.path.join(base, path)
     853              if path.startswith(self.path):
     854                  yield path
     855  
     856      def __eq__(self, other):
     857          return (isinstance(other, InstalledDistribution) and
     858                  self.path == other.path)
     859  
     860      # See http://docs.python.org/reference/datamodel#object.__hash__
     861      __hash__ = object.__hash__
     862  
     863  
     864  class ESC[4;38;5;81mEggInfoDistribution(ESC[4;38;5;149mBaseInstalledDistribution):
     865      """Created with the *path* of the ``.egg-info`` directory or file provided
     866      to the constructor. It reads the metadata contained in the file itself, or
     867      if the given path happens to be a directory, the metadata is read from the
     868      file ``PKG-INFO`` under that directory."""
     869  
     870      requested = True    # as we have no way of knowing, assume it was
     871      shared_locations = {}
     872  
     873      def __init__(self, path, env=None):
     874          def set_name_and_version(s, n, v):
     875              s.name = n
     876              s.key = n.lower()   # for case-insensitive comparisons
     877              s.version = v
     878  
     879          self.path = path
     880          self.dist_path = env
     881          if env and env._cache_enabled and path in env._cache_egg.path:
     882              metadata = env._cache_egg.path[path].metadata
     883              set_name_and_version(self, metadata.name, metadata.version)
     884          else:
     885              metadata = self._get_metadata(path)
     886  
     887              # Need to be set before caching
     888              set_name_and_version(self, metadata.name, metadata.version)
     889  
     890              if env and env._cache_enabled:
     891                  env._cache_egg.add(self)
     892          super(EggInfoDistribution, self).__init__(metadata, path, env)
     893  
     894      def _get_metadata(self, path):
     895          requires = None
     896  
     897          def parse_requires_data(data):
     898              """Create a list of dependencies from a requires.txt file.
     899  
     900              *data*: the contents of a setuptools-produced requires.txt file.
     901              """
     902              reqs = []
     903              lines = data.splitlines()
     904              for line in lines:
     905                  line = line.strip()
     906                  if line.startswith('['):
     907                      logger.warning('Unexpected line: quitting requirement scan: %r',
     908                                     line)
     909                      break
     910                  r = parse_requirement(line)
     911                  if not r:
     912                      logger.warning('Not recognised as a requirement: %r', line)
     913                      continue
     914                  if r.extras:
     915                      logger.warning('extra requirements in requires.txt are '
     916                                     'not supported')
     917                  if not r.constraints:
     918                      reqs.append(r.name)
     919                  else:
     920                      cons = ', '.join('%s%s' % c for c in r.constraints)
     921                      reqs.append('%s (%s)' % (r.name, cons))
     922              return reqs
     923  
     924          def parse_requires_path(req_path):
     925              """Create a list of dependencies from a requires.txt file.
     926  
     927              *req_path*: the path to a setuptools-produced requires.txt file.
     928              """
     929  
     930              reqs = []
     931              try:
     932                  with codecs.open(req_path, 'r', 'utf-8') as fp:
     933                      reqs = parse_requires_data(fp.read())
     934              except IOError:
     935                  pass
     936              return reqs
     937  
     938          tl_path = tl_data = None
     939          if path.endswith('.egg'):
     940              if os.path.isdir(path):
     941                  p = os.path.join(path, 'EGG-INFO')
     942                  meta_path = os.path.join(p, 'PKG-INFO')
     943                  metadata = Metadata(path=meta_path, scheme='legacy')
     944                  req_path = os.path.join(p, 'requires.txt')
     945                  tl_path = os.path.join(p, 'top_level.txt')
     946                  requires = parse_requires_path(req_path)
     947              else:
     948                  # FIXME handle the case where zipfile is not available
     949                  zipf = zipimport.zipimporter(path)
     950                  fileobj = StringIO(
     951                      zipf.get_data('EGG-INFO/PKG-INFO').decode('utf8'))
     952                  metadata = Metadata(fileobj=fileobj, scheme='legacy')
     953                  try:
     954                      data = zipf.get_data('EGG-INFO/requires.txt')
     955                      tl_data = zipf.get_data('EGG-INFO/top_level.txt').decode('utf-8')
     956                      requires = parse_requires_data(data.decode('utf-8'))
     957                  except IOError:
     958                      requires = None
     959          elif path.endswith('.egg-info'):
     960              if os.path.isdir(path):
     961                  req_path = os.path.join(path, 'requires.txt')
     962                  requires = parse_requires_path(req_path)
     963                  path = os.path.join(path, 'PKG-INFO')
     964                  tl_path = os.path.join(path, 'top_level.txt')
     965              metadata = Metadata(path=path, scheme='legacy')
     966          else:
     967              raise DistlibException('path must end with .egg-info or .egg, '
     968                                     'got %r' % path)
     969  
     970          if requires:
     971              metadata.add_requirements(requires)
     972          # look for top-level modules in top_level.txt, if present
     973          if tl_data is None:
     974              if tl_path is not None and os.path.exists(tl_path):
     975                  with open(tl_path, 'rb') as f:
     976                      tl_data = f.read().decode('utf-8')
     977          if not tl_data:
     978              tl_data = []
     979          else:
     980              tl_data = tl_data.splitlines()
     981          self.modules = tl_data
     982          return metadata
     983  
     984      def __repr__(self):
     985          return '<EggInfoDistribution %r %s at %r>' % (
     986              self.name, self.version, self.path)
     987  
     988      def __str__(self):
     989          return "%s %s" % (self.name, self.version)
     990  
     991      def check_installed_files(self):
     992          """
     993          Checks that the hashes and sizes of the files in ``RECORD`` are
     994          matched by the files themselves. Returns a (possibly empty) list of
     995          mismatches. Each entry in the mismatch list will be a tuple consisting
     996          of the path, 'exists', 'size' or 'hash' according to what didn't match
     997          (existence is checked first, then size, then hash), the expected
     998          value and the actual value.
     999          """
    1000          mismatches = []
    1001          record_path = os.path.join(self.path, 'installed-files.txt')
    1002          if os.path.exists(record_path):
    1003              for path, _, _ in self.list_installed_files():
    1004                  if path == record_path:
    1005                      continue
    1006                  if not os.path.exists(path):
    1007                      mismatches.append((path, 'exists', True, False))
    1008          return mismatches
    1009  
    1010      def list_installed_files(self):
    1011          """
    1012          Iterates over the ``installed-files.txt`` entries and returns a tuple
    1013          ``(path, hash, size)`` for each line.
    1014  
    1015          :returns: a list of (path, hash, size)
    1016          """
    1017  
    1018          def _md5(path):
    1019              f = open(path, 'rb')
    1020              try:
    1021                  content = f.read()
    1022              finally:
    1023                  f.close()
    1024              return hashlib.md5(content).hexdigest()
    1025  
    1026          def _size(path):
    1027              return os.stat(path).st_size
    1028  
    1029          record_path = os.path.join(self.path, 'installed-files.txt')
    1030          result = []
    1031          if os.path.exists(record_path):
    1032              with codecs.open(record_path, 'r', encoding='utf-8') as f:
    1033                  for line in f:
    1034                      line = line.strip()
    1035                      p = os.path.normpath(os.path.join(self.path, line))
    1036                      # "./" is present as a marker between installed files
    1037                      # and installation metadata files
    1038                      if not os.path.exists(p):
    1039                          logger.warning('Non-existent file: %s', p)
    1040                          if p.endswith(('.pyc', '.pyo')):
    1041                              continue
    1042                          #otherwise fall through and fail
    1043                      if not os.path.isdir(p):
    1044                          result.append((p, _md5(p), _size(p)))
    1045              result.append((record_path, None, None))
    1046          return result
    1047  
    1048      def list_distinfo_files(self, absolute=False):
    1049          """
    1050          Iterates over the ``installed-files.txt`` entries and returns paths for
    1051          each line if the path is pointing to a file located in the
    1052          ``.egg-info`` directory or one of its subdirectories.
    1053  
    1054          :parameter absolute: If *absolute* is ``True``, each returned path is
    1055                            transformed into a local absolute path. Otherwise the
    1056                            raw value from ``installed-files.txt`` is returned.
    1057          :type absolute: boolean
    1058          :returns: iterator of paths
    1059          """
    1060          record_path = os.path.join(self.path, 'installed-files.txt')
    1061          if os.path.exists(record_path):
    1062              skip = True
    1063              with codecs.open(record_path, 'r', encoding='utf-8') as f:
    1064                  for line in f:
    1065                      line = line.strip()
    1066                      if line == './':
    1067                          skip = False
    1068                          continue
    1069                      if not skip:
    1070                          p = os.path.normpath(os.path.join(self.path, line))
    1071                          if p.startswith(self.path):
    1072                              if absolute:
    1073                                  yield p
    1074                              else:
    1075                                  yield line
    1076  
    1077      def __eq__(self, other):
    1078          return (isinstance(other, EggInfoDistribution) and
    1079                  self.path == other.path)
    1080  
    1081      # See http://docs.python.org/reference/datamodel#object.__hash__
    1082      __hash__ = object.__hash__
    1083  
    1084  new_dist_class = InstalledDistribution
    1085  old_dist_class = EggInfoDistribution
    1086  
    1087  
    1088  class ESC[4;38;5;81mDependencyGraph(ESC[4;38;5;149mobject):
    1089      """
    1090      Represents a dependency graph between distributions.
    1091  
    1092      The dependency relationships are stored in an ``adjacency_list`` that maps
    1093      distributions to a list of ``(other, label)`` tuples where  ``other``
    1094      is a distribution and the edge is labeled with ``label`` (i.e. the version
    1095      specifier, if such was provided). Also, for more efficient traversal, for
    1096      every distribution ``x``, a list of predecessors is kept in
    1097      ``reverse_list[x]``. An edge from distribution ``a`` to
    1098      distribution ``b`` means that ``a`` depends on ``b``. If any missing
    1099      dependencies are found, they are stored in ``missing``, which is a
    1100      dictionary that maps distributions to a list of requirements that were not
    1101      provided by any other distributions.
    1102      """
    1103  
    1104      def __init__(self):
    1105          self.adjacency_list = {}
    1106          self.reverse_list = {}
    1107          self.missing = {}
    1108  
    1109      def add_distribution(self, distribution):
    1110          """Add the *distribution* to the graph.
    1111  
    1112          :type distribution: :class:`distutils2.database.InstalledDistribution`
    1113                              or :class:`distutils2.database.EggInfoDistribution`
    1114          """
    1115          self.adjacency_list[distribution] = []
    1116          self.reverse_list[distribution] = []
    1117          #self.missing[distribution] = []
    1118  
    1119      def add_edge(self, x, y, label=None):
    1120          """Add an edge from distribution *x* to distribution *y* with the given
    1121          *label*.
    1122  
    1123          :type x: :class:`distutils2.database.InstalledDistribution` or
    1124                   :class:`distutils2.database.EggInfoDistribution`
    1125          :type y: :class:`distutils2.database.InstalledDistribution` or
    1126                   :class:`distutils2.database.EggInfoDistribution`
    1127          :type label: ``str`` or ``None``
    1128          """
    1129          self.adjacency_list[x].append((y, label))
    1130          # multiple edges are allowed, so be careful
    1131          if x not in self.reverse_list[y]:
    1132              self.reverse_list[y].append(x)
    1133  
    1134      def add_missing(self, distribution, requirement):
    1135          """
    1136          Add a missing *requirement* for the given *distribution*.
    1137  
    1138          :type distribution: :class:`distutils2.database.InstalledDistribution`
    1139                              or :class:`distutils2.database.EggInfoDistribution`
    1140          :type requirement: ``str``
    1141          """
    1142          logger.debug('%s missing %r', distribution, requirement)
    1143          self.missing.setdefault(distribution, []).append(requirement)
    1144  
    1145      def _repr_dist(self, dist):
    1146          return '%s %s' % (dist.name, dist.version)
    1147  
    1148      def repr_node(self, dist, level=1):
    1149          """Prints only a subgraph"""
    1150          output = [self._repr_dist(dist)]
    1151          for other, label in self.adjacency_list[dist]:
    1152              dist = self._repr_dist(other)
    1153              if label is not None:
    1154                  dist = '%s [%s]' % (dist, label)
    1155              output.append('    ' * level + str(dist))
    1156              suboutput = self.repr_node(other, level + 1)
    1157              subs = suboutput.split('\n')
    1158              output.extend(subs[1:])
    1159          return '\n'.join(output)
    1160  
    1161      def to_dot(self, f, skip_disconnected=True):
    1162          """Writes a DOT output for the graph to the provided file *f*.
    1163  
    1164          If *skip_disconnected* is set to ``True``, then all distributions
    1165          that are not dependent on any other distribution are skipped.
    1166  
    1167          :type f: has to support ``file``-like operations
    1168          :type skip_disconnected: ``bool``
    1169          """
    1170          disconnected = []
    1171  
    1172          f.write("digraph dependencies {\n")
    1173          for dist, adjs in self.adjacency_list.items():
    1174              if len(adjs) == 0 and not skip_disconnected:
    1175                  disconnected.append(dist)
    1176              for other, label in adjs:
    1177                  if not label is None:
    1178                      f.write('"%s" -> "%s" [label="%s"]\n' %
    1179                              (dist.name, other.name, label))
    1180                  else:
    1181                      f.write('"%s" -> "%s"\n' % (dist.name, other.name))
    1182          if not skip_disconnected and len(disconnected) > 0:
    1183              f.write('subgraph disconnected {\n')
    1184              f.write('label = "Disconnected"\n')
    1185              f.write('bgcolor = red\n')
    1186  
    1187              for dist in disconnected:
    1188                  f.write('"%s"' % dist.name)
    1189                  f.write('\n')
    1190              f.write('}\n')
    1191          f.write('}\n')
    1192  
    1193      def topological_sort(self):
    1194          """
    1195          Perform a topological sort of the graph.
    1196          :return: A tuple, the first element of which is a topologically sorted
    1197                   list of distributions, and the second element of which is a
    1198                   list of distributions that cannot be sorted because they have
    1199                   circular dependencies and so form a cycle.
    1200          """
    1201          result = []
    1202          # Make a shallow copy of the adjacency list
    1203          alist = {}
    1204          for k, v in self.adjacency_list.items():
    1205              alist[k] = v[:]
    1206          while True:
    1207              # See what we can remove in this run
    1208              to_remove = []
    1209              for k, v in list(alist.items())[:]:
    1210                  if not v:
    1211                      to_remove.append(k)
    1212                      del alist[k]
    1213              if not to_remove:
    1214                  # What's left in alist (if anything) is a cycle.
    1215                  break
    1216              # Remove from the adjacency list of others
    1217              for k, v in alist.items():
    1218                  alist[k] = [(d, r) for d, r in v if d not in to_remove]
    1219              logger.debug('Moving to result: %s',
    1220                           ['%s (%s)' % (d.name, d.version) for d in to_remove])
    1221              result.extend(to_remove)
    1222          return result, list(alist.keys())
    1223  
    1224      def __repr__(self):
    1225          """Representation of the graph"""
    1226          output = []
    1227          for dist, adjs in self.adjacency_list.items():
    1228              output.append(self.repr_node(dist))
    1229          return '\n'.join(output)
    1230  
    1231  
    1232  def make_graph(dists, scheme='default'):
    1233      """Makes a dependency graph from the given distributions.
    1234  
    1235      :parameter dists: a list of distributions
    1236      :type dists: list of :class:`distutils2.database.InstalledDistribution` and
    1237                   :class:`distutils2.database.EggInfoDistribution` instances
    1238      :rtype: a :class:`DependencyGraph` instance
    1239      """
    1240      scheme = get_scheme(scheme)
    1241      graph = DependencyGraph()
    1242      provided = {}  # maps names to lists of (version, dist) tuples
    1243  
    1244      # first, build the graph and find out what's provided
    1245      for dist in dists:
    1246          graph.add_distribution(dist)
    1247  
    1248          for p in dist.provides:
    1249              name, version = parse_name_and_version(p)
    1250              logger.debug('Add to provided: %s, %s, %s', name, version, dist)
    1251              provided.setdefault(name, []).append((version, dist))
    1252  
    1253      # now make the edges
    1254      for dist in dists:
    1255          requires = (dist.run_requires | dist.meta_requires |
    1256                      dist.build_requires | dist.dev_requires)
    1257          for req in requires:
    1258              try:
    1259                  matcher = scheme.matcher(req)
    1260              except UnsupportedVersionError:
    1261                  # XXX compat-mode if cannot read the version
    1262                  logger.warning('could not read version %r - using name only',
    1263                                 req)
    1264                  name = req.split()[0]
    1265                  matcher = scheme.matcher(name)
    1266  
    1267              name = matcher.key   # case-insensitive
    1268  
    1269              matched = False
    1270              if name in provided:
    1271                  for version, provider in provided[name]:
    1272                      try:
    1273                          match = matcher.match(version)
    1274                      except UnsupportedVersionError:
    1275                          match = False
    1276  
    1277                      if match:
    1278                          graph.add_edge(dist, provider, req)
    1279                          matched = True
    1280                          break
    1281              if not matched:
    1282                  graph.add_missing(dist, req)
    1283      return graph
    1284  
    1285  
    1286  def get_dependent_dists(dists, dist):
    1287      """Recursively generate a list of distributions from *dists* that are
    1288      dependent on *dist*.
    1289  
    1290      :param dists: a list of distributions
    1291      :param dist: a distribution, member of *dists* for which we are interested
    1292      """
    1293      if dist not in dists:
    1294          raise DistlibException('given distribution %r is not a member '
    1295                                 'of the list' % dist.name)
    1296      graph = make_graph(dists)
    1297  
    1298      dep = [dist]  # dependent distributions
    1299      todo = graph.reverse_list[dist]  # list of nodes we should inspect
    1300  
    1301      while todo:
    1302          d = todo.pop()
    1303          dep.append(d)
    1304          for succ in graph.reverse_list[d]:
    1305              if succ not in dep:
    1306                  todo.append(succ)
    1307  
    1308      dep.pop(0)  # remove dist from dep, was there to prevent infinite loops
    1309      return dep
    1310  
    1311  
    1312  def get_required_dists(dists, dist):
    1313      """Recursively generate a list of distributions from *dists* that are
    1314      required by *dist*.
    1315  
    1316      :param dists: a list of distributions
    1317      :param dist: a distribution, member of *dists* for which we are interested
    1318                   in finding the dependencies.
    1319      """
    1320      if dist not in dists:
    1321          raise DistlibException('given distribution %r is not a member '
    1322                                 'of the list' % dist.name)
    1323      graph = make_graph(dists)
    1324  
    1325      req = set()  # required distributions
    1326      todo = graph.adjacency_list[dist]  # list of nodes we should inspect
    1327      seen = set(t[0] for t in todo) # already added to todo
    1328  
    1329      while todo:
    1330          d = todo.pop()[0]
    1331          req.add(d)
    1332          pred_list = graph.adjacency_list[d]
    1333          for pred in pred_list:
    1334              d = pred[0]
    1335              if d not in req and d not in seen:
    1336                  seen.add(d)
    1337                  todo.append(pred)
    1338      return req
    1339  
    1340  
    1341  def make_dist(name, version, **kwargs):
    1342      """
    1343      A convenience method for making a dist given just a name and version.
    1344      """
    1345      summary = kwargs.pop('summary', 'Placeholder for summary')
    1346      md = Metadata(**kwargs)
    1347      md.name = name
    1348      md.version = version
    1349      md.summary = summary or 'Placeholder for summary'
    1350      return Distribution(md)