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