python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
pip/
_vendor/
distlib/
metadata.py
       1  # -*- coding: utf-8 -*-
       2  #
       3  # Copyright (C) 2012 The Python Software Foundation.
       4  # See LICENSE.txt and CONTRIBUTORS.txt.
       5  #
       6  """Implementation of the Metadata for Python packages PEPs.
       7  
       8  Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1 and 2.2).
       9  """
      10  from __future__ import unicode_literals
      11  
      12  import codecs
      13  from email import message_from_file
      14  import json
      15  import logging
      16  import re
      17  
      18  
      19  from . import DistlibException, __version__
      20  from .compat import StringIO, string_types, text_type
      21  from .markers import interpret
      22  from .util import extract_by_key, get_extras
      23  from .version import get_scheme, PEP440_VERSION_RE
      24  
      25  logger = logging.getLogger(__name__)
      26  
      27  
      28  class ESC[4;38;5;81mMetadataMissingError(ESC[4;38;5;149mDistlibException):
      29      """A required metadata is missing"""
      30  
      31  
      32  class ESC[4;38;5;81mMetadataConflictError(ESC[4;38;5;149mDistlibException):
      33      """Attempt to read or write metadata fields that are conflictual."""
      34  
      35  
      36  class ESC[4;38;5;81mMetadataUnrecognizedVersionError(ESC[4;38;5;149mDistlibException):
      37      """Unknown metadata version number."""
      38  
      39  
      40  class ESC[4;38;5;81mMetadataInvalidError(ESC[4;38;5;149mDistlibException):
      41      """A metadata value is invalid"""
      42  
      43  # public API of this module
      44  __all__ = ['Metadata', 'PKG_INFO_ENCODING', 'PKG_INFO_PREFERRED_VERSION']
      45  
      46  # Encoding used for the PKG-INFO files
      47  PKG_INFO_ENCODING = 'utf-8'
      48  
      49  # preferred version. Hopefully will be changed
      50  # to 1.2 once PEP 345 is supported everywhere
      51  PKG_INFO_PREFERRED_VERSION = '1.1'
      52  
      53  _LINE_PREFIX_1_2 = re.compile('\n       \\|')
      54  _LINE_PREFIX_PRE_1_2 = re.compile('\n        ')
      55  _241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
      56                 'Summary', 'Description',
      57                 'Keywords', 'Home-page', 'Author', 'Author-email',
      58                 'License')
      59  
      60  _314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
      61                 'Supported-Platform', 'Summary', 'Description',
      62                 'Keywords', 'Home-page', 'Author', 'Author-email',
      63                 'License', 'Classifier', 'Download-URL', 'Obsoletes',
      64                 'Provides', 'Requires')
      65  
      66  _314_MARKERS = ('Obsoletes', 'Provides', 'Requires', 'Classifier',
      67                  'Download-URL')
      68  
      69  _345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
      70                 'Supported-Platform', 'Summary', 'Description',
      71                 'Keywords', 'Home-page', 'Author', 'Author-email',
      72                 'Maintainer', 'Maintainer-email', 'License',
      73                 'Classifier', 'Download-URL', 'Obsoletes-Dist',
      74                 'Project-URL', 'Provides-Dist', 'Requires-Dist',
      75                 'Requires-Python', 'Requires-External')
      76  
      77  _345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python',
      78                  'Obsoletes-Dist', 'Requires-External', 'Maintainer',
      79                  'Maintainer-email', 'Project-URL')
      80  
      81  _426_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
      82                 'Supported-Platform', 'Summary', 'Description',
      83                 'Keywords', 'Home-page', 'Author', 'Author-email',
      84                 'Maintainer', 'Maintainer-email', 'License',
      85                 'Classifier', 'Download-URL', 'Obsoletes-Dist',
      86                 'Project-URL', 'Provides-Dist', 'Requires-Dist',
      87                 'Requires-Python', 'Requires-External', 'Private-Version',
      88                 'Obsoleted-By', 'Setup-Requires-Dist', 'Extension',
      89                 'Provides-Extra')
      90  
      91  _426_MARKERS = ('Private-Version', 'Provides-Extra', 'Obsoleted-By',
      92                  'Setup-Requires-Dist', 'Extension')
      93  
      94  # See issue #106: Sometimes 'Requires' and 'Provides' occur wrongly in
      95  # the metadata. Include them in the tuple literal below to allow them
      96  # (for now).
      97  # Ditto for Obsoletes - see issue #140.
      98  _566_FIELDS = _426_FIELDS + ('Description-Content-Type',
      99                               'Requires', 'Provides', 'Obsoletes')
     100  
     101  _566_MARKERS = ('Description-Content-Type',)
     102  
     103  _643_MARKERS = ('Dynamic', 'License-File')
     104  
     105  _643_FIELDS = _566_FIELDS + _643_MARKERS
     106  
     107  _ALL_FIELDS = set()
     108  _ALL_FIELDS.update(_241_FIELDS)
     109  _ALL_FIELDS.update(_314_FIELDS)
     110  _ALL_FIELDS.update(_345_FIELDS)
     111  _ALL_FIELDS.update(_426_FIELDS)
     112  _ALL_FIELDS.update(_566_FIELDS)
     113  _ALL_FIELDS.update(_643_FIELDS)
     114  
     115  EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
     116  
     117  
     118  def _version2fieldlist(version):
     119      if version == '1.0':
     120          return _241_FIELDS
     121      elif version == '1.1':
     122          return _314_FIELDS
     123      elif version == '1.2':
     124          return _345_FIELDS
     125      elif version in ('1.3', '2.1'):
     126          # avoid adding field names if already there
     127          return _345_FIELDS + tuple(f for f in _566_FIELDS if f not in _345_FIELDS)
     128      elif version == '2.0':
     129          raise ValueError('Metadata 2.0 is withdrawn and not supported')
     130          # return _426_FIELDS
     131      elif version == '2.2':
     132          return _643_FIELDS
     133      raise MetadataUnrecognizedVersionError(version)
     134  
     135  
     136  def _best_version(fields):
     137      """Detect the best version depending on the fields used."""
     138      def _has_marker(keys, markers):
     139          for marker in markers:
     140              if marker in keys:
     141                  return True
     142          return False
     143  
     144      keys = []
     145      for key, value in fields.items():
     146          if value in ([], 'UNKNOWN', None):
     147              continue
     148          keys.append(key)
     149  
     150      possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2']  # 2.0 removed
     151  
     152      # first let's try to see if a field is not part of one of the version
     153      for key in keys:
     154          if key not in _241_FIELDS and '1.0' in possible_versions:
     155              possible_versions.remove('1.0')
     156              logger.debug('Removed 1.0 due to %s', key)
     157          if key not in _314_FIELDS and '1.1' in possible_versions:
     158              possible_versions.remove('1.1')
     159              logger.debug('Removed 1.1 due to %s', key)
     160          if key not in _345_FIELDS and '1.2' in possible_versions:
     161              possible_versions.remove('1.2')
     162              logger.debug('Removed 1.2 due to %s', key)
     163          if key not in _566_FIELDS and '1.3' in possible_versions:
     164              possible_versions.remove('1.3')
     165              logger.debug('Removed 1.3 due to %s', key)
     166          if key not in _566_FIELDS and '2.1' in possible_versions:
     167              if key != 'Description':  # In 2.1, description allowed after headers
     168                  possible_versions.remove('2.1')
     169                  logger.debug('Removed 2.1 due to %s', key)
     170          if key not in _643_FIELDS and '2.2' in possible_versions:
     171              possible_versions.remove('2.2')
     172              logger.debug('Removed 2.2 due to %s', key)
     173          # if key not in _426_FIELDS and '2.0' in possible_versions:
     174              # possible_versions.remove('2.0')
     175              # logger.debug('Removed 2.0 due to %s', key)
     176  
     177      # possible_version contains qualified versions
     178      if len(possible_versions) == 1:
     179          return possible_versions[0]   # found !
     180      elif len(possible_versions) == 0:
     181          logger.debug('Out of options - unknown metadata set: %s', fields)
     182          raise MetadataConflictError('Unknown metadata set')
     183  
     184      # let's see if one unique marker is found
     185      is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS)
     186      is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS)
     187      is_2_1 = '2.1' in possible_versions and _has_marker(keys, _566_MARKERS)
     188      # is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
     189      is_2_2 = '2.2' in possible_versions and _has_marker(keys, _643_MARKERS)
     190      if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_2) > 1:
     191          raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2 fields')
     192  
     193      # we have the choice, 1.0, or 1.2, 2.1 or 2.2
     194      #   - 1.0 has a broken Summary field but works with all tools
     195      #   - 1.1 is to avoid
     196      #   - 1.2 fixes Summary but has little adoption
     197      #   - 2.1 adds more features
     198      #   - 2.2 is the latest
     199      if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_2:
     200          # we couldn't find any specific marker
     201          if PKG_INFO_PREFERRED_VERSION in possible_versions:
     202              return PKG_INFO_PREFERRED_VERSION
     203      if is_1_1:
     204          return '1.1'
     205      if is_1_2:
     206          return '1.2'
     207      if is_2_1:
     208          return '2.1'
     209      # if is_2_2:
     210          # return '2.2'
     211  
     212      return '2.2'
     213  
     214  # This follows the rules about transforming keys as described in
     215  # https://www.python.org/dev/peps/pep-0566/#id17
     216  _ATTR2FIELD = {
     217      name.lower().replace("-", "_"): name for name in _ALL_FIELDS
     218  }
     219  _FIELD2ATTR = {field: attr for attr, field in _ATTR2FIELD.items()}
     220  
     221  _PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist')
     222  _VERSIONS_FIELDS = ('Requires-Python',)
     223  _VERSION_FIELDS = ('Version',)
     224  _LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes',
     225                 'Requires', 'Provides', 'Obsoletes-Dist',
     226                 'Provides-Dist', 'Requires-Dist', 'Requires-External',
     227                 'Project-URL', 'Supported-Platform', 'Setup-Requires-Dist',
     228                 'Provides-Extra', 'Extension', 'License-File')
     229  _LISTTUPLEFIELDS = ('Project-URL',)
     230  
     231  _ELEMENTSFIELD = ('Keywords',)
     232  
     233  _UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description')
     234  
     235  _MISSING = object()
     236  
     237  _FILESAFE = re.compile('[^A-Za-z0-9.]+')
     238  
     239  
     240  def _get_name_and_version(name, version, for_filename=False):
     241      """Return the distribution name with version.
     242  
     243      If for_filename is true, return a filename-escaped form."""
     244      if for_filename:
     245          # For both name and version any runs of non-alphanumeric or '.'
     246          # characters are replaced with a single '-'.  Additionally any
     247          # spaces in the version string become '.'
     248          name = _FILESAFE.sub('-', name)
     249          version = _FILESAFE.sub('-', version.replace(' ', '.'))
     250      return '%s-%s' % (name, version)
     251  
     252  
     253  class ESC[4;38;5;81mLegacyMetadata(ESC[4;38;5;149mobject):
     254      """The legacy metadata of a release.
     255  
     256      Supports versions 1.0, 1.1, 1.2, 2.0 and 1.3/2.1 (auto-detected). You can
     257      instantiate the class with one of these arguments (or none):
     258      - *path*, the path to a metadata file
     259      - *fileobj* give a file-like object with metadata as content
     260      - *mapping* is a dict-like object
     261      - *scheme* is a version scheme name
     262      """
     263      # TODO document the mapping API and UNKNOWN default key
     264  
     265      def __init__(self, path=None, fileobj=None, mapping=None,
     266                   scheme='default'):
     267          if [path, fileobj, mapping].count(None) < 2:
     268              raise TypeError('path, fileobj and mapping are exclusive')
     269          self._fields = {}
     270          self.requires_files = []
     271          self._dependencies = None
     272          self.scheme = scheme
     273          if path is not None:
     274              self.read(path)
     275          elif fileobj is not None:
     276              self.read_file(fileobj)
     277          elif mapping is not None:
     278              self.update(mapping)
     279              self.set_metadata_version()
     280  
     281      def set_metadata_version(self):
     282          self._fields['Metadata-Version'] = _best_version(self._fields)
     283  
     284      def _write_field(self, fileobj, name, value):
     285          fileobj.write('%s: %s\n' % (name, value))
     286  
     287      def __getitem__(self, name):
     288          return self.get(name)
     289  
     290      def __setitem__(self, name, value):
     291          return self.set(name, value)
     292  
     293      def __delitem__(self, name):
     294          field_name = self._convert_name(name)
     295          try:
     296              del self._fields[field_name]
     297          except KeyError:
     298              raise KeyError(name)
     299  
     300      def __contains__(self, name):
     301          return (name in self._fields or
     302                  self._convert_name(name) in self._fields)
     303  
     304      def _convert_name(self, name):
     305          if name in _ALL_FIELDS:
     306              return name
     307          name = name.replace('-', '_').lower()
     308          return _ATTR2FIELD.get(name, name)
     309  
     310      def _default_value(self, name):
     311          if name in _LISTFIELDS or name in _ELEMENTSFIELD:
     312              return []
     313          return 'UNKNOWN'
     314  
     315      def _remove_line_prefix(self, value):
     316          if self.metadata_version in ('1.0', '1.1'):
     317              return _LINE_PREFIX_PRE_1_2.sub('\n', value)
     318          else:
     319              return _LINE_PREFIX_1_2.sub('\n', value)
     320  
     321      def __getattr__(self, name):
     322          if name in _ATTR2FIELD:
     323              return self[name]
     324          raise AttributeError(name)
     325  
     326      #
     327      # Public API
     328      #
     329  
     330  #    dependencies = property(_get_dependencies, _set_dependencies)
     331  
     332      def get_fullname(self, filesafe=False):
     333          """Return the distribution name with version.
     334  
     335          If filesafe is true, return a filename-escaped form."""
     336          return _get_name_and_version(self['Name'], self['Version'], filesafe)
     337  
     338      def is_field(self, name):
     339          """return True if name is a valid metadata key"""
     340          name = self._convert_name(name)
     341          return name in _ALL_FIELDS
     342  
     343      def is_multi_field(self, name):
     344          name = self._convert_name(name)
     345          return name in _LISTFIELDS
     346  
     347      def read(self, filepath):
     348          """Read the metadata values from a file path."""
     349          fp = codecs.open(filepath, 'r', encoding='utf-8')
     350          try:
     351              self.read_file(fp)
     352          finally:
     353              fp.close()
     354  
     355      def read_file(self, fileob):
     356          """Read the metadata values from a file object."""
     357          msg = message_from_file(fileob)
     358          self._fields['Metadata-Version'] = msg['metadata-version']
     359  
     360          # When reading, get all the fields we can
     361          for field in _ALL_FIELDS:
     362              if field not in msg:
     363                  continue
     364              if field in _LISTFIELDS:
     365                  # we can have multiple lines
     366                  values = msg.get_all(field)
     367                  if field in _LISTTUPLEFIELDS and values is not None:
     368                      values = [tuple(value.split(',')) for value in values]
     369                  self.set(field, values)
     370              else:
     371                  # single line
     372                  value = msg[field]
     373                  if value is not None and value != 'UNKNOWN':
     374                      self.set(field, value)
     375  
     376          # PEP 566 specifies that the body be used for the description, if
     377          # available
     378          body = msg.get_payload()
     379          self["Description"] = body if body else self["Description"]
     380          # logger.debug('Attempting to set metadata for %s', self)
     381          # self.set_metadata_version()
     382  
     383      def write(self, filepath, skip_unknown=False):
     384          """Write the metadata fields to filepath."""
     385          fp = codecs.open(filepath, 'w', encoding='utf-8')
     386          try:
     387              self.write_file(fp, skip_unknown)
     388          finally:
     389              fp.close()
     390  
     391      def write_file(self, fileobject, skip_unknown=False):
     392          """Write the PKG-INFO format data to a file object."""
     393          self.set_metadata_version()
     394  
     395          for field in _version2fieldlist(self['Metadata-Version']):
     396              values = self.get(field)
     397              if skip_unknown and values in ('UNKNOWN', [], ['UNKNOWN']):
     398                  continue
     399              if field in _ELEMENTSFIELD:
     400                  self._write_field(fileobject, field, ','.join(values))
     401                  continue
     402              if field not in _LISTFIELDS:
     403                  if field == 'Description':
     404                      if self.metadata_version in ('1.0', '1.1'):
     405                          values = values.replace('\n', '\n        ')
     406                      else:
     407                          values = values.replace('\n', '\n       |')
     408                  values = [values]
     409  
     410              if field in _LISTTUPLEFIELDS:
     411                  values = [','.join(value) for value in values]
     412  
     413              for value in values:
     414                  self._write_field(fileobject, field, value)
     415  
     416      def update(self, other=None, **kwargs):
     417          """Set metadata values from the given iterable `other` and kwargs.
     418  
     419          Behavior is like `dict.update`: If `other` has a ``keys`` method,
     420          they are looped over and ``self[key]`` is assigned ``other[key]``.
     421          Else, ``other`` is an iterable of ``(key, value)`` iterables.
     422  
     423          Keys that don't match a metadata field or that have an empty value are
     424          dropped.
     425          """
     426          def _set(key, value):
     427              if key in _ATTR2FIELD and value:
     428                  self.set(self._convert_name(key), value)
     429  
     430          if not other:
     431              # other is None or empty container
     432              pass
     433          elif hasattr(other, 'keys'):
     434              for k in other.keys():
     435                  _set(k, other[k])
     436          else:
     437              for k, v in other:
     438                  _set(k, v)
     439  
     440          if kwargs:
     441              for k, v in kwargs.items():
     442                  _set(k, v)
     443  
     444      def set(self, name, value):
     445          """Control then set a metadata field."""
     446          name = self._convert_name(name)
     447  
     448          if ((name in _ELEMENTSFIELD or name == 'Platform') and
     449              not isinstance(value, (list, tuple))):
     450              if isinstance(value, string_types):
     451                  value = [v.strip() for v in value.split(',')]
     452              else:
     453                  value = []
     454          elif (name in _LISTFIELDS and
     455                not isinstance(value, (list, tuple))):
     456              if isinstance(value, string_types):
     457                  value = [value]
     458              else:
     459                  value = []
     460  
     461          if logger.isEnabledFor(logging.WARNING):
     462              project_name = self['Name']
     463  
     464              scheme = get_scheme(self.scheme)
     465              if name in _PREDICATE_FIELDS and value is not None:
     466                  for v in value:
     467                      # check that the values are valid
     468                      if not scheme.is_valid_matcher(v.split(';')[0]):
     469                          logger.warning(
     470                              "'%s': '%s' is not valid (field '%s')",
     471                              project_name, v, name)
     472              # FIXME this rejects UNKNOWN, is that right?
     473              elif name in _VERSIONS_FIELDS and value is not None:
     474                  if not scheme.is_valid_constraint_list(value):
     475                      logger.warning("'%s': '%s' is not a valid version (field '%s')",
     476                                     project_name, value, name)
     477              elif name in _VERSION_FIELDS and value is not None:
     478                  if not scheme.is_valid_version(value):
     479                      logger.warning("'%s': '%s' is not a valid version (field '%s')",
     480                                     project_name, value, name)
     481  
     482          if name in _UNICODEFIELDS:
     483              if name == 'Description':
     484                  value = self._remove_line_prefix(value)
     485  
     486          self._fields[name] = value
     487  
     488      def get(self, name, default=_MISSING):
     489          """Get a metadata field."""
     490          name = self._convert_name(name)
     491          if name not in self._fields:
     492              if default is _MISSING:
     493                  default = self._default_value(name)
     494              return default
     495          if name in _UNICODEFIELDS:
     496              value = self._fields[name]
     497              return value
     498          elif name in _LISTFIELDS:
     499              value = self._fields[name]
     500              if value is None:
     501                  return []
     502              res = []
     503              for val in value:
     504                  if name not in _LISTTUPLEFIELDS:
     505                      res.append(val)
     506                  else:
     507                      # That's for Project-URL
     508                      res.append((val[0], val[1]))
     509              return res
     510  
     511          elif name in _ELEMENTSFIELD:
     512              value = self._fields[name]
     513              if isinstance(value, string_types):
     514                  return value.split(',')
     515          return self._fields[name]
     516  
     517      def check(self, strict=False):
     518          """Check if the metadata is compliant. If strict is True then raise if
     519          no Name or Version are provided"""
     520          self.set_metadata_version()
     521  
     522          # XXX should check the versions (if the file was loaded)
     523          missing, warnings = [], []
     524  
     525          for attr in ('Name', 'Version'):  # required by PEP 345
     526              if attr not in self:
     527                  missing.append(attr)
     528  
     529          if strict and missing != []:
     530              msg = 'missing required metadata: %s' % ', '.join(missing)
     531              raise MetadataMissingError(msg)
     532  
     533          for attr in ('Home-page', 'Author'):
     534              if attr not in self:
     535                  missing.append(attr)
     536  
     537          # checking metadata 1.2 (XXX needs to check 1.1, 1.0)
     538          if self['Metadata-Version'] != '1.2':
     539              return missing, warnings
     540  
     541          scheme = get_scheme(self.scheme)
     542  
     543          def are_valid_constraints(value):
     544              for v in value:
     545                  if not scheme.is_valid_matcher(v.split(';')[0]):
     546                      return False
     547              return True
     548  
     549          for fields, controller in ((_PREDICATE_FIELDS, are_valid_constraints),
     550                                     (_VERSIONS_FIELDS,
     551                                      scheme.is_valid_constraint_list),
     552                                     (_VERSION_FIELDS,
     553                                      scheme.is_valid_version)):
     554              for field in fields:
     555                  value = self.get(field, None)
     556                  if value is not None and not controller(value):
     557                      warnings.append("Wrong value for '%s': %s" % (field, value))
     558  
     559          return missing, warnings
     560  
     561      def todict(self, skip_missing=False):
     562          """Return fields as a dict.
     563  
     564          Field names will be converted to use the underscore-lowercase style
     565          instead of hyphen-mixed case (i.e. home_page instead of Home-page).
     566          This is as per https://www.python.org/dev/peps/pep-0566/#id17.
     567          """
     568          self.set_metadata_version()
     569  
     570          fields = _version2fieldlist(self['Metadata-Version'])
     571  
     572          data = {}
     573  
     574          for field_name in fields:
     575              if not skip_missing or field_name in self._fields:
     576                  key = _FIELD2ATTR[field_name]
     577                  if key != 'project_url':
     578                      data[key] = self[field_name]
     579                  else:
     580                      data[key] = [','.join(u) for u in self[field_name]]
     581  
     582          return data
     583  
     584      def add_requirements(self, requirements):
     585          if self['Metadata-Version'] == '1.1':
     586              # we can't have 1.1 metadata *and* Setuptools requires
     587              for field in ('Obsoletes', 'Requires', 'Provides'):
     588                  if field in self:
     589                      del self[field]
     590          self['Requires-Dist'] += requirements
     591  
     592      # Mapping API
     593      # TODO could add iter* variants
     594  
     595      def keys(self):
     596          return list(_version2fieldlist(self['Metadata-Version']))
     597  
     598      def __iter__(self):
     599          for key in self.keys():
     600              yield key
     601  
     602      def values(self):
     603          return [self[key] for key in self.keys()]
     604  
     605      def items(self):
     606          return [(key, self[key]) for key in self.keys()]
     607  
     608      def __repr__(self):
     609          return '<%s %s %s>' % (self.__class__.__name__, self.name,
     610                                 self.version)
     611  
     612  
     613  METADATA_FILENAME = 'pydist.json'
     614  WHEEL_METADATA_FILENAME = 'metadata.json'
     615  LEGACY_METADATA_FILENAME = 'METADATA'
     616  
     617  
     618  class ESC[4;38;5;81mMetadata(ESC[4;38;5;149mobject):
     619      """
     620      The metadata of a release. This implementation uses 2.1
     621      metadata where possible. If not possible, it wraps a LegacyMetadata
     622      instance which handles the key-value metadata format.
     623      """
     624  
     625      METADATA_VERSION_MATCHER = re.compile(r'^\d+(\.\d+)*$')
     626  
     627      NAME_MATCHER = re.compile('^[0-9A-Z]([0-9A-Z_.-]*[0-9A-Z])?$', re.I)
     628  
     629      FIELDNAME_MATCHER = re.compile('^[A-Z]([0-9A-Z-]*[0-9A-Z])?$', re.I)
     630  
     631      VERSION_MATCHER = PEP440_VERSION_RE
     632  
     633      SUMMARY_MATCHER = re.compile('.{1,2047}')
     634  
     635      METADATA_VERSION = '2.0'
     636  
     637      GENERATOR = 'distlib (%s)' % __version__
     638  
     639      MANDATORY_KEYS = {
     640          'name': (),
     641          'version': (),
     642          'summary': ('legacy',),
     643      }
     644  
     645      INDEX_KEYS = ('name version license summary description author '
     646                    'author_email keywords platform home_page classifiers '
     647                    'download_url')
     648  
     649      DEPENDENCY_KEYS = ('extras run_requires test_requires build_requires '
     650                         'dev_requires provides meta_requires obsoleted_by '
     651                         'supports_environments')
     652  
     653      SYNTAX_VALIDATORS = {
     654          'metadata_version': (METADATA_VERSION_MATCHER, ()),
     655          'name': (NAME_MATCHER, ('legacy',)),
     656          'version': (VERSION_MATCHER, ('legacy',)),
     657          'summary': (SUMMARY_MATCHER, ('legacy',)),
     658          'dynamic': (FIELDNAME_MATCHER, ('legacy',)),
     659      }
     660  
     661      __slots__ = ('_legacy', '_data', 'scheme')
     662  
     663      def __init__(self, path=None, fileobj=None, mapping=None,
     664                   scheme='default'):
     665          if [path, fileobj, mapping].count(None) < 2:
     666              raise TypeError('path, fileobj and mapping are exclusive')
     667          self._legacy = None
     668          self._data = None
     669          self.scheme = scheme
     670          #import pdb; pdb.set_trace()
     671          if mapping is not None:
     672              try:
     673                  self._validate_mapping(mapping, scheme)
     674                  self._data = mapping
     675              except MetadataUnrecognizedVersionError:
     676                  self._legacy = LegacyMetadata(mapping=mapping, scheme=scheme)
     677                  self.validate()
     678          else:
     679              data = None
     680              if path:
     681                  with open(path, 'rb') as f:
     682                      data = f.read()
     683              elif fileobj:
     684                  data = fileobj.read()
     685              if data is None:
     686                  # Initialised with no args - to be added
     687                  self._data = {
     688                      'metadata_version': self.METADATA_VERSION,
     689                      'generator': self.GENERATOR,
     690                  }
     691              else:
     692                  if not isinstance(data, text_type):
     693                      data = data.decode('utf-8')
     694                  try:
     695                      self._data = json.loads(data)
     696                      self._validate_mapping(self._data, scheme)
     697                  except ValueError:
     698                      # Note: MetadataUnrecognizedVersionError does not
     699                      # inherit from ValueError (it's a DistlibException,
     700                      # which should not inherit from ValueError).
     701                      # The ValueError comes from the json.load - if that
     702                      # succeeds and we get a validation error, we want
     703                      # that to propagate
     704                      self._legacy = LegacyMetadata(fileobj=StringIO(data),
     705                                                    scheme=scheme)
     706                      self.validate()
     707  
     708      common_keys = set(('name', 'version', 'license', 'keywords', 'summary'))
     709  
     710      none_list = (None, list)
     711      none_dict = (None, dict)
     712  
     713      mapped_keys = {
     714          'run_requires': ('Requires-Dist', list),
     715          'build_requires': ('Setup-Requires-Dist', list),
     716          'dev_requires': none_list,
     717          'test_requires': none_list,
     718          'meta_requires': none_list,
     719          'extras': ('Provides-Extra', list),
     720          'modules': none_list,
     721          'namespaces': none_list,
     722          'exports': none_dict,
     723          'commands': none_dict,
     724          'classifiers': ('Classifier', list),
     725          'source_url': ('Download-URL', None),
     726          'metadata_version': ('Metadata-Version', None),
     727      }
     728  
     729      del none_list, none_dict
     730  
     731      def __getattribute__(self, key):
     732          common = object.__getattribute__(self, 'common_keys')
     733          mapped = object.__getattribute__(self, 'mapped_keys')
     734          if key in mapped:
     735              lk, maker = mapped[key]
     736              if self._legacy:
     737                  if lk is None:
     738                      result = None if maker is None else maker()
     739                  else:
     740                      result = self._legacy.get(lk)
     741              else:
     742                  value = None if maker is None else maker()
     743                  if key not in ('commands', 'exports', 'modules', 'namespaces',
     744                                 'classifiers'):
     745                      result = self._data.get(key, value)
     746                  else:
     747                      # special cases for PEP 459
     748                      sentinel = object()
     749                      result = sentinel
     750                      d = self._data.get('extensions')
     751                      if d:
     752                          if key == 'commands':
     753                              result = d.get('python.commands', value)
     754                          elif key == 'classifiers':
     755                              d = d.get('python.details')
     756                              if d:
     757                                  result = d.get(key, value)
     758                          else:
     759                              d = d.get('python.exports')
     760                              if not d:
     761                                  d = self._data.get('python.exports')
     762                              if d:
     763                                  result = d.get(key, value)
     764                      if result is sentinel:
     765                          result = value
     766          elif key not in common:
     767              result = object.__getattribute__(self, key)
     768          elif self._legacy:
     769              result = self._legacy.get(key)
     770          else:
     771              result = self._data.get(key)
     772          return result
     773  
     774      def _validate_value(self, key, value, scheme=None):
     775          if key in self.SYNTAX_VALIDATORS:
     776              pattern, exclusions = self.SYNTAX_VALIDATORS[key]
     777              if (scheme or self.scheme) not in exclusions:
     778                  m = pattern.match(value)
     779                  if not m:
     780                      raise MetadataInvalidError("'%s' is an invalid value for "
     781                                                 "the '%s' property" % (value,
     782                                                                      key))
     783  
     784      def __setattr__(self, key, value):
     785          self._validate_value(key, value)
     786          common = object.__getattribute__(self, 'common_keys')
     787          mapped = object.__getattribute__(self, 'mapped_keys')
     788          if key in mapped:
     789              lk, _ = mapped[key]
     790              if self._legacy:
     791                  if lk is None:
     792                      raise NotImplementedError
     793                  self._legacy[lk] = value
     794              elif key not in ('commands', 'exports', 'modules', 'namespaces',
     795                               'classifiers'):
     796                  self._data[key] = value
     797              else:
     798                  # special cases for PEP 459
     799                  d = self._data.setdefault('extensions', {})
     800                  if key == 'commands':
     801                      d['python.commands'] = value
     802                  elif key == 'classifiers':
     803                      d = d.setdefault('python.details', {})
     804                      d[key] = value
     805                  else:
     806                      d = d.setdefault('python.exports', {})
     807                      d[key] = value
     808          elif key not in common:
     809              object.__setattr__(self, key, value)
     810          else:
     811              if key == 'keywords':
     812                  if isinstance(value, string_types):
     813                      value = value.strip()
     814                      if value:
     815                          value = value.split()
     816                      else:
     817                          value = []
     818              if self._legacy:
     819                  self._legacy[key] = value
     820              else:
     821                  self._data[key] = value
     822  
     823      @property
     824      def name_and_version(self):
     825          return _get_name_and_version(self.name, self.version, True)
     826  
     827      @property
     828      def provides(self):
     829          if self._legacy:
     830              result = self._legacy['Provides-Dist']
     831          else:
     832              result = self._data.setdefault('provides', [])
     833          s = '%s (%s)' % (self.name, self.version)
     834          if s not in result:
     835              result.append(s)
     836          return result
     837  
     838      @provides.setter
     839      def provides(self, value):
     840          if self._legacy:
     841              self._legacy['Provides-Dist'] = value
     842          else:
     843              self._data['provides'] = value
     844  
     845      def get_requirements(self, reqts, extras=None, env=None):
     846          """
     847          Base method to get dependencies, given a set of extras
     848          to satisfy and an optional environment context.
     849          :param reqts: A list of sometimes-wanted dependencies,
     850                        perhaps dependent on extras and environment.
     851          :param extras: A list of optional components being requested.
     852          :param env: An optional environment for marker evaluation.
     853          """
     854          if self._legacy:
     855              result = reqts
     856          else:
     857              result = []
     858              extras = get_extras(extras or [], self.extras)
     859              for d in reqts:
     860                  if 'extra' not in d and 'environment' not in d:
     861                      # unconditional
     862                      include = True
     863                  else:
     864                      if 'extra' not in d:
     865                          # Not extra-dependent - only environment-dependent
     866                          include = True
     867                      else:
     868                          include = d.get('extra') in extras
     869                      if include:
     870                          # Not excluded because of extras, check environment
     871                          marker = d.get('environment')
     872                          if marker:
     873                              include = interpret(marker, env)
     874                  if include:
     875                      result.extend(d['requires'])
     876              for key in ('build', 'dev', 'test'):
     877                  e = ':%s:' % key
     878                  if e in extras:
     879                      extras.remove(e)
     880                      # A recursive call, but it should terminate since 'test'
     881                      # has been removed from the extras
     882                      reqts = self._data.get('%s_requires' % key, [])
     883                      result.extend(self.get_requirements(reqts, extras=extras,
     884                                                          env=env))
     885          return result
     886  
     887      @property
     888      def dictionary(self):
     889          if self._legacy:
     890              return self._from_legacy()
     891          return self._data
     892  
     893      @property
     894      def dependencies(self):
     895          if self._legacy:
     896              raise NotImplementedError
     897          else:
     898              return extract_by_key(self._data, self.DEPENDENCY_KEYS)
     899  
     900      @dependencies.setter
     901      def dependencies(self, value):
     902          if self._legacy:
     903              raise NotImplementedError
     904          else:
     905              self._data.update(value)
     906  
     907      def _validate_mapping(self, mapping, scheme):
     908          if mapping.get('metadata_version') != self.METADATA_VERSION:
     909              raise MetadataUnrecognizedVersionError()
     910          missing = []
     911          for key, exclusions in self.MANDATORY_KEYS.items():
     912              if key not in mapping:
     913                  if scheme not in exclusions:
     914                      missing.append(key)
     915          if missing:
     916              msg = 'Missing metadata items: %s' % ', '.join(missing)
     917              raise MetadataMissingError(msg)
     918          for k, v in mapping.items():
     919              self._validate_value(k, v, scheme)
     920  
     921      def validate(self):
     922          if self._legacy:
     923              missing, warnings = self._legacy.check(True)
     924              if missing or warnings:
     925                  logger.warning('Metadata: missing: %s, warnings: %s',
     926                                 missing, warnings)
     927          else:
     928              self._validate_mapping(self._data, self.scheme)
     929  
     930      def todict(self):
     931          if self._legacy:
     932              return self._legacy.todict(True)
     933          else:
     934              result = extract_by_key(self._data, self.INDEX_KEYS)
     935              return result
     936  
     937      def _from_legacy(self):
     938          assert self._legacy and not self._data
     939          result = {
     940              'metadata_version': self.METADATA_VERSION,
     941              'generator': self.GENERATOR,
     942          }
     943          lmd = self._legacy.todict(True)     # skip missing ones
     944          for k in ('name', 'version', 'license', 'summary', 'description',
     945                    'classifier'):
     946              if k in lmd:
     947                  if k == 'classifier':
     948                      nk = 'classifiers'
     949                  else:
     950                      nk = k
     951                  result[nk] = lmd[k]
     952          kw = lmd.get('Keywords', [])
     953          if kw == ['']:
     954              kw = []
     955          result['keywords'] = kw
     956          keys = (('requires_dist', 'run_requires'),
     957                  ('setup_requires_dist', 'build_requires'))
     958          for ok, nk in keys:
     959              if ok in lmd and lmd[ok]:
     960                  result[nk] = [{'requires': lmd[ok]}]
     961          result['provides'] = self.provides
     962          author = {}
     963          maintainer = {}
     964          return result
     965  
     966      LEGACY_MAPPING = {
     967          'name': 'Name',
     968          'version': 'Version',
     969          ('extensions', 'python.details', 'license'): 'License',
     970          'summary': 'Summary',
     971          'description': 'Description',
     972          ('extensions', 'python.project', 'project_urls', 'Home'): 'Home-page',
     973          ('extensions', 'python.project', 'contacts', 0, 'name'): 'Author',
     974          ('extensions', 'python.project', 'contacts', 0, 'email'): 'Author-email',
     975          'source_url': 'Download-URL',
     976          ('extensions', 'python.details', 'classifiers'): 'Classifier',
     977      }
     978  
     979      def _to_legacy(self):
     980          def process_entries(entries):
     981              reqts = set()
     982              for e in entries:
     983                  extra = e.get('extra')
     984                  env = e.get('environment')
     985                  rlist = e['requires']
     986                  for r in rlist:
     987                      if not env and not extra:
     988                          reqts.add(r)
     989                      else:
     990                          marker = ''
     991                          if extra:
     992                              marker = 'extra == "%s"' % extra
     993                          if env:
     994                              if marker:
     995                                  marker = '(%s) and %s' % (env, marker)
     996                              else:
     997                                  marker = env
     998                          reqts.add(';'.join((r, marker)))
     999              return reqts
    1000  
    1001          assert self._data and not self._legacy
    1002          result = LegacyMetadata()
    1003          nmd = self._data
    1004          # import pdb; pdb.set_trace()
    1005          for nk, ok in self.LEGACY_MAPPING.items():
    1006              if not isinstance(nk, tuple):
    1007                  if nk in nmd:
    1008                      result[ok] = nmd[nk]
    1009              else:
    1010                  d = nmd
    1011                  found = True
    1012                  for k in nk:
    1013                      try:
    1014                          d = d[k]
    1015                      except (KeyError, IndexError):
    1016                          found = False
    1017                          break
    1018                  if found:
    1019                      result[ok] = d
    1020          r1 = process_entries(self.run_requires + self.meta_requires)
    1021          r2 = process_entries(self.build_requires + self.dev_requires)
    1022          if self.extras:
    1023              result['Provides-Extra'] = sorted(self.extras)
    1024          result['Requires-Dist'] = sorted(r1)
    1025          result['Setup-Requires-Dist'] = sorted(r2)
    1026          # TODO: any other fields wanted
    1027          return result
    1028  
    1029      def write(self, path=None, fileobj=None, legacy=False, skip_unknown=True):
    1030          if [path, fileobj].count(None) != 1:
    1031              raise ValueError('Exactly one of path and fileobj is needed')
    1032          self.validate()
    1033          if legacy:
    1034              if self._legacy:
    1035                  legacy_md = self._legacy
    1036              else:
    1037                  legacy_md = self._to_legacy()
    1038              if path:
    1039                  legacy_md.write(path, skip_unknown=skip_unknown)
    1040              else:
    1041                  legacy_md.write_file(fileobj, skip_unknown=skip_unknown)
    1042          else:
    1043              if self._legacy:
    1044                  d = self._from_legacy()
    1045              else:
    1046                  d = self._data
    1047              if fileobj:
    1048                  json.dump(d, fileobj, ensure_ascii=True, indent=2,
    1049                            sort_keys=True)
    1050              else:
    1051                  with codecs.open(path, 'w', 'utf-8') as f:
    1052                      json.dump(d, f, ensure_ascii=True, indent=2,
    1053                                sort_keys=True)
    1054  
    1055      def add_requirements(self, requirements):
    1056          if self._legacy:
    1057              self._legacy.add_requirements(requirements)
    1058          else:
    1059              run_requires = self._data.setdefault('run_requires', [])
    1060              always = None
    1061              for entry in run_requires:
    1062                  if 'environment' not in entry and 'extra' not in entry:
    1063                      always = entry
    1064                      break
    1065              if always is None:
    1066                  always = { 'requires': requirements }
    1067                  run_requires.insert(0, always)
    1068              else:
    1069                  rset = set(always['requires']) | set(requirements)
    1070                  always['requires'] = sorted(rset)
    1071  
    1072      def __repr__(self):
    1073          name = self.name or '(no name)'
    1074          version = self.version or 'no version'
    1075          return '<%s %s %s (%s)>' % (self.__class__.__name__,
    1076                                      self.metadata_version, name, version)