Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/distlib/metadata.py @ 0:d30785e31577 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author | guerler |
---|---|
date | Fri, 31 Jul 2020 00:18:57 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:d30785e31577 |
---|---|
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) |