1
# -*- coding: utf-8 -*-
3
# Copyright (C) 2012 The Python Software Foundation.
4
# See LICENSE.txt and CONTRIBUTORS.txt.
6
"""Implementation of the Metadata for Python packages PEPs.
8
Supports all metadata formats (1.0, 1.1, 1.2, and 2.0 experimental).
10
from __future__ import unicode_literals
13
from email import message_from_file
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
25
logger = logging.getLogger(__name__)
28
class MetadataMissingError(DistlibException):
29
"""A required metadata is missing"""
32
class MetadataConflictError(DistlibException):
33
"""Attempt to read or write metadata fields that are conflictual."""
36
class MetadataUnrecognizedVersionError(DistlibException):
37
"""Unknown metadata version number."""
40
class MetadataInvalidError(DistlibException):
41
"""A metadata value is invalid"""
43
# public API of this module
44
__all__ = ['Metadata', 'PKG_INFO_ENCODING', 'PKG_INFO_PREFERRED_VERSION']
46
# Encoding used for the PKG-INFO files
47
PKG_INFO_ENCODING = 'utf-8'
49
# preferred version. Hopefully will be changed
50
# to 1.2 once PEP 345 is supported everywhere
51
PKG_INFO_PREFERRED_VERSION = '1.1'
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',
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')
66
_314_MARKERS = ('Obsoletes', 'Provides', 'Requires', 'Classifier',
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')
77
_345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python',
78
'Obsoletes-Dist', 'Requires-External', 'Maintainer',
79
'Maintainer-email', 'Project-URL')
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',
91
_426_MARKERS = ('Private-Version', 'Provides-Extra', 'Obsoleted-By',
92
'Setup-Requires-Dist', 'Extension')
95
_ALL_FIELDS.update(_241_FIELDS)
96
_ALL_FIELDS.update(_314_FIELDS)
97
_ALL_FIELDS.update(_345_FIELDS)
98
_ALL_FIELDS.update(_426_FIELDS)
100
EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
103
def _version2fieldlist(version):
106
elif version == '1.1':
108
elif version == '1.2':
110
elif version == '2.0':
112
raise MetadataUnrecognizedVersionError(version)
115
def _best_version(fields):
116
"""Detect the best version depending on the fields used."""
117
def _has_marker(keys, markers):
118
for marker in markers:
124
for key, value in fields.items():
125
if value in ([], 'UNKNOWN', None):
129
possible_versions = ['1.0', '1.1', '1.2', '2.0']
131
# first let's try to see if a field is not part of one of the version
133
if key not in _241_FIELDS and '1.0' in possible_versions:
134
possible_versions.remove('1.0')
135
if key not in _314_FIELDS and '1.1' in possible_versions:
136
possible_versions.remove('1.1')
137
if key not in _345_FIELDS and '1.2' in possible_versions:
138
possible_versions.remove('1.2')
139
if key not in _426_FIELDS and '2.0' in possible_versions:
140
possible_versions.remove('2.0')
142
# possible_version contains qualified versions
143
if len(possible_versions) == 1:
144
return possible_versions[0] # found !
145
elif len(possible_versions) == 0:
146
raise MetadataConflictError('Unknown metadata set')
148
# let's see if one unique marker is found
149
is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS)
150
is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS)
151
is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
152
if int(is_1_1) + int(is_1_2) + int(is_2_0) > 1:
153
raise MetadataConflictError('You used incompatible 1.1/1.2/2.0 fields')
155
# we have the choice, 1.0, or 1.2, or 2.0
156
# - 1.0 has a broken Summary field but works with all tools
158
# - 1.2 fixes Summary but has little adoption
159
# - 2.0 adds more features and is very new
160
if not is_1_1 and not is_1_2 and not is_2_0:
161
# we couldn't find any specific marker
162
if PKG_INFO_PREFERRED_VERSION in possible_versions:
163
return PKG_INFO_PREFERRED_VERSION
172
'metadata_version': 'Metadata-Version',
174
'version': 'Version',
175
'platform': 'Platform',
176
'supported_platform': 'Supported-Platform',
177
'summary': 'Summary',
178
'description': 'Description',
179
'keywords': 'Keywords',
180
'home_page': 'Home-page',
182
'author_email': 'Author-email',
183
'maintainer': 'Maintainer',
184
'maintainer_email': 'Maintainer-email',
185
'license': 'License',
186
'classifier': 'Classifier',
187
'download_url': 'Download-URL',
188
'obsoletes_dist': 'Obsoletes-Dist',
189
'provides_dist': 'Provides-Dist',
190
'requires_dist': 'Requires-Dist',
191
'setup_requires_dist': 'Setup-Requires-Dist',
192
'requires_python': 'Requires-Python',
193
'requires_external': 'Requires-External',
194
'requires': 'Requires',
195
'provides': 'Provides',
196
'obsoletes': 'Obsoletes',
197
'project_url': 'Project-URL',
198
'private_version': 'Private-Version',
199
'obsoleted_by': 'Obsoleted-By',
200
'extension': 'Extension',
201
'provides_extra': 'Provides-Extra',
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',)
214
_ELEMENTSFIELD = ('Keywords',)
216
_UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description')
220
_FILESAFE = re.compile('[^A-Za-z0-9.]+')
223
def _get_name_and_version(name, version, for_filename=False):
224
"""Return the distribution name with version.
226
If for_filename is true, return a filename-escaped form."""
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)
236
class LegacyMetadata(object):
237
"""The legacy metadata of a release.
239
Supports versions 1.0, 1.1 and 1.2 (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
246
# TODO document the mapping API and UNKNOWN default key
248
def __init__(self, path=None, fileobj=None, mapping=None,
250
if [path, fileobj, mapping].count(None) < 2:
251
raise TypeError('path, fileobj and mapping are exclusive')
253
self.requires_files = []
254
self._dependencies = None
258
elif fileobj is not None:
259
self.read_file(fileobj)
260
elif mapping is not None:
262
self.set_metadata_version()
264
def set_metadata_version(self):
265
self._fields['Metadata-Version'] = _best_version(self._fields)
267
def _write_field(self, fileobj, name, value):
268
fileobj.write('%s: %s\n' % (name, value))
270
def __getitem__(self, name):
271
return self.get(name)
273
def __setitem__(self, name, value):
274
return self.set(name, value)
276
def __delitem__(self, name):
277
field_name = self._convert_name(name)
279
del self._fields[field_name]
283
def __contains__(self, name):
284
return (name in self._fields or
285
self._convert_name(name) in self._fields)
287
def _convert_name(self, name):
288
if name in _ALL_FIELDS:
290
name = name.replace('-', '_').lower()
291
return _ATTR2FIELD.get(name, name)
293
def _default_value(self, name):
294
if name in _LISTFIELDS or name in _ELEMENTSFIELD:
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)
302
return _LINE_PREFIX_1_2.sub('\n', value)
304
def __getattr__(self, name):
305
if name in _ATTR2FIELD:
307
raise AttributeError(name)
313
# dependencies = property(_get_dependencies, _set_dependencies)
315
def get_fullname(self, filesafe=False):
316
"""Return the distribution name with version.
318
If filesafe is true, return a filename-escaped form."""
319
return _get_name_and_version(self['Name'], self['Version'], filesafe)
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
326
def is_multi_field(self, name):
327
name = self._convert_name(name)
328
return name in _LISTFIELDS
330
def read(self, filepath):
331
"""Read the metadata values from a file path."""
332
fp = codecs.open(filepath, 'r', encoding='utf-8')
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']
343
# When reading, get all the fields we can
344
for field in _ALL_FIELDS:
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)
356
if value is not None and value != 'UNKNOWN':
357
self.set(field, value)
358
self.set_metadata_version()
360
def write(self, filepath, skip_unknown=False):
361
"""Write the metadata fields to filepath."""
362
fp = codecs.open(filepath, 'w', encoding='utf-8')
364
self.write_file(fp, skip_unknown)
368
def write_file(self, fileobject, skip_unknown=False):
369
"""Write the PKG-INFO format data to a file object."""
370
self.set_metadata_version()
372
for field in _version2fieldlist(self['Metadata-Version']):
373
values = self.get(field)
374
if skip_unknown and values in ('UNKNOWN', [], ['UNKNOWN']):
376
if field in _ELEMENTSFIELD:
377
self._write_field(fileobject, field, ','.join(values))
379
if field not in _LISTFIELDS:
380
if field == 'Description':
381
if self.metadata_version in ('1.0', '1.1'):
382
values = values.replace('\n', '\n ')
384
values = values.replace('\n', '\n |')
387
if field in _LISTTUPLEFIELDS:
388
values = [','.join(value) for value in values]
391
self._write_field(fileobject, field, value)
393
def update(self, other=None, **kwargs):
394
"""Set metadata values from the given iterable `other` and kwargs.
396
Behavior is like `dict.update`: If `other` has a ``keys`` method,
397
they are looped over and ``self[key]`` is assigned ``other[key]``.
398
Else, ``other`` is an iterable of ``(key, value)`` iterables.
400
Keys that don't match a metadata field or that have an empty value are
403
def _set(key, value):
404
if key in _ATTR2FIELD and value:
405
self.set(self._convert_name(key), value)
408
# other is None or empty container
410
elif hasattr(other, 'keys'):
411
for k in other.keys():
418
for k, v in kwargs.items():
421
def set(self, name, value):
422
"""Control then set a metadata field."""
423
name = self._convert_name(name)
425
if ((name in _ELEMENTSFIELD or name == 'Platform') and
426
not isinstance(value, (list, tuple))):
427
if isinstance(value, string_types):
428
value = [v.strip() for v in value.split(',')]
431
elif (name in _LISTFIELDS and
432
not isinstance(value, (list, tuple))):
433
if isinstance(value, string_types):
438
if logger.isEnabledFor(logging.WARNING):
439
project_name = self['Name']
441
scheme = get_scheme(self.scheme)
442
if name in _PREDICATE_FIELDS and value is not None:
444
# check that the values are valid
445
if not scheme.is_valid_matcher(v.split(';')[0]):
447
"'%s': '%s' is not valid (field '%s')",
448
project_name, v, name)
449
# FIXME this rejects UNKNOWN, is that right?
450
elif name in _VERSIONS_FIELDS and value is not None:
451
if not scheme.is_valid_constraint_list(value):
452
logger.warning("'%s': '%s' is not a valid version (field '%s')",
453
project_name, value, name)
454
elif name in _VERSION_FIELDS and value is not None:
455
if not scheme.is_valid_version(value):
456
logger.warning("'%s': '%s' is not a valid version (field '%s')",
457
project_name, value, name)
459
if name in _UNICODEFIELDS:
460
if name == 'Description':
461
value = self._remove_line_prefix(value)
463
self._fields[name] = value
465
def get(self, name, default=_MISSING):
466
"""Get a metadata field."""
467
name = self._convert_name(name)
468
if name not in self._fields:
469
if default is _MISSING:
470
default = self._default_value(name)
472
if name in _UNICODEFIELDS:
473
value = self._fields[name]
475
elif name in _LISTFIELDS:
476
value = self._fields[name]
481
if name not in _LISTTUPLEFIELDS:
484
# That's for Project-URL
485
res.append((val[0], val[1]))
488
elif name in _ELEMENTSFIELD:
489
value = self._fields[name]
490
if isinstance(value, string_types):
491
return value.split(',')
492
return self._fields[name]
494
def check(self, strict=False):
495
"""Check if the metadata is compliant. If strict is True then raise if
496
no Name or Version are provided"""
497
self.set_metadata_version()
499
# XXX should check the versions (if the file was loaded)
500
missing, warnings = [], []
502
for attr in ('Name', 'Version'): # required by PEP 345
506
if strict and missing != []:
507
msg = 'missing required metadata: %s' % ', '.join(missing)
508
raise MetadataMissingError(msg)
510
for attr in ('Home-page', 'Author'):
514
# checking metadata 1.2 (XXX needs to check 1.1, 1.0)
515
if self['Metadata-Version'] != '1.2':
516
return missing, warnings
518
scheme = get_scheme(self.scheme)
520
def are_valid_constraints(value):
522
if not scheme.is_valid_matcher(v.split(';')[0]):
526
for fields, controller in ((_PREDICATE_FIELDS, are_valid_constraints),
528
scheme.is_valid_constraint_list),
530
scheme.is_valid_version)):
532
value = self.get(field, None)
533
if value is not None and not controller(value):
534
warnings.append("Wrong value for '%s': %s" % (field, value))
536
return missing, warnings
538
def todict(self, skip_missing=False):
539
"""Return fields as a dict.
541
Field names will be converted to use the underscore-lowercase style
542
instead of hyphen-mixed case (i.e. home_page instead of Home-page).
544
self.set_metadata_version()
547
('metadata_version', 'Metadata-Version'),
549
('version', 'Version'),
550
('summary', 'Summary'),
551
('home_page', 'Home-page'),
552
('author', 'Author'),
553
('author_email', 'Author-email'),
554
('license', 'License'),
555
('description', 'Description'),
556
('keywords', 'Keywords'),
557
('platform', 'Platform'),
558
('classifiers', 'Classifier'),
559
('download_url', 'Download-URL'),
563
for key, field_name in mapping_1_0:
564
if not skip_missing or field_name in self._fields:
565
data[key] = self[field_name]
567
if self['Metadata-Version'] == '1.2':
569
('requires_dist', 'Requires-Dist'),
570
('requires_python', 'Requires-Python'),
571
('requires_external', 'Requires-External'),
572
('provides_dist', 'Provides-Dist'),
573
('obsoletes_dist', 'Obsoletes-Dist'),
574
('project_url', 'Project-URL'),
575
('maintainer', 'Maintainer'),
576
('maintainer_email', 'Maintainer-email'),
578
for key, field_name in mapping_1_2:
579
if not skip_missing or field_name in self._fields:
580
if key != 'project_url':
581
data[key] = self[field_name]
583
data[key] = [','.join(u) for u in self[field_name]]
585
elif self['Metadata-Version'] == '1.1':
587
('provides', 'Provides'),
588
('requires', 'Requires'),
589
('obsoletes', 'Obsoletes'),
591
for key, field_name in mapping_1_1:
592
if not skip_missing or field_name in self._fields:
593
data[key] = self[field_name]
597
def add_requirements(self, requirements):
598
if self['Metadata-Version'] == '1.1':
599
# we can't have 1.1 metadata *and* Setuptools requires
600
for field in ('Obsoletes', 'Requires', 'Provides'):
603
self['Requires-Dist'] += requirements
606
# TODO could add iter* variants
609
return list(_version2fieldlist(self['Metadata-Version']))
612
for key in self.keys():
616
return [self[key] for key in self.keys()]
619
return [(key, self[key]) for key in self.keys()]
622
return '<%s %s %s>' % (self.__class__.__name__, self.name,
626
METADATA_FILENAME = 'pydist.json'
627
WHEEL_METADATA_FILENAME = 'metadata.json'
630
class Metadata(object):
632
The metadata of a release. This implementation uses 2.0 (JSON)
633
metadata where possible. If not possible, it wraps a LegacyMetadata
634
instance which handles the key-value metadata format.
637
METADATA_VERSION_MATCHER = re.compile('^\d+(\.\d+)*$')
639
NAME_MATCHER = re.compile('^[0-9A-Z]([0-9A-Z_.-]*[0-9A-Z])?$', re.I)
641
VERSION_MATCHER = PEP440_VERSION_RE
643
SUMMARY_MATCHER = re.compile('.{1,2047}')
645
METADATA_VERSION = '2.0'
647
GENERATOR = 'distlib (%s)' % __version__
652
'summary': ('legacy',),
655
INDEX_KEYS = ('name version license summary description author '
656
'author_email keywords platform home_page classifiers '
659
DEPENDENCY_KEYS = ('extras run_requires test_requires build_requires '
660
'dev_requires provides meta_requires obsoleted_by '
661
'supports_environments')
663
SYNTAX_VALIDATORS = {
664
'metadata_version': (METADATA_VERSION_MATCHER, ()),
665
'name': (NAME_MATCHER, ('legacy',)),
666
'version': (VERSION_MATCHER, ('legacy',)),
667
'summary': (SUMMARY_MATCHER, ('legacy',)),
670
__slots__ = ('_legacy', '_data', 'scheme')
672
def __init__(self, path=None, fileobj=None, mapping=None,
674
if [path, fileobj, mapping].count(None) < 2:
675
raise TypeError('path, fileobj and mapping are exclusive')
679
#import pdb; pdb.set_trace()
680
if mapping is not None:
682
self._validate_mapping(mapping, scheme)
684
except MetadataUnrecognizedVersionError:
685
self._legacy = LegacyMetadata(mapping=mapping, scheme=scheme)
690
with open(path, 'rb') as f:
693
data = fileobj.read()
695
# Initialised with no args - to be added
697
'metadata_version': self.METADATA_VERSION,
698
'generator': self.GENERATOR,
701
if not isinstance(data, text_type):
702
data = data.decode('utf-8')
704
self._data = json.loads(data)
705
self._validate_mapping(self._data, scheme)
707
# Note: MetadataUnrecognizedVersionError does not
708
# inherit from ValueError (it's a DistlibException,
709
# which should not inherit from ValueError).
710
# The ValueError comes from the json.load - if that
711
# succeeds and we get a validation error, we want
713
self._legacy = LegacyMetadata(fileobj=StringIO(data),
717
common_keys = set(('name', 'version', 'license', 'keywords', 'summary'))
719
none_list = (None, list)
720
none_dict = (None, dict)
723
'run_requires': ('Requires-Dist', list),
724
'build_requires': ('Setup-Requires-Dist', list),
725
'dev_requires': none_list,
726
'test_requires': none_list,
727
'meta_requires': none_list,
728
'extras': ('Provides-Extra', list),
729
'modules': none_list,
730
'namespaces': none_list,
731
'exports': none_dict,
732
'commands': none_dict,
733
'classifiers': ('Classifier', list),
734
'source_url': ('Download-URL', None),
735
'metadata_version': ('Metadata-Version', None),
738
del none_list, none_dict
740
def __getattribute__(self, key):
741
common = object.__getattribute__(self, 'common_keys')
742
mapped = object.__getattribute__(self, 'mapped_keys')
744
lk, maker = mapped[key]
747
result = None if maker is None else maker()
749
result = self._legacy.get(lk)
751
value = None if maker is None else maker()
752
if key not in ('commands', 'exports', 'modules', 'namespaces',
754
result = self._data.get(key, value)
756
# special cases for PEP 459
759
d = self._data.get('extensions')
761
if key == 'commands':
762
result = d.get('python.commands', value)
763
elif key == 'classifiers':
764
d = d.get('python.details')
766
result = d.get(key, value)
768
d = d.get('python.exports')
770
d = self._data.get('python.exports')
772
result = d.get(key, value)
773
if result is sentinel:
775
elif key not in common:
776
result = object.__getattribute__(self, key)
778
result = self._legacy.get(key)
780
result = self._data.get(key)
783
def _validate_value(self, key, value, scheme=None):
784
if key in self.SYNTAX_VALIDATORS:
785
pattern, exclusions = self.SYNTAX_VALIDATORS[key]
786
if (scheme or self.scheme) not in exclusions:
787
m = pattern.match(value)
789
raise MetadataInvalidError("'%s' is an invalid value for "
790
"the '%s' property" % (value,
793
def __setattr__(self, key, value):
794
self._validate_value(key, value)
795
common = object.__getattribute__(self, 'common_keys')
796
mapped = object.__getattribute__(self, 'mapped_keys')
801
raise NotImplementedError
802
self._legacy[lk] = value
803
elif key not in ('commands', 'exports', 'modules', 'namespaces',
805
self._data[key] = value
807
# special cases for PEP 459
808
d = self._data.setdefault('extensions', {})
809
if key == 'commands':
810
d['python.commands'] = value
811
elif key == 'classifiers':
812
d = d.setdefault('python.details', {})
815
d = d.setdefault('python.exports', {})
817
elif key not in common:
818
object.__setattr__(self, key, value)
820
if key == 'keywords':
821
if isinstance(value, string_types):
822
value = value.strip()
824
value = value.split()
828
self._legacy[key] = value
830
self._data[key] = value
833
def name_and_version(self):
834
return _get_name_and_version(self.name, self.version, True)
839
result = self._legacy['Provides-Dist']
841
result = self._data.setdefault('provides', [])
842
s = '%s (%s)' % (self.name, self.version)
848
def provides(self, value):
850
self._legacy['Provides-Dist'] = value
852
self._data['provides'] = value
854
def get_requirements(self, reqts, extras=None, env=None):
856
Base method to get dependencies, given a set of extras
857
to satisfy and an optional environment context.
858
:param reqts: A list of sometimes-wanted dependencies,
859
perhaps dependent on extras and environment.
860
:param extras: A list of optional components being requested.
861
:param env: An optional environment for marker evaluation.
867
extras = get_extras(extras or [], self.extras)
869
if 'extra' not in d and 'environment' not in d:
874
# Not extra-dependent - only environment-dependent
877
include = d.get('extra') in extras
879
# Not excluded because of extras, check environment
880
marker = d.get('environment')
882
include = interpret(marker, env)
884
result.extend(d['requires'])
885
for key in ('build', 'dev', 'test'):
889
# A recursive call, but it should terminate since 'test'
890
# has been removed from the extras
891
reqts = self._data.get('%s_requires' % key, [])
892
result.extend(self.get_requirements(reqts, extras=extras,
897
def dictionary(self):
899
return self._from_legacy()
903
def dependencies(self):
905
raise NotImplementedError
907
return extract_by_key(self._data, self.DEPENDENCY_KEYS)
910
def dependencies(self, value):
912
raise NotImplementedError
914
self._data.update(value)
916
def _validate_mapping(self, mapping, scheme):
917
if mapping.get('metadata_version') != self.METADATA_VERSION:
918
raise MetadataUnrecognizedVersionError()
920
for key, exclusions in self.MANDATORY_KEYS.items():
921
if key not in mapping:
922
if scheme not in exclusions:
925
msg = 'Missing metadata items: %s' % ', '.join(missing)
926
raise MetadataMissingError(msg)
927
for k, v in mapping.items():
928
self._validate_value(k, v, scheme)
932
missing, warnings = self._legacy.check(True)
933
if missing or warnings:
934
logger.warning('Metadata: missing: %s, warnings: %s',
937
self._validate_mapping(self._data, self.scheme)
941
return self._legacy.todict(True)
943
result = extract_by_key(self._data, self.INDEX_KEYS)
946
def _from_legacy(self):
947
assert self._legacy and not self._data
949
'metadata_version': self.METADATA_VERSION,
950
'generator': self.GENERATOR,
952
lmd = self._legacy.todict(True) # skip missing ones
953
for k in ('name', 'version', 'license', 'summary', 'description',
956
if k == 'classifier':
961
kw = lmd.get('Keywords', [])
964
result['keywords'] = kw
965
keys = (('requires_dist', 'run_requires'),
966
('setup_requires_dist', 'build_requires'))
968
if ok in lmd and lmd[ok]:
969
result[nk] = [{'requires': lmd[ok]}]
970
result['provides'] = self.provides
977
'version': 'Version',
978
'license': 'License',
979
'summary': 'Summary',
980
'description': 'Description',
981
'classifiers': 'Classifier',
984
def _to_legacy(self):
985
def process_entries(entries):
988
extra = e.get('extra')
989
env = e.get('environment')
990
rlist = e['requires']
992
if not env and not extra:
997
marker = 'extra == "%s"' % extra
1000
marker = '(%s) and %s' % (env, marker)
1003
reqts.add(';'.join((r, marker)))
1006
assert self._data and not self._legacy
1007
result = LegacyMetadata()
1009
for nk, ok in self.LEGACY_MAPPING.items():
1011
result[ok] = nmd[nk]
1012
r1 = process_entries(self.run_requires + self.meta_requires)
1013
r2 = process_entries(self.build_requires + self.dev_requires)
1015
result['Provides-Extra'] = sorted(self.extras)
1016
result['Requires-Dist'] = sorted(r1)
1017
result['Setup-Requires-Dist'] = sorted(r2)
1018
# TODO: other fields such as contacts
1021
def write(self, path=None, fileobj=None, legacy=False, skip_unknown=True):
1022
if [path, fileobj].count(None) != 1:
1023
raise ValueError('Exactly one of path and fileobj is needed')
1027
legacy_md = self._legacy
1029
legacy_md = self._to_legacy()
1031
legacy_md.write(path, skip_unknown=skip_unknown)
1033
legacy_md.write_file(fileobj, skip_unknown=skip_unknown)
1036
d = self._from_legacy()
1040
json.dump(d, fileobj, ensure_ascii=True, indent=2,
1043
with codecs.open(path, 'w', 'utf-8') as f:
1044
json.dump(d, f, ensure_ascii=True, indent=2,
1047
def add_requirements(self, requirements):
1049
self._legacy.add_requirements(requirements)
1051
run_requires = self._data.setdefault('run_requires', [])
1053
for entry in run_requires:
1054
if 'environment' not in entry and 'extra' not in entry:
1058
always = { 'requires': requirements }
1059
run_requires.insert(0, always)
1061
rset = set(always['requires']) | set(requirements)
1062
always['requires'] = sorted(rset)
1065
name = self.name or '(no name)'
1066
version = self.version or 'no version'
1067
return '<%s %s %s (%s)>' % (self.__class__.__name__,
1068
self.metadata_version, name, version)