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