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