Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1 | """Implementation of the Metadata for Python packages PEPs. |
| 2 | |
| 3 | Supports all metadata formats (1.0, 1.1, 1.2). |
| 4 | """ |
| 5 | |
| 6 | import re |
| 7 | import logging |
| 8 | |
| 9 | from io import StringIO |
| 10 | from email import message_from_file |
| 11 | from packaging import logger |
| 12 | from packaging.markers import interpret |
| 13 | from packaging.version import (is_valid_predicate, is_valid_version, |
| 14 | is_valid_versions) |
| 15 | from packaging.errors import (MetadataMissingError, |
| 16 | MetadataConflictError, |
| 17 | MetadataUnrecognizedVersionError) |
| 18 | |
| 19 | try: |
| 20 | # docutils is installed |
| 21 | from docutils.utils import Reporter |
| 22 | from docutils.parsers.rst import Parser |
| 23 | from docutils import frontend |
| 24 | from docutils import nodes |
| 25 | |
| 26 | class SilentReporter(Reporter): |
| 27 | |
| 28 | def __init__(self, source, report_level, halt_level, stream=None, |
| 29 | debug=0, encoding='ascii', error_handler='replace'): |
| 30 | self.messages = [] |
| 31 | Reporter.__init__(self, source, report_level, halt_level, stream, |
| 32 | debug, encoding, error_handler) |
| 33 | |
| 34 | def system_message(self, level, message, *children, **kwargs): |
| 35 | self.messages.append((level, message, children, kwargs)) |
| 36 | |
| 37 | _HAS_DOCUTILS = True |
| 38 | except ImportError: |
| 39 | # docutils is not installed |
| 40 | _HAS_DOCUTILS = False |
| 41 | |
| 42 | # public API of this module |
| 43 | __all__ = ['Metadata', 'PKG_INFO_ENCODING', 'PKG_INFO_PREFERRED_VERSION'] |
| 44 | |
| 45 | # Encoding used for the PKG-INFO files |
| 46 | PKG_INFO_ENCODING = 'utf-8' |
| 47 | |
| 48 | # preferred version. Hopefully will be changed |
| 49 | # to 1.2 once PEP 345 is supported everywhere |
| 50 | PKG_INFO_PREFERRED_VERSION = '1.0' |
| 51 | |
| 52 | _LINE_PREFIX = re.compile('\n \|') |
| 53 | _241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', |
| 54 | 'Summary', 'Description', |
| 55 | 'Keywords', 'Home-page', 'Author', 'Author-email', |
| 56 | 'License') |
| 57 | |
| 58 | _314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', |
| 59 | 'Supported-Platform', 'Summary', 'Description', |
| 60 | 'Keywords', 'Home-page', 'Author', 'Author-email', |
| 61 | 'License', 'Classifier', 'Download-URL', 'Obsoletes', |
| 62 | 'Provides', 'Requires') |
| 63 | |
| 64 | _314_MARKERS = ('Obsoletes', 'Provides', 'Requires') |
| 65 | |
| 66 | _345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', |
| 67 | 'Supported-Platform', 'Summary', 'Description', |
| 68 | 'Keywords', 'Home-page', 'Author', 'Author-email', |
| 69 | 'Maintainer', 'Maintainer-email', 'License', |
| 70 | 'Classifier', 'Download-URL', 'Obsoletes-Dist', |
| 71 | 'Project-URL', 'Provides-Dist', 'Requires-Dist', |
| 72 | 'Requires-Python', 'Requires-External') |
| 73 | |
| 74 | _345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python', |
| 75 | 'Obsoletes-Dist', 'Requires-External', 'Maintainer', |
| 76 | 'Maintainer-email', 'Project-URL') |
| 77 | |
| 78 | _ALL_FIELDS = set() |
| 79 | _ALL_FIELDS.update(_241_FIELDS) |
| 80 | _ALL_FIELDS.update(_314_FIELDS) |
| 81 | _ALL_FIELDS.update(_345_FIELDS) |
| 82 | |
| 83 | |
| 84 | def _version2fieldlist(version): |
| 85 | if version == '1.0': |
| 86 | return _241_FIELDS |
| 87 | elif version == '1.1': |
| 88 | return _314_FIELDS |
| 89 | elif version == '1.2': |
| 90 | return _345_FIELDS |
| 91 | raise MetadataUnrecognizedVersionError(version) |
| 92 | |
| 93 | |
| 94 | def _best_version(fields): |
| 95 | """Detect the best version depending on the fields used.""" |
| 96 | def _has_marker(keys, markers): |
| 97 | for marker in markers: |
| 98 | if marker in keys: |
| 99 | return True |
| 100 | return False |
| 101 | |
| 102 | keys = list(fields) |
| 103 | possible_versions = ['1.0', '1.1', '1.2'] |
| 104 | |
| 105 | # first let's try to see if a field is not part of one of the version |
| 106 | for key in keys: |
| 107 | if key not in _241_FIELDS and '1.0' in possible_versions: |
| 108 | possible_versions.remove('1.0') |
| 109 | if key not in _314_FIELDS and '1.1' in possible_versions: |
| 110 | possible_versions.remove('1.1') |
| 111 | if key not in _345_FIELDS and '1.2' in possible_versions: |
| 112 | possible_versions.remove('1.2') |
| 113 | |
| 114 | # possible_version contains qualified versions |
| 115 | if len(possible_versions) == 1: |
| 116 | return possible_versions[0] # found ! |
| 117 | elif len(possible_versions) == 0: |
| 118 | raise MetadataConflictError('Unknown metadata set') |
| 119 | |
| 120 | # let's see if one unique marker is found |
| 121 | is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS) |
| 122 | is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS) |
| 123 | if is_1_1 and is_1_2: |
| 124 | raise MetadataConflictError('You used incompatible 1.1 and 1.2 fields') |
| 125 | |
| 126 | # we have the choice, either 1.0, or 1.2 |
| 127 | # - 1.0 has a broken Summary field but works with all tools |
| 128 | # - 1.1 is to avoid |
| 129 | # - 1.2 fixes Summary but is not widespread yet |
| 130 | if not is_1_1 and not is_1_2: |
| 131 | # we couldn't find any specific marker |
| 132 | if PKG_INFO_PREFERRED_VERSION in possible_versions: |
| 133 | return PKG_INFO_PREFERRED_VERSION |
| 134 | if is_1_1: |
| 135 | return '1.1' |
| 136 | |
| 137 | # default marker when 1.0 is disqualified |
| 138 | return '1.2' |
| 139 | |
| 140 | |
| 141 | _ATTR2FIELD = { |
| 142 | 'metadata_version': 'Metadata-Version', |
| 143 | 'name': 'Name', |
| 144 | 'version': 'Version', |
| 145 | 'platform': 'Platform', |
| 146 | 'supported_platform': 'Supported-Platform', |
| 147 | 'summary': 'Summary', |
| 148 | 'description': 'Description', |
| 149 | 'keywords': 'Keywords', |
| 150 | 'home_page': 'Home-page', |
| 151 | 'author': 'Author', |
| 152 | 'author_email': 'Author-email', |
| 153 | 'maintainer': 'Maintainer', |
| 154 | 'maintainer_email': 'Maintainer-email', |
| 155 | 'license': 'License', |
| 156 | 'classifier': 'Classifier', |
| 157 | 'download_url': 'Download-URL', |
| 158 | 'obsoletes_dist': 'Obsoletes-Dist', |
| 159 | 'provides_dist': 'Provides-Dist', |
| 160 | 'requires_dist': 'Requires-Dist', |
| 161 | 'requires_python': 'Requires-Python', |
| 162 | 'requires_external': 'Requires-External', |
| 163 | 'requires': 'Requires', |
| 164 | 'provides': 'Provides', |
| 165 | 'obsoletes': 'Obsoletes', |
| 166 | 'project_url': 'Project-URL', |
| 167 | } |
| 168 | |
| 169 | _PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist') |
| 170 | _VERSIONS_FIELDS = ('Requires-Python',) |
| 171 | _VERSION_FIELDS = ('Version',) |
| 172 | _LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes', |
| 173 | 'Requires', 'Provides', 'Obsoletes-Dist', |
| 174 | 'Provides-Dist', 'Requires-Dist', 'Requires-External', |
| 175 | 'Project-URL', 'Supported-Platform') |
| 176 | _LISTTUPLEFIELDS = ('Project-URL',) |
| 177 | |
| 178 | _ELEMENTSFIELD = ('Keywords',) |
| 179 | |
| 180 | _UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description') |
| 181 | |
| 182 | _MISSING = object() |
| 183 | |
| 184 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 185 | class Metadata: |
| 186 | """The metadata of a release. |
| 187 | |
| 188 | Supports versions 1.0, 1.1 and 1.2 (auto-detected). You can |
| 189 | instantiate the class with one of these arguments (or none): |
| 190 | - *path*, the path to a METADATA file |
| 191 | - *fileobj* give a file-like object with METADATA as content |
| 192 | - *mapping* is a dict-like object |
| 193 | """ |
| 194 | # TODO document that execution_context and platform_dependent are used |
| 195 | # to filter on query, not when setting a key |
| 196 | # also document the mapping API and UNKNOWN default key |
| 197 | |
| 198 | def __init__(self, path=None, platform_dependent=False, |
| 199 | execution_context=None, fileobj=None, mapping=None): |
| 200 | self._fields = {} |
| 201 | self.requires_files = [] |
| 202 | self.docutils_support = _HAS_DOCUTILS |
| 203 | self.platform_dependent = platform_dependent |
| 204 | self.execution_context = execution_context |
| 205 | if [path, fileobj, mapping].count(None) < 2: |
| 206 | raise TypeError('path, fileobj and mapping are exclusive') |
| 207 | if path is not None: |
| 208 | self.read(path) |
| 209 | elif fileobj is not None: |
| 210 | self.read_file(fileobj) |
| 211 | elif mapping is not None: |
| 212 | self.update(mapping) |
| 213 | |
| 214 | def _set_best_version(self): |
| 215 | self._fields['Metadata-Version'] = _best_version(self._fields) |
| 216 | |
| 217 | def _write_field(self, file, name, value): |
| 218 | file.write('%s: %s\n' % (name, value)) |
| 219 | |
| 220 | def __getitem__(self, name): |
| 221 | return self.get(name) |
| 222 | |
| 223 | def __setitem__(self, name, value): |
| 224 | return self.set(name, value) |
| 225 | |
| 226 | def __delitem__(self, name): |
| 227 | field_name = self._convert_name(name) |
| 228 | try: |
| 229 | del self._fields[field_name] |
| 230 | except KeyError: |
| 231 | raise KeyError(name) |
| 232 | self._set_best_version() |
| 233 | |
| 234 | def __contains__(self, name): |
| 235 | return (name in self._fields or |
| 236 | self._convert_name(name) in self._fields) |
| 237 | |
| 238 | def _convert_name(self, name): |
| 239 | if name in _ALL_FIELDS: |
| 240 | return name |
| 241 | name = name.replace('-', '_').lower() |
| 242 | return _ATTR2FIELD.get(name, name) |
| 243 | |
| 244 | def _default_value(self, name): |
| 245 | if name in _LISTFIELDS or name in _ELEMENTSFIELD: |
| 246 | return [] |
| 247 | return 'UNKNOWN' |
| 248 | |
| 249 | def _check_rst_data(self, data): |
| 250 | """Return warnings when the provided data has syntax errors.""" |
| 251 | source_path = StringIO() |
| 252 | parser = Parser() |
| 253 | settings = frontend.OptionParser().get_default_values() |
| 254 | settings.tab_width = 4 |
| 255 | settings.pep_references = None |
| 256 | settings.rfc_references = None |
| 257 | reporter = SilentReporter(source_path, |
| 258 | settings.report_level, |
| 259 | settings.halt_level, |
| 260 | stream=settings.warning_stream, |
| 261 | debug=settings.debug, |
| 262 | encoding=settings.error_encoding, |
| 263 | error_handler=settings.error_encoding_error_handler) |
| 264 | |
| 265 | document = nodes.document(settings, reporter, source=source_path) |
| 266 | document.note_source(source_path, -1) |
| 267 | try: |
| 268 | parser.parse(data, document) |
| 269 | except AttributeError: |
| 270 | reporter.messages.append((-1, 'Could not finish the parsing.', |
| 271 | '', {})) |
| 272 | |
| 273 | return reporter.messages |
| 274 | |
| 275 | def _platform(self, value): |
| 276 | if not self.platform_dependent or ';' not in value: |
| 277 | return True, value |
| 278 | value, marker = value.split(';') |
| 279 | return interpret(marker, self.execution_context), value |
| 280 | |
| 281 | def _remove_line_prefix(self, value): |
| 282 | return _LINE_PREFIX.sub('\n', value) |
| 283 | |
| 284 | # |
| 285 | # Public API |
| 286 | # |
| 287 | def get_fullname(self): |
| 288 | """Return the distribution name with version""" |
| 289 | return '%s-%s' % (self['Name'], self['Version']) |
| 290 | |
| 291 | def is_metadata_field(self, name): |
| 292 | """return True if name is a valid metadata key""" |
| 293 | name = self._convert_name(name) |
| 294 | return name in _ALL_FIELDS |
| 295 | |
| 296 | def is_multi_field(self, name): |
| 297 | name = self._convert_name(name) |
| 298 | return name in _LISTFIELDS |
| 299 | |
| 300 | def read(self, filepath): |
| 301 | """Read the metadata values from a file path.""" |
Victor Stinner | c336452 | 2011-05-19 18:49:56 +0200 | [diff] [blame] | 302 | with open(filepath, 'r', encoding='utf-8') as fp: |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 303 | self.read_file(fp) |
| 304 | |
| 305 | def read_file(self, fileob): |
| 306 | """Read the metadata values from a file object.""" |
| 307 | msg = message_from_file(fileob) |
| 308 | self._fields['Metadata-Version'] = msg['metadata-version'] |
| 309 | |
| 310 | for field in _version2fieldlist(self['Metadata-Version']): |
| 311 | if field in _LISTFIELDS: |
| 312 | # we can have multiple lines |
| 313 | values = msg.get_all(field) |
| 314 | if field in _LISTTUPLEFIELDS and values is not None: |
| 315 | values = [tuple(value.split(',')) for value in values] |
| 316 | self.set(field, values) |
| 317 | else: |
| 318 | # single line |
| 319 | value = msg[field] |
| 320 | if value is not None and value != 'UNKNOWN': |
| 321 | self.set(field, value) |
| 322 | |
| 323 | def write(self, filepath): |
| 324 | """Write the metadata fields to filepath.""" |
Victor Stinner | c336452 | 2011-05-19 18:49:56 +0200 | [diff] [blame] | 325 | with open(filepath, 'w', encoding='utf-8') as fp: |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 326 | self.write_file(fp) |
| 327 | |
| 328 | def write_file(self, fileobject): |
| 329 | """Write the PKG-INFO format data to a file object.""" |
| 330 | self._set_best_version() |
| 331 | for field in _version2fieldlist(self['Metadata-Version']): |
| 332 | values = self.get(field) |
| 333 | if field in _ELEMENTSFIELD: |
| 334 | self._write_field(fileobject, field, ','.join(values)) |
| 335 | continue |
| 336 | if field not in _LISTFIELDS: |
| 337 | if field == 'Description': |
| 338 | values = values.replace('\n', '\n |') |
| 339 | values = [values] |
| 340 | |
| 341 | if field in _LISTTUPLEFIELDS: |
| 342 | values = [','.join(value) for value in values] |
| 343 | |
| 344 | for value in values: |
| 345 | self._write_field(fileobject, field, value) |
| 346 | |
| 347 | def update(self, other=None, **kwargs): |
| 348 | """Set metadata values from the given iterable `other` and kwargs. |
| 349 | |
| 350 | Behavior is like `dict.update`: If `other` has a ``keys`` method, |
| 351 | they are looped over and ``self[key]`` is assigned ``other[key]``. |
| 352 | Else, ``other`` is an iterable of ``(key, value)`` iterables. |
| 353 | |
| 354 | Keys that don't match a metadata field or that have an empty value are |
| 355 | dropped. |
| 356 | """ |
| 357 | def _set(key, value): |
| 358 | if key in _ATTR2FIELD and value: |
| 359 | self.set(self._convert_name(key), value) |
| 360 | |
| 361 | if other is None: |
| 362 | pass |
| 363 | elif hasattr(other, 'keys'): |
| 364 | for k in other.keys(): |
| 365 | _set(k, other[k]) |
| 366 | else: |
| 367 | for k, v in other: |
| 368 | _set(k, v) |
| 369 | |
| 370 | if kwargs: |
| 371 | self.update(kwargs) |
| 372 | |
| 373 | def set(self, name, value): |
| 374 | """Control then set a metadata field.""" |
| 375 | name = self._convert_name(name) |
| 376 | |
| 377 | if ((name in _ELEMENTSFIELD or name == 'Platform') and |
| 378 | not isinstance(value, (list, tuple))): |
| 379 | if isinstance(value, str): |
| 380 | value = [v.strip() for v in value.split(',')] |
| 381 | else: |
| 382 | value = [] |
| 383 | elif (name in _LISTFIELDS and |
| 384 | not isinstance(value, (list, tuple))): |
| 385 | if isinstance(value, str): |
| 386 | value = [value] |
| 387 | else: |
| 388 | value = [] |
| 389 | |
| 390 | if logger.isEnabledFor(logging.WARNING): |
Tarek Ziade | b9c0987 | 2011-05-30 12:25:38 +0200 | [diff] [blame] | 391 | project_name = self['Name'] |
| 392 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 393 | if name in _PREDICATE_FIELDS and value is not None: |
| 394 | for v in value: |
| 395 | # check that the values are valid predicates |
| 396 | if not is_valid_predicate(v.split(';')[0]): |
| 397 | logger.warning( |
Tarek Ziade | b9c0987 | 2011-05-30 12:25:38 +0200 | [diff] [blame] | 398 | '%r: %r is not a valid predicate (field %r)', |
| 399 | project_name, v, name) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 400 | # FIXME this rejects UNKNOWN, is that right? |
| 401 | elif name in _VERSIONS_FIELDS and value is not None: |
| 402 | if not is_valid_versions(value): |
Tarek Ziade | b9c0987 | 2011-05-30 12:25:38 +0200 | [diff] [blame] | 403 | logger.warning('%r: %r is not a valid version (field %r)', |
| 404 | project_name, value, name) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 405 | elif name in _VERSION_FIELDS and value is not None: |
| 406 | if not is_valid_version(value): |
Tarek Ziade | b9c0987 | 2011-05-30 12:25:38 +0200 | [diff] [blame] | 407 | logger.warning('%r: %r is not a valid version (field %r)', |
| 408 | project_name, value, name) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 409 | |
| 410 | if name in _UNICODEFIELDS: |
| 411 | if name == 'Description': |
| 412 | value = self._remove_line_prefix(value) |
| 413 | |
| 414 | self._fields[name] = value |
| 415 | self._set_best_version() |
| 416 | |
| 417 | def get(self, name, default=_MISSING): |
| 418 | """Get a metadata field.""" |
| 419 | name = self._convert_name(name) |
| 420 | if name not in self._fields: |
| 421 | if default is _MISSING: |
| 422 | default = self._default_value(name) |
| 423 | return default |
| 424 | if name in _UNICODEFIELDS: |
| 425 | value = self._fields[name] |
| 426 | return value |
| 427 | elif name in _LISTFIELDS: |
| 428 | value = self._fields[name] |
| 429 | if value is None: |
| 430 | return [] |
| 431 | res = [] |
| 432 | for val in value: |
| 433 | valid, val = self._platform(val) |
| 434 | if not valid: |
| 435 | continue |
| 436 | if name not in _LISTTUPLEFIELDS: |
| 437 | res.append(val) |
| 438 | else: |
| 439 | # That's for Project-URL |
| 440 | res.append((val[0], val[1])) |
| 441 | return res |
| 442 | |
| 443 | elif name in _ELEMENTSFIELD: |
| 444 | valid, value = self._platform(self._fields[name]) |
| 445 | if not valid: |
| 446 | return [] |
| 447 | if isinstance(value, str): |
| 448 | return value.split(',') |
| 449 | valid, value = self._platform(self._fields[name]) |
| 450 | if not valid: |
| 451 | return None |
| 452 | return value |
| 453 | |
| 454 | def check(self, strict=False, restructuredtext=False): |
| 455 | """Check if the metadata is compliant. If strict is False then raise if |
| 456 | no Name or Version are provided""" |
| 457 | # XXX should check the versions (if the file was loaded) |
| 458 | missing, warnings = [], [] |
| 459 | |
| 460 | for attr in ('Name', 'Version'): # required by PEP 345 |
| 461 | if attr not in self: |
| 462 | missing.append(attr) |
| 463 | |
| 464 | if strict and missing != []: |
| 465 | msg = 'missing required metadata: %s' % ', '.join(missing) |
| 466 | raise MetadataMissingError(msg) |
| 467 | |
| 468 | for attr in ('Home-page', 'Author'): |
| 469 | if attr not in self: |
| 470 | missing.append(attr) |
| 471 | |
| 472 | if _HAS_DOCUTILS and restructuredtext: |
| 473 | warnings.extend(self._check_rst_data(self['Description'])) |
| 474 | |
| 475 | # checking metadata 1.2 (XXX needs to check 1.1, 1.0) |
| 476 | if self['Metadata-Version'] != '1.2': |
| 477 | return missing, warnings |
| 478 | |
| 479 | def is_valid_predicates(value): |
| 480 | for v in value: |
| 481 | if not is_valid_predicate(v.split(';')[0]): |
| 482 | return False |
| 483 | return True |
| 484 | |
| 485 | for fields, controller in ((_PREDICATE_FIELDS, is_valid_predicates), |
| 486 | (_VERSIONS_FIELDS, is_valid_versions), |
| 487 | (_VERSION_FIELDS, is_valid_version)): |
| 488 | for field in fields: |
| 489 | value = self.get(field, None) |
| 490 | if value is not None and not controller(value): |
| 491 | warnings.append('Wrong value for %r: %s' % (field, value)) |
| 492 | |
| 493 | return missing, warnings |
| 494 | |
| 495 | def todict(self): |
| 496 | """Return fields as a dict. |
| 497 | |
| 498 | Field names will be converted to use the underscore-lowercase style |
| 499 | instead of hyphen-mixed case (i.e. home_page instead of Home-page). |
| 500 | """ |
| 501 | data = { |
| 502 | 'metadata_version': self['Metadata-Version'], |
| 503 | 'name': self['Name'], |
| 504 | 'version': self['Version'], |
| 505 | 'summary': self['Summary'], |
| 506 | 'home_page': self['Home-page'], |
| 507 | 'author': self['Author'], |
| 508 | 'author_email': self['Author-email'], |
| 509 | 'license': self['License'], |
| 510 | 'description': self['Description'], |
| 511 | 'keywords': self['Keywords'], |
| 512 | 'platform': self['Platform'], |
| 513 | 'classifier': self['Classifier'], |
| 514 | 'download_url': self['Download-URL'], |
| 515 | } |
| 516 | |
| 517 | if self['Metadata-Version'] == '1.2': |
| 518 | data['requires_dist'] = self['Requires-Dist'] |
| 519 | data['requires_python'] = self['Requires-Python'] |
| 520 | data['requires_external'] = self['Requires-External'] |
| 521 | data['provides_dist'] = self['Provides-Dist'] |
| 522 | data['obsoletes_dist'] = self['Obsoletes-Dist'] |
| 523 | data['project_url'] = [','.join(url) for url in |
| 524 | self['Project-URL']] |
| 525 | |
| 526 | elif self['Metadata-Version'] == '1.1': |
| 527 | data['provides'] = self['Provides'] |
| 528 | data['requires'] = self['Requires'] |
| 529 | data['obsoletes'] = self['Obsoletes'] |
| 530 | |
| 531 | return data |
| 532 | |
| 533 | # Mapping API |
| 534 | |
| 535 | def keys(self): |
| 536 | return _version2fieldlist(self['Metadata-Version']) |
| 537 | |
| 538 | def __iter__(self): |
| 539 | for key in self.keys(): |
| 540 | yield key |
| 541 | |
| 542 | def values(self): |
| 543 | return [self[key] for key in list(self.keys())] |
| 544 | |
| 545 | def items(self): |
| 546 | return [(key, self[key]) for key in list(self.keys())] |