summaryrefslogtreecommitdiff
path: root/venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/metadata.py
diff options
context:
space:
mode:
Diffstat (limited to 'venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/metadata.py')
-rw-r--r--venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/metadata.py1091
1 files changed, 1091 insertions, 0 deletions
diff --git a/venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/metadata.py b/venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/metadata.py
new file mode 100644
index 0000000..10a1fee
--- /dev/null
+++ b/venv/lib/python3.7/site-packages/pip-10.0.1-py3.7.egg/pip/_vendor/distlib/metadata.py
@@ -0,0 +1,1091 @@
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
8Supports all metadata formats (1.0, 1.1, 1.2, and 2.0 experimental).
9"""
10from __future__ import unicode_literals
11
12import codecs
13from email import message_from_file
14import json
15import logging
16import re
17
18
19from . import DistlibException, __version__
20from .compat import StringIO, string_types, text_type
21from .markers import interpret
22from .util import extract_by_key, get_extras
23from .version import get_scheme, PEP440_VERSION_RE
24
25logger = logging.getLogger(__name__)
26
27
28class MetadataMissingError(DistlibException):
29 """A required metadata is missing"""
30
31
32class MetadataConflictError(DistlibException):
33 """Attempt to read or write metadata fields that are conflictual."""
34
35
36class MetadataUnrecognizedVersionError(DistlibException):
37 """Unknown metadata version number."""
38
39
40class 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
47PKG_INFO_ENCODING = 'utf-8'
48
49# preferred version. Hopefully will be changed
50# to 1.2 once PEP 345 is supported everywhere
51PKG_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_566_FIELDS = _426_FIELDS + ('Description-Content-Type',)
95
96_566_MARKERS = ('Description-Content-Type',)
97
98_ALL_FIELDS = set()
99_ALL_FIELDS.update(_241_FIELDS)
100_ALL_FIELDS.update(_314_FIELDS)
101_ALL_FIELDS.update(_345_FIELDS)
102_ALL_FIELDS.update(_426_FIELDS)
103_ALL_FIELDS.update(_566_FIELDS)
104
105EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
106
107
108def _version2fieldlist(version):
109 if version == '1.0':
110 return _241_FIELDS
111 elif version == '1.1':
112 return _314_FIELDS
113 elif version == '1.2':
114 return _345_FIELDS
115 elif version in ('1.3', '2.1'):
116 return _345_FIELDS + _566_FIELDS
117 elif version == '2.0':
118 return _426_FIELDS
119 raise MetadataUnrecognizedVersionError(version)
120
121
122def _best_version(fields):
123 """Detect the best version depending on the fields used."""
124 def _has_marker(keys, markers):
125 for marker in markers:
126 if marker in keys:
127 return True
128 return False
129
130 keys = []
131 for key, value in fields.items():
132 if value in ([], 'UNKNOWN', None):
133 continue
134 keys.append(key)
135
136 possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.0', '2.1']
137
138 # first let's try to see if a field is not part of one of the version
139 for key in keys:
140 if key not in _241_FIELDS and '1.0' in possible_versions:
141 possible_versions.remove('1.0')
142 logger.debug('Removed 1.0 due to %s', key)
143 if key not in _314_FIELDS and '1.1' in possible_versions:
144 possible_versions.remove('1.1')
145 logger.debug('Removed 1.1 due to %s', key)
146 if key not in _345_FIELDS and '1.2' in possible_versions:
147 possible_versions.remove('1.2')
148 logger.debug('Removed 1.2 due to %s', key)
149 if key not in _566_FIELDS and '1.3' in possible_versions:
150 possible_versions.remove('1.3')
151 logger.debug('Removed 1.3 due to %s', key)
152 if key not in _566_FIELDS and '2.1' in possible_versions:
153 if key != 'Description': # In 2.1, description allowed after headers
154 possible_versions.remove('2.1')
155 logger.debug('Removed 2.1 due to %s', key)
156 if key not in _426_FIELDS and '2.0' in possible_versions:
157 possible_versions.remove('2.0')
158 logger.debug('Removed 2.0 due to %s', key)
159
160 # possible_version contains qualified versions
161 if len(possible_versions) == 1:
162 return possible_versions[0] # found !
163 elif len(possible_versions) == 0:
164 logger.debug('Out of options - unknown metadata set: %s', fields)
165 raise MetadataConflictError('Unknown metadata set')
166
167 # let's see if one unique marker is found
168 is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS)
169 is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS)
170 is_2_1 = '2.1' in possible_versions and _has_marker(keys, _566_MARKERS)
171 is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
172 if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_0) > 1:
173 raise MetadataConflictError('You used incompatible 1.1/1.2/2.0/2.1 fields')
174
175 # we have the choice, 1.0, or 1.2, or 2.0
176 # - 1.0 has a broken Summary field but works with all tools
177 # - 1.1 is to avoid
178 # - 1.2 fixes Summary but has little adoption
179 # - 2.0 adds more features and is very new
180 if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_0:
181 # we couldn't find any specific marker
182 if PKG_INFO_PREFERRED_VERSION in possible_versions:
183 return PKG_INFO_PREFERRED_VERSION
184 if is_1_1:
185 return '1.1'
186 if is_1_2:
187 return '1.2'
188 if is_2_1:
189 return '2.1'
190
191 return '2.0'
192
193_ATTR2FIELD = {
194 'metadata_version': 'Metadata-Version',
195 'name': 'Name',
196 'version': 'Version',
197 'platform': 'Platform',
198 'supported_platform': 'Supported-Platform',
199 'summary': 'Summary',
200 'description': 'Description',
201 'keywords': 'Keywords',
202 'home_page': 'Home-page',
203 'author': 'Author',
204 'author_email': 'Author-email',
205 'maintainer': 'Maintainer',
206 'maintainer_email': 'Maintainer-email',
207 'license': 'License',
208 'classifier': 'Classifier',
209 'download_url': 'Download-URL',
210 'obsoletes_dist': 'Obsoletes-Dist',
211 'provides_dist': 'Provides-Dist',
212 'requires_dist': 'Requires-Dist',
213 'setup_requires_dist': 'Setup-Requires-Dist',
214 'requires_python': 'Requires-Python',
215 'requires_external': 'Requires-External',
216 'requires': 'Requires',
217 'provides': 'Provides',
218 'obsoletes': 'Obsoletes',
219 'project_url': 'Project-URL',
220 'private_version': 'Private-Version',
221 'obsoleted_by': 'Obsoleted-By',
222 'extension': 'Extension',
223 'provides_extra': 'Provides-Extra',
224}
225
226_PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist')
227_VERSIONS_FIELDS = ('Requires-Python',)
228_VERSION_FIELDS = ('Version',)
229_LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes',
230 'Requires', 'Provides', 'Obsoletes-Dist',
231 'Provides-Dist', 'Requires-Dist', 'Requires-External',
232 'Project-URL', 'Supported-Platform', 'Setup-Requires-Dist',
233 'Provides-Extra', 'Extension')
234_LISTTUPLEFIELDS = ('Project-URL',)
235
236_ELEMENTSFIELD = ('Keywords',)
237
238_UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description')
239
240_MISSING = object()
241
242_FILESAFE = re.compile('[^A-Za-z0-9.]+')
243
244
245def _get_name_and_version(name, version, for_filename=False):
246 """Return the distribution name with version.
247
248 If for_filename is true, return a filename-escaped form."""
249 if for_filename:
250 # For both name and version any runs of non-alphanumeric or '.'
251 # characters are replaced with a single '-'. Additionally any
252 # spaces in the version string become '.'
253 name = _FILESAFE.sub('-', name)
254 version = _FILESAFE.sub('-', version.replace(' ', '.'))
255 return '%s-%s' % (name, version)
256
257
258class LegacyMetadata(object):
259 """The legacy metadata of a release.
260
261 Supports versions 1.0, 1.1 and 1.2 (auto-detected). You can
262 instantiate the class with one of these arguments (or none):
263 - *path*, the path to a metadata file
264 - *fileobj* give a file-like object with metadata as content
265 - *mapping* is a dict-like object
266 - *scheme* is a version scheme name
267 """
268 # TODO document the mapping API and UNKNOWN default key
269
270 def __init__(self, path=None, fileobj=None, mapping=None,
271 scheme='default'):
272 if [path, fileobj, mapping].count(None) < 2:
273 raise TypeError('path, fileobj and mapping are exclusive')
274 self._fields = {}
275 self.requires_files = []
276 self._dependencies = None
277 self.scheme = scheme
278 if path is not None:
279 self.read(path)
280 elif fileobj is not None:
281 self.read_file(fileobj)
282 elif mapping is not None:
283 self.update(mapping)
284 self.set_metadata_version()
285
286 def set_metadata_version(self):
287 self._fields['Metadata-Version'] = _best_version(self._fields)
288
289 def _write_field(self, fileobj, name, value):
290 fileobj.write('%s: %s\n' % (name, value))
291
292 def __getitem__(self, name):
293 return self.get(name)
294
295 def __setitem__(self, name, value):
296 return self.set(name, value)
297
298 def __delitem__(self, name):
299 field_name = self._convert_name(name)
300 try:
301 del self._fields[field_name]
302 except KeyError:
303 raise KeyError(name)
304
305 def __contains__(self, name):
306 return (name in self._fields or
307 self._convert_name(name) in self._fields)
308
309 def _convert_name(self, name):
310 if name in _ALL_FIELDS:
311 return name
312 name = name.replace('-', '_').lower()
313 return _ATTR2FIELD.get(name, name)
314
315 def _default_value(self, name):
316 if name in _LISTFIELDS or name in _ELEMENTSFIELD:
317 return []
318 return 'UNKNOWN'
319
320 def _remove_line_prefix(self, value):
321 if self.metadata_version in ('1.0', '1.1'):
322 return _LINE_PREFIX_PRE_1_2.sub('\n', value)
323 else:
324 return _LINE_PREFIX_1_2.sub('\n', value)
325
326 def __getattr__(self, name):
327 if name in _ATTR2FIELD:
328 return self[name]
329 raise AttributeError(name)
330
331 #
332 # Public API
333 #
334
335# dependencies = property(_get_dependencies, _set_dependencies)
336
337 def get_fullname(self, filesafe=False):
338 """Return the distribution name with version.
339
340 If filesafe is true, return a filename-escaped form."""
341 return _get_name_and_version(self['Name'], self['Version'], filesafe)
342
343 def is_field(self, name):
344 """return True if name is a valid metadata key"""
345 name = self._convert_name(name)
346 return name in _ALL_FIELDS
347
348 def is_multi_field(self, name):
349 name = self._convert_name(name)
350 return name in _LISTFIELDS
351
352 def read(self, filepath):
353 """Read the metadata values from a file path."""
354 fp = codecs.open(filepath, 'r', encoding='utf-8')
355 try:
356 self.read_file(fp)
357 finally:
358 fp.close()
359
360 def read_file(self, fileob):
361 """Read the metadata values from a file object."""
362 msg = message_from_file(fileob)
363 self._fields['Metadata-Version'] = msg['metadata-version']
364
365 # When reading, get all the fields we can
366 for field in _ALL_FIELDS:
367 if field not in msg:
368 continue
369 if field in _LISTFIELDS:
370 # we can have multiple lines
371 values = msg.get_all(field)
372 if field in _LISTTUPLEFIELDS and values is not None:
373 values = [tuple(value.split(',')) for value in values]
374 self.set(field, values)
375 else:
376 # single line
377 value = msg[field]
378 if value is not None and value != 'UNKNOWN':
379 self.set(field, value)
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 """
567 self.set_metadata_version()
568
569 mapping_1_0 = (
570 ('metadata_version', 'Metadata-Version'),
571 ('name', 'Name'),
572 ('version', 'Version'),
573 ('summary', 'Summary'),
574 ('home_page', 'Home-page'),
575 ('author', 'Author'),
576 ('author_email', 'Author-email'),
577 ('license', 'License'),
578 ('description', 'Description'),
579 ('keywords', 'Keywords'),
580 ('platform', 'Platform'),
581 ('classifiers', 'Classifier'),
582 ('download_url', 'Download-URL'),
583 )
584
585 data = {}
586 for key, field_name in mapping_1_0:
587 if not skip_missing or field_name in self._fields:
588 data[key] = self[field_name]
589
590 if self['Metadata-Version'] == '1.2':
591 mapping_1_2 = (
592 ('requires_dist', 'Requires-Dist'),
593 ('requires_python', 'Requires-Python'),
594 ('requires_external', 'Requires-External'),
595 ('provides_dist', 'Provides-Dist'),
596 ('obsoletes_dist', 'Obsoletes-Dist'),
597 ('project_url', 'Project-URL'),
598 ('maintainer', 'Maintainer'),
599 ('maintainer_email', 'Maintainer-email'),
600 )
601 for key, field_name in mapping_1_2:
602 if not skip_missing or field_name in self._fields:
603 if key != 'project_url':
604 data[key] = self[field_name]
605 else:
606 data[key] = [','.join(u) for u in self[field_name]]
607
608 elif self['Metadata-Version'] == '1.1':
609 mapping_1_1 = (
610 ('provides', 'Provides'),
611 ('requires', 'Requires'),
612 ('obsoletes', 'Obsoletes'),
613 )
614 for key, field_name in mapping_1_1:
615 if not skip_missing or field_name in self._fields:
616 data[key] = self[field_name]
617
618 return data
619
620 def add_requirements(self, requirements):
621 if self['Metadata-Version'] == '1.1':
622 # we can't have 1.1 metadata *and* Setuptools requires
623 for field in ('Obsoletes', 'Requires', 'Provides'):
624 if field in self:
625 del self[field]
626 self['Requires-Dist'] += requirements
627
628 # Mapping API
629 # TODO could add iter* variants
630
631 def keys(self):
632 return list(_version2fieldlist(self['Metadata-Version']))
633
634 def __iter__(self):
635 for key in self.keys():
636 yield key
637
638 def values(self):
639 return [self[key] for key in self.keys()]
640
641 def items(self):
642 return [(key, self[key]) for key in self.keys()]
643
644 def __repr__(self):
645 return '<%s %s %s>' % (self.__class__.__name__, self.name,
646 self.version)
647
648
649METADATA_FILENAME = 'pydist.json'
650WHEEL_METADATA_FILENAME = 'metadata.json'
651
652
653class Metadata(object):
654 """
655 The metadata of a release. This implementation uses 2.0 (JSON)
656 metadata where possible. If not possible, it wraps a LegacyMetadata
657 instance which handles the key-value metadata format.
658 """
659
660 METADATA_VERSION_MATCHER = re.compile(r'^\d+(\.\d+)*$')
661
662 NAME_MATCHER = re.compile('^[0-9A-Z]([0-9A-Z_.-]*[0-9A-Z])?$', re.I)
663
664 VERSION_MATCHER = PEP440_VERSION_RE
665
666 SUMMARY_MATCHER = re.compile('.{1,2047}')
667
668 METADATA_VERSION = '2.0'
669
670 GENERATOR = 'distlib (%s)' % __version__
671
672 MANDATORY_KEYS = {
673 'name': (),
674 'version': (),
675 'summary': ('legacy',),
676 }
677
678 INDEX_KEYS = ('name version license summary description author '
679 'author_email keywords platform home_page classifiers '
680 'download_url')
681
682 DEPENDENCY_KEYS = ('extras run_requires test_requires build_requires '
683 'dev_requires provides meta_requires obsoleted_by '
684 'supports_environments')
685
686 SYNTAX_VALIDATORS = {
687 'metadata_version': (METADATA_VERSION_MATCHER, ()),
688 'name': (NAME_MATCHER, ('legacy',)),
689 'version': (VERSION_MATCHER, ('legacy',)),
690 'summary': (SUMMARY_MATCHER, ('legacy',)),
691 }
692
693 __slots__ = ('_legacy', '_data', 'scheme')
694
695 def __init__(self, path=None, fileobj=None, mapping=None,
696 scheme='default'):
697 if [path, fileobj, mapping].count(None) < 2:
698 raise TypeError('path, fileobj and mapping are exclusive')
699 self._legacy = None
700 self._data = None
701 self.scheme = scheme
702 #import pdb; pdb.set_trace()
703 if mapping is not None:
704 try:
705 self._validate_mapping(mapping, scheme)
706 self._data = mapping
707 except MetadataUnrecognizedVersionError:
708 self._legacy = LegacyMetadata(mapping=mapping, scheme=scheme)
709 self.validate()
710 else:
711 data = None
712 if path:
713 with open(path, 'rb') as f:
714 data = f.read()
715 elif fileobj:
716 data = fileobj.read()
717 if data is None:
718 # Initialised with no args - to be added
719 self._data = {
720 'metadata_version': self.METADATA_VERSION,
721 'generator': self.GENERATOR,
722 }
723 else:
724 if not isinstance(data, text_type):
725 data = data.decode('utf-8')
726 try:
727 self._data = json.loads(data)
728 self._validate_mapping(self._data, scheme)
729 except ValueError:
730 # Note: MetadataUnrecognizedVersionError does not
731 # inherit from ValueError (it's a DistlibException,
732 # which should not inherit from ValueError).
733 # The ValueError comes from the json.load - if that
734 # succeeds and we get a validation error, we want
735 # that to propagate
736 self._legacy = LegacyMetadata(fileobj=StringIO(data),
737 scheme=scheme)
738 self.validate()
739
740 common_keys = set(('name', 'version', 'license', 'keywords', 'summary'))
741
742 none_list = (None, list)
743 none_dict = (None, dict)
744
745 mapped_keys = {
746 'run_requires': ('Requires-Dist', list),
747 'build_requires': ('Setup-Requires-Dist', list),
748 'dev_requires': none_list,
749 'test_requires': none_list,
750 'meta_requires': none_list,
751 'extras': ('Provides-Extra', list),
752 'modules': none_list,
753 'namespaces': none_list,
754 'exports': none_dict,
755 'commands': none_dict,
756 'classifiers': ('Classifier', list),
757 'source_url': ('Download-URL', None),
758 'metadata_version': ('Metadata-Version', None),
759 }
760
761 del none_list, none_dict
762
763 def __getattribute__(self, key):
764 common = object.__getattribute__(self, 'common_keys')
765 mapped = object.__getattribute__(self, 'mapped_keys')
766 if key in mapped:
767 lk, maker = mapped[key]
768 if self._legacy:
769 if lk is None:
770 result = None if maker is None else maker()
771 else:
772 result = self._legacy.get(lk)
773 else:
774 value = None if maker is None else maker()
775 if key not in ('commands', 'exports', 'modules', 'namespaces',
776 'classifiers'):
777 result = self._data.get(key, value)
778 else:
779 # special cases for PEP 459
780 sentinel = object()
781 result = sentinel
782 d = self._data.get('extensions')
783 if d:
784 if key == 'commands':
785 result = d.get('python.commands', value)
786 elif key == 'classifiers':
787 d = d.get('python.details')
788 if d:
789 result = d.get(key, value)
790 else:
791 d = d.get('python.exports')
792 if not d:
793 d = self._data.get('python.exports')
794 if d:
795 result = d.get(key, value)
796 if result is sentinel:
797 result = value
798 elif key not in common:
799 result = object.__getattribute__(self, key)
800 elif self._legacy:
801 result = self._legacy.get(key)
802 else:
803 result = self._data.get(key)
804 return result
805
806 def _validate_value(self, key, value, scheme=None):
807 if key in self.SYNTAX_VALIDATORS:
808 pattern, exclusions = self.SYNTAX_VALIDATORS[key]
809 if (scheme or self.scheme) not in exclusions:
810 m = pattern.match(value)
811 if not m:
812 raise MetadataInvalidError("'%s' is an invalid value for "
813 "the '%s' property" % (value,
814 key))
815
816 def __setattr__(self, key, value):
817 self._validate_value(key, value)
818 common = object.__getattribute__(self, 'common_keys')
819 mapped = object.__getattribute__(self, 'mapped_keys')
820 if key in mapped:
821 lk, _ = mapped[key]
822 if self._legacy:
823 if lk is None:
824 raise NotImplementedError
825 self._legacy[lk] = value
826 elif key not in ('commands', 'exports', 'modules', 'namespaces',
827 'classifiers'):
828 self._data[key] = value
829 else:
830 # special cases for PEP 459
831 d = self._data.setdefault('extensions', {})
832 if key == 'commands':
833 d['python.commands'] = value
834 elif key == 'classifiers':
835 d = d.setdefault('python.details', {})
836 d[key] = value
837 else:
838 d = d.setdefault('python.exports', {})
839 d[key] = value
840 elif key not in common:
841 object.__setattr__(self, key, value)
842 else:
843 if key == 'keywords':
844 if isinstance(value, string_types):
845 value = value.strip()
846 if value:
847 value = value.split()
848 else:
849 value = []
850 if self._legacy:
851 self._legacy[key] = value
852 else:
853 self._data[key] = value
854
855 @property
856 def name_and_version(self):
857 return _get_name_and_version(self.name, self.version, True)
858
859 @property
860 def provides(self):
861 if self._legacy:
862 result = self._legacy['Provides-Dist']
863 else:
864 result = self._data.setdefault('provides', [])
865 s = '%s (%s)' % (self.name, self.version)
866 if s not in result:
867 result.append(s)
868 return result
869
870 @provides.setter
871 def provides(self, value):
872 if self._legacy:
873 self._legacy['Provides-Dist'] = value
874 else:
875 self._data['provides'] = value
876
877 def get_requirements(self, reqts, extras=None, env=None):
878 """
879 Base method to get dependencies, given a set of extras
880 to satisfy and an optional environment context.
881 :param reqts: A list of sometimes-wanted dependencies,
882 perhaps dependent on extras and environment.
883 :param extras: A list of optional components being requested.
884 :param env: An optional environment for marker evaluation.
885 """
886 if self._legacy:
887 result = reqts
888 else:
889 result = []
890 extras = get_extras(extras or [], self.extras)
891 for d in reqts:
892 if 'extra' not in d and 'environment' not in d:
893 # unconditional
894 include = True
895 else:
896 if 'extra' not in d:
897 # Not extra-dependent - only environment-dependent
898 include = True
899 else:
900 include = d.get('extra') in extras
901 if include:
902 # Not excluded because of extras, check environment
903 marker = d.get('environment')
904 if marker:
905 include = interpret(marker, env)
906 if include:
907 result.extend(d['requires'])
908 for key in ('build', 'dev', 'test'):
909 e = ':%s:' % key
910 if e in extras:
911 extras.remove(e)
912 # A recursive call, but it should terminate since 'test'
913 # has been removed from the extras
914 reqts = self._data.get('%s_requires' % key, [])
915 result.extend(self.get_requirements(reqts, extras=extras,
916 env=env))
917 return result
918
919 @property
920 def dictionary(self):
921 if self._legacy:
922 return self._from_legacy()
923 return self._data
924
925 @property
926 def dependencies(self):
927 if self._legacy:
928 raise NotImplementedError
929 else:
930 return extract_by_key(self._data, self.DEPENDENCY_KEYS)
931
932 @dependencies.setter
933 def dependencies(self, value):
934 if self._legacy:
935 raise NotImplementedError
936 else:
937 self._data.update(value)
938
939 def _validate_mapping(self, mapping, scheme):
940 if mapping.get('metadata_version') != self.METADATA_VERSION:
941 raise MetadataUnrecognizedVersionError()
942 missing = []
943 for key, exclusions in self.MANDATORY_KEYS.items():
944 if key not in mapping:
945 if scheme not in exclusions:
946 missing.append(key)
947 if missing:
948 msg = 'Missing metadata items: %s' % ', '.join(missing)
949 raise MetadataMissingError(msg)
950 for k, v in mapping.items():
951 self._validate_value(k, v, scheme)
952
953 def validate(self):
954 if self._legacy:
955 missing, warnings = self._legacy.check(True)
956 if missing or warnings:
957 logger.warning('Metadata: missing: %s, warnings: %s',
958 missing, warnings)
959 else:
960 self._validate_mapping(self._data, self.scheme)
961
962 def todict(self):
963 if self._legacy:
964 return self._legacy.todict(True)
965 else:
966 result = extract_by_key(self._data, self.INDEX_KEYS)
967 return result
968
969 def _from_legacy(self):
970 assert self._legacy and not self._data
971 result = {
972 'metadata_version': self.METADATA_VERSION,
973 'generator': self.GENERATOR,
974 }
975 lmd = self._legacy.todict(True) # skip missing ones
976 for k in ('name', 'version', 'license', 'summary', 'description',
977 'classifier'):
978 if k in lmd:
979 if k == 'classifier':
980 nk = 'classifiers'
981 else:
982 nk = k
983 result[nk] = lmd[k]
984 kw = lmd.get('Keywords', [])
985 if kw == ['']:
986 kw = []
987 result['keywords'] = kw
988 keys = (('requires_dist', 'run_requires'),
989 ('setup_requires_dist', 'build_requires'))
990 for ok, nk in keys:
991 if ok in lmd and lmd[ok]:
992 result[nk] = [{'requires': lmd[ok]}]
993 result['provides'] = self.provides
994 author = {}
995 maintainer = {}
996 return result
997
998 LEGACY_MAPPING = {
999 'name': 'Name',
1000 'version': 'Version',
1001 'license': 'License',
1002 'summary': 'Summary',
1003 'description': 'Description',
1004 'classifiers': 'Classifier',
1005 }
1006
1007 def _to_legacy(self):
1008 def process_entries(entries):
1009 reqts = set()
1010 for e in entries:
1011 extra = e.get('extra')
1012 env = e.get('environment')
1013 rlist = e['requires']
1014 for r in rlist:
1015 if not env and not extra:
1016 reqts.add(r)
1017 else:
1018 marker = ''
1019 if extra:
1020 marker = 'extra == "%s"' % extra
1021 if env:
1022 if marker:
1023 marker = '(%s) and %s' % (env, marker)
1024 else:
1025 marker = env
1026 reqts.add(';'.join((r, marker)))
1027 return reqts
1028
1029 assert self._data and not self._legacy
1030 result = LegacyMetadata()
1031 nmd = self._data
1032 for nk, ok in self.LEGACY_MAPPING.items():
1033 if nk in nmd:
1034 result[ok] = nmd[nk]
1035 r1 = process_entries(self.run_requires + self.meta_requires)
1036 r2 = process_entries(self.build_requires + self.dev_requires)
1037 if self.extras:
1038 result['Provides-Extra'] = sorted(self.extras)
1039 result['Requires-Dist'] = sorted(r1)
1040 result['Setup-Requires-Dist'] = sorted(r2)
1041 # TODO: other fields such as contacts
1042 return result
1043
1044 def write(self, path=None, fileobj=None, legacy=False, skip_unknown=True):
1045 if [path, fileobj].count(None) != 1:
1046 raise ValueError('Exactly one of path and fileobj is needed')
1047 self.validate()
1048 if legacy:
1049 if self._legacy:
1050 legacy_md = self._legacy
1051 else:
1052 legacy_md = self._to_legacy()
1053 if path:
1054 legacy_md.write(path, skip_unknown=skip_unknown)
1055 else:
1056 legacy_md.write_file(fileobj, skip_unknown=skip_unknown)
1057 else:
1058 if self._legacy:
1059 d = self._from_legacy()
1060 else:
1061 d = self._data
1062 if fileobj:
1063 json.dump(d, fileobj, ensure_ascii=True, indent=2,
1064 sort_keys=True)
1065 else:
1066 with codecs.open(path, 'w', 'utf-8') as f:
1067 json.dump(d, f, ensure_ascii=True, indent=2,
1068 sort_keys=True)
1069
1070 def add_requirements(self, requirements):
1071 if self._legacy:
1072 self._legacy.add_requirements(requirements)
1073 else:
1074 run_requires = self._data.setdefault('run_requires', [])
1075 always = None
1076 for entry in run_requires:
1077 if 'environment' not in entry and 'extra' not in entry:
1078 always = entry
1079 break
1080 if always is None:
1081 always = { 'requires': requirements }
1082 run_requires.insert(0, always)
1083 else:
1084 rset = set(always['requires']) | set(requirements)
1085 always['requires'] = sorted(rset)
1086
1087 def __repr__(self):
1088 name = self.name or '(no name)'
1089 version = self.version or 'no version'
1090 return '<%s %s %s (%s)>' % (self.__class__.__name__,
1091 self.metadata_version, name, version)