comparison env/lib/python3.9/site-packages/distlib/metadata.py @ 0:4f3585e2f14b draft default tip

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