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, 1.3/2.1 and 2.2).
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')
94 # See issue #106: Sometimes 'Requires' and 'Provides' occur wrongly in
95 # the metadata. Include them in the tuple literal below to allow them
97 # Ditto for Obsoletes - see issue #140.
98 _566_FIELDS
= _426_FIELDS
+ ('Description-Content-Type',
99 'Requires', 'Provides', 'Obsoletes')
101 _566_MARKERS
= ('Description-Content-Type',)
103 _643_MARKERS
= ('Dynamic', 'License-File')
105 _643_FIELDS
= _566_FIELDS
+ _643_MARKERS
108 _ALL_FIELDS
.update(_241_FIELDS
)
109 _ALL_FIELDS
.update(_314_FIELDS
)
110 _ALL_FIELDS
.update(_345_FIELDS
)
111 _ALL_FIELDS
.update(_426_FIELDS
)
112 _ALL_FIELDS
.update(_566_FIELDS
)
113 _ALL_FIELDS
.update(_643_FIELDS
)
115 EXTRA_RE
= re
.compile(r
'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
118 def _version2fieldlist(version
):
121 elif version
== '1.1':
123 elif version
== '1.2':
125 elif version
in ('1.3', '2.1'):
126 # avoid adding field names if already there
127 return _345_FIELDS
+ tuple(f
for f
in _566_FIELDS
if f
not in _345_FIELDS
)
128 elif version
== '2.0':
129 raise ValueError('Metadata 2.0 is withdrawn and not supported')
131 elif version
== '2.2':
133 raise MetadataUnrecognizedVersionError(version
)
136 def _best_version(fields
):
137 """Detect the best version depending on the fields used."""
138 def _has_marker(keys
, markers
):
139 for marker
in markers
:
145 for key
, value
in fields
.items():
146 if value
in ([], 'UNKNOWN', None):
150 possible_versions
= ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2'] # 2.0 removed
152 # first let's try to see if a field is not part of one of the version
154 if key
not in _241_FIELDS
and '1.0' in possible_versions
:
155 possible_versions
.remove('1.0')
156 logger
.debug('Removed 1.0 due to %s', key
)
157 if key
not in _314_FIELDS
and '1.1' in possible_versions
:
158 possible_versions
.remove('1.1')
159 logger
.debug('Removed 1.1 due to %s', key
)
160 if key
not in _345_FIELDS
and '1.2' in possible_versions
:
161 possible_versions
.remove('1.2')
162 logger
.debug('Removed 1.2 due to %s', key
)
163 if key
not in _566_FIELDS
and '1.3' in possible_versions
:
164 possible_versions
.remove('1.3')
165 logger
.debug('Removed 1.3 due to %s', key
)
166 if key
not in _566_FIELDS
and '2.1' in possible_versions
:
167 if key
!= 'Description': # In 2.1, description allowed after headers
168 possible_versions
.remove('2.1')
169 logger
.debug('Removed 2.1 due to %s', key
)
170 if key
not in _643_FIELDS
and '2.2' in possible_versions
:
171 possible_versions
.remove('2.2')
172 logger
.debug('Removed 2.2 due to %s', key
)
173 # if key not in _426_FIELDS and '2.0' in possible_versions:
174 # possible_versions.remove('2.0')
175 # logger.debug('Removed 2.0 due to %s', key)
177 # possible_version contains qualified versions
178 if len(possible_versions
) == 1:
179 return possible_versions
[0] # found !
180 elif len(possible_versions
) == 0:
181 logger
.debug('Out of options - unknown metadata set: %s', fields
)
182 raise MetadataConflictError('Unknown metadata set')
184 # let's see if one unique marker is found
185 is_1_1
= '1.1' in possible_versions
and _has_marker(keys
, _314_MARKERS
)
186 is_1_2
= '1.2' in possible_versions
and _has_marker(keys
, _345_MARKERS
)
187 is_2_1
= '2.1' in possible_versions
and _has_marker(keys
, _566_MARKERS
)
188 # is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
189 is_2_2
= '2.2' in possible_versions
and _has_marker(keys
, _643_MARKERS
)
190 if int(is_1_1
) + int(is_1_2
) + int(is_2_1
) + int(is_2_2
) > 1:
191 raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2 fields')
193 # we have the choice, 1.0, or 1.2, 2.1 or 2.2
194 # - 1.0 has a broken Summary field but works with all tools
196 # - 1.2 fixes Summary but has little adoption
197 # - 2.1 adds more features
198 # - 2.2 is the latest
199 if not is_1_1
and not is_1_2
and not is_2_1
and not is_2_2
:
200 # we couldn't find any specific marker
201 if PKG_INFO_PREFERRED_VERSION
in possible_versions
:
202 return PKG_INFO_PREFERRED_VERSION
214 # This follows the rules about transforming keys as described in
215 # https://www.python.org/dev/peps/pep-0566/#id17
217 name
.lower().replace("-", "_"): name
for name
in _ALL_FIELDS
219 _FIELD2ATTR
= {field: attr for attr, field in _ATTR2FIELD.items()}
221 _PREDICATE_FIELDS
= ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist')
222 _VERSIONS_FIELDS
= ('Requires-Python',)
223 _VERSION_FIELDS
= ('Version',)
224 _LISTFIELDS
= ('Platform', 'Classifier', 'Obsoletes',
225 'Requires', 'Provides', 'Obsoletes-Dist',
226 'Provides-Dist', 'Requires-Dist', 'Requires-External',
227 'Project-URL', 'Supported-Platform', 'Setup-Requires-Dist',
228 'Provides-Extra', 'Extension', 'License-File')
229 _LISTTUPLEFIELDS
= ('Project-URL',)
231 _ELEMENTSFIELD
= ('Keywords',)
233 _UNICODEFIELDS
= ('Author', 'Maintainer', 'Summary', 'Description')
237 _FILESAFE
= re
.compile('[^A-Za-z0-9.]+')
240 def _get_name_and_version(name
, version
, for_filename
=False):
241 """Return the distribution name with version.
243 If for_filename is true, return a filename-escaped form."""
245 # For both name and version any runs of non-alphanumeric or '.'
246 # characters are replaced with a single '-'. Additionally any
247 # spaces in the version string become '.'
248 name
= _FILESAFE
.sub('-', name
)
249 version
= _FILESAFE
.sub('-', version
.replace(' ', '.'))
250 return '%s-%s' % (name
, version
)
253 class LegacyMetadata(object):
254 """The legacy metadata of a release.
256 Supports versions 1.0, 1.1, 1.2, 2.0 and 1.3/2.1 (auto-detected). You can
257 instantiate the class with one of these arguments (or none):
258 - *path*, the path to a metadata file
259 - *fileobj* give a file-like object with metadata as content
260 - *mapping* is a dict-like object
261 - *scheme* is a version scheme name
263 # TODO document the mapping API and UNKNOWN default key
265 def __init__(self
, path
=None, fileobj
=None, mapping
=None,
267 if [path
, fileobj
, mapping
].count(None) < 2:
268 raise TypeError('path, fileobj and mapping are exclusive')
270 self
.requires_files
= []
271 self
._dependencies
= None
275 elif fileobj
is not None:
276 self
.read_file(fileobj
)
277 elif mapping
is not None:
279 self
.set_metadata_version()
281 def set_metadata_version(self
):
282 self
._fields
['Metadata-Version'] = _best_version(self
._fields
)
284 def _write_field(self
, fileobj
, name
, value
):
285 fileobj
.write('%s: %s\n' % (name
, value
))
287 def __getitem__(self
, name
):
288 return self
.get(name
)
290 def __setitem__(self
, name
, value
):
291 return self
.set(name
, value
)
293 def __delitem__(self
, name
):
294 field_name
= self
._convert
_name
(name
)
296 del self
._fields
[field_name
]
300 def __contains__(self
, name
):
301 return (name
in self
._fields
or
302 self
._convert
_name
(name
) in self
._fields
)
304 def _convert_name(self
, name
):
305 if name
in _ALL_FIELDS
:
307 name
= name
.replace('-', '_').lower()
308 return _ATTR2FIELD
.get(name
, name
)
310 def _default_value(self
, name
):
311 if name
in _LISTFIELDS
or name
in _ELEMENTSFIELD
:
315 def _remove_line_prefix(self
, value
):
316 if self
.metadata_version
in ('1.0', '1.1'):
317 return _LINE_PREFIX_PRE_1_2
.sub('\n', value
)
319 return _LINE_PREFIX_1_2
.sub('\n', value
)
321 def __getattr__(self
, name
):
322 if name
in _ATTR2FIELD
:
324 raise AttributeError(name
)
330 # dependencies = property(_get_dependencies, _set_dependencies)
332 def get_fullname(self
, filesafe
=False):
333 """Return the distribution name with version.
335 If filesafe is true, return a filename-escaped form."""
336 return _get_name_and_version(self
['Name'], self
['Version'], filesafe
)
338 def is_field(self
, name
):
339 """return True if name is a valid metadata key"""
340 name
= self
._convert
_name
(name
)
341 return name
in _ALL_FIELDS
343 def is_multi_field(self
, name
):
344 name
= self
._convert
_name
(name
)
345 return name
in _LISTFIELDS
347 def read(self
, filepath
):
348 """Read the metadata values from a file path."""
349 fp
= codecs
.open(filepath
, 'r', encoding
='utf-8')
355 def read_file(self
, fileob
):
356 """Read the metadata values from a file object."""
357 msg
= message_from_file(fileob
)
358 self
._fields
['Metadata-Version'] = msg
['metadata-version']
360 # When reading, get all the fields we can
361 for field
in _ALL_FIELDS
:
364 if field
in _LISTFIELDS
:
365 # we can have multiple lines
366 values
= msg
.get_all(field
)
367 if field
in _LISTTUPLEFIELDS
and values
is not None:
368 values
= [tuple(value
.split(',')) for value
in values
]
369 self
.set(field
, values
)
373 if value
is not None and value
!= 'UNKNOWN':
374 self
.set(field
, value
)
376 # PEP 566 specifies that the body be used for the description, if
378 body
= msg
.get_payload()
379 self
["Description"] = body
if body
else self
["Description"]
380 # logger.debug('Attempting to set metadata for %s', self)
381 # self.set_metadata_version()
383 def write(self
, filepath
, skip_unknown
=False):
384 """Write the metadata fields to filepath."""
385 fp
= codecs
.open(filepath
, 'w', encoding
='utf-8')
387 self
.write_file(fp
, skip_unknown
)
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()
395 for field
in _version2fieldlist(self
['Metadata-Version']):
396 values
= self
.get(field
)
397 if skip_unknown
and values
in ('UNKNOWN', [], ['UNKNOWN']):
399 if field
in _ELEMENTSFIELD
:
400 self
._write
_field
(fileobject
, field
, ','.join(values
))
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 ')
407 values
= values
.replace('\n', '\n |')
410 if field
in _LISTTUPLEFIELDS
:
411 values
= [','.join(value
) for value
in values
]
414 self
._write
_field
(fileobject
, field
, value
)
416 def update(self
, other
=None, **kwargs
):
417 """Set metadata values from the given iterable `other` and kwargs.
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.
423 Keys that don't match a metadata field or that have an empty value are
426 def _set(key
, value
):
427 if key
in _ATTR2FIELD
and value
:
428 self
.set(self
._convert
_name
(key
), value
)
431 # other is None or empty container
433 elif hasattr(other
, 'keys'):
434 for k
in other
.keys():
441 for k
, v
in kwargs
.items():
444 def set(self
, name
, value
):
445 """Control then set a metadata field."""
446 name
= self
._convert
_name
(name
)
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(',')]
454 elif (name
in _LISTFIELDS
and
455 not isinstance(value
, (list, tuple))):
456 if isinstance(value
, string_types
):
461 if logger
.isEnabledFor(logging
.WARNING
):
462 project_name
= self
['Name']
464 scheme
= get_scheme(self
.scheme
)
465 if name
in _PREDICATE_FIELDS
and value
is not None:
467 # check that the values are valid
468 if not scheme
.is_valid_matcher(v
.split(';')[0]):
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
)
482 if name
in _UNICODEFIELDS
:
483 if name
== 'Description':
484 value
= self
._remove
_line
_prefix
(value
)
486 self
._fields
[name
] = value
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
)
495 if name
in _UNICODEFIELDS
:
496 value
= self
._fields
[name
]
498 elif name
in _LISTFIELDS
:
499 value
= self
._fields
[name
]
504 if name
not in _LISTTUPLEFIELDS
:
507 # That's for Project-URL
508 res
.append((val
[0], val
[1]))
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
]
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()
522 # XXX should check the versions (if the file was loaded)
523 missing
, warnings
= [], []
525 for attr
in ('Name', 'Version'): # required by PEP 345
529 if strict
and missing
!= []:
530 msg
= 'missing required metadata: %s' % ', '.join(missing
)
531 raise MetadataMissingError(msg
)
533 for attr
in ('Home-page', 'Author'):
537 # checking metadata 1.2 (XXX needs to check 1.1, 1.0)
538 if self
['Metadata-Version'] != '1.2':
539 return missing
, warnings
541 scheme
= get_scheme(self
.scheme
)
543 def are_valid_constraints(value
):
545 if not scheme
.is_valid_matcher(v
.split(';')[0]):
549 for fields
, controller
in ((_PREDICATE_FIELDS
, are_valid_constraints
),
551 scheme
.is_valid_constraint_list
),
553 scheme
.is_valid_version
)):
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
))
559 return missing
, warnings
561 def todict(self
, skip_missing
=False):
562 """Return fields as a dict.
564 Field names will be converted to use the underscore-lowercase style
565 instead of hyphen-mixed case (i.e. home_page instead of Home-page).
566 This is as per https://www.python.org/dev/peps/pep-0566/#id17.
568 self
.set_metadata_version()
570 fields
= _version2fieldlist(self
['Metadata-Version'])
574 for field_name
in fields
:
575 if not skip_missing
or field_name
in self
._fields
:
576 key
= _FIELD2ATTR
[field_name
]
577 if key
!= 'project_url':
578 data
[key
] = self
[field_name
]
580 data
[key
] = [','.join(u
) for u
in self
[field_name
]]
584 def add_requirements(self
, requirements
):
585 if self
['Metadata-Version'] == '1.1':
586 # we can't have 1.1 metadata *and* Setuptools requires
587 for field
in ('Obsoletes', 'Requires', 'Provides'):
590 self
['Requires-Dist'] += requirements
593 # TODO could add iter* variants
596 return list(_version2fieldlist(self
['Metadata-Version']))
599 for key
in self
.keys():
603 return [self
[key
] for key
in self
.keys()]
606 return [(key
, self
[key
]) for key
in self
.keys()]
609 return '<%s %s %s>' % (self
.__class
__.__name
__, self
.name
,
613 METADATA_FILENAME
= 'pydist.json'
614 WHEEL_METADATA_FILENAME
= 'metadata.json'
615 LEGACY_METADATA_FILENAME
= 'METADATA'
618 class Metadata(object):
620 The metadata of a release. This implementation uses 2.1
621 metadata where possible. If not possible, it wraps a LegacyMetadata
622 instance which handles the key-value metadata format.
625 METADATA_VERSION_MATCHER
= re
.compile(r
'^\d+(\.\d+)*$')
627 NAME_MATCHER
= re
.compile('^[0-9A-Z]([0-9A-Z_.-]*[0-9A-Z])?$', re
.I
)
629 FIELDNAME_MATCHER
= re
.compile('^[A-Z]([0-9A-Z-]*[0-9A-Z])?$', re
.I
)
631 VERSION_MATCHER
= PEP440_VERSION_RE
633 SUMMARY_MATCHER
= re
.compile('.{1,2047}')
635 METADATA_VERSION
= '2.0'
637 GENERATOR
= 'distlib (%s)' % __version__
642 'summary': ('legacy',),
645 INDEX_KEYS
= ('name version license summary description author '
646 'author_email keywords platform home_page classifiers '
649 DEPENDENCY_KEYS
= ('extras run_requires test_requires build_requires '
650 'dev_requires provides meta_requires obsoleted_by '
651 'supports_environments')
653 SYNTAX_VALIDATORS
= {
654 'metadata_version': (METADATA_VERSION_MATCHER
, ()),
655 'name': (NAME_MATCHER
, ('legacy',)),
656 'version': (VERSION_MATCHER
, ('legacy',)),
657 'summary': (SUMMARY_MATCHER
, ('legacy',)),
658 'dynamic': (FIELDNAME_MATCHER
, ('legacy',)),
661 __slots__
= ('_legacy', '_data', 'scheme')
663 def __init__(self
, path
=None, fileobj
=None, mapping
=None,
665 if [path
, fileobj
, mapping
].count(None) < 2:
666 raise TypeError('path, fileobj and mapping are exclusive')
670 #import pdb; pdb.set_trace()
671 if mapping
is not None:
673 self
._validate
_mapping
(mapping
, scheme
)
675 except MetadataUnrecognizedVersionError
:
676 self
._legacy
= LegacyMetadata(mapping
=mapping
, scheme
=scheme
)
681 with open(path
, 'rb') as f
:
684 data
= fileobj
.read()
686 # Initialised with no args - to be added
688 'metadata_version': self
.METADATA_VERSION
,
689 'generator': self
.GENERATOR
,
692 if not isinstance(data
, text_type
):
693 data
= data
.decode('utf-8')
695 self
._data
= json
.loads(data
)
696 self
._validate
_mapping
(self
._data
, scheme
)
698 # Note: MetadataUnrecognizedVersionError does not
699 # inherit from ValueError (it's a DistlibException,
700 # which should not inherit from ValueError).
701 # The ValueError comes from the json.load - if that
702 # succeeds and we get a validation error, we want
704 self
._legacy
= LegacyMetadata(fileobj
=StringIO(data
),
708 common_keys
= set(('name', 'version', 'license', 'keywords', 'summary'))
710 none_list
= (None, list)
711 none_dict
= (None, dict)
714 'run_requires': ('Requires-Dist', list),
715 'build_requires': ('Setup-Requires-Dist', list),
716 'dev_requires': none_list
,
717 'test_requires': none_list
,
718 'meta_requires': none_list
,
719 'extras': ('Provides-Extra', list),
720 'modules': none_list
,
721 'namespaces': none_list
,
722 'exports': none_dict
,
723 'commands': none_dict
,
724 'classifiers': ('Classifier', list),
725 'source_url': ('Download-URL', None),
726 'metadata_version': ('Metadata-Version', None),
729 del none_list
, none_dict
731 def __getattribute__(self
, key
):
732 common
= object.__getattribute
__(self
, 'common_keys')
733 mapped
= object.__getattribute
__(self
, 'mapped_keys')
735 lk
, maker
= mapped
[key
]
738 result
= None if maker
is None else maker()
740 result
= self
._legacy
.get(lk
)
742 value
= None if maker
is None else maker()
743 if key
not in ('commands', 'exports', 'modules', 'namespaces',
745 result
= self
._data
.get(key
, value
)
747 # special cases for PEP 459
750 d
= self
._data
.get('extensions')
752 if key
== 'commands':
753 result
= d
.get('python.commands', value
)
754 elif key
== 'classifiers':
755 d
= d
.get('python.details')
757 result
= d
.get(key
, value
)
759 d
= d
.get('python.exports')
761 d
= self
._data
.get('python.exports')
763 result
= d
.get(key
, value
)
764 if result
is sentinel
:
766 elif key
not in common
:
767 result
= object.__getattribute
__(self
, key
)
769 result
= self
._legacy
.get(key
)
771 result
= self
._data
.get(key
)
774 def _validate_value(self
, key
, value
, scheme
=None):
775 if key
in self
.SYNTAX_VALIDATORS
:
776 pattern
, exclusions
= self
.SYNTAX_VALIDATORS
[key
]
777 if (scheme
or self
.scheme
) not in exclusions
:
778 m
= pattern
.match(value
)
780 raise MetadataInvalidError("'%s' is an invalid value for "
781 "the '%s' property" % (value
,
784 def __setattr__(self
, key
, value
):
785 self
._validate
_value
(key
, value
)
786 common
= object.__getattribute
__(self
, 'common_keys')
787 mapped
= object.__getattribute
__(self
, 'mapped_keys')
792 raise NotImplementedError
793 self
._legacy
[lk
] = value
794 elif key
not in ('commands', 'exports', 'modules', 'namespaces',
796 self
._data
[key
] = value
798 # special cases for PEP 459
799 d
= self
._data
.setdefault('extensions', {})
800 if key
== 'commands':
801 d
['python.commands'] = value
802 elif key
== 'classifiers':
803 d
= d
.setdefault('python.details', {})
806 d
= d
.setdefault('python.exports', {})
808 elif key
not in common
:
809 object.__setattr
__(self
, key
, value
)
811 if key
== 'keywords':
812 if isinstance(value
, string_types
):
813 value
= value
.strip()
815 value
= value
.split()
819 self
._legacy
[key
] = value
821 self
._data
[key
] = value
824 def name_and_version(self
):
825 return _get_name_and_version(self
.name
, self
.version
, True)
830 result
= self
._legacy
['Provides-Dist']
832 result
= self
._data
.setdefault('provides', [])
833 s
= '%s (%s)' % (self
.name
, self
.version
)
839 def provides(self
, value
):
841 self
._legacy
['Provides-Dist'] = value
843 self
._data
['provides'] = value
845 def get_requirements(self
, reqts
, extras
=None, env
=None):
847 Base method to get dependencies, given a set of extras
848 to satisfy and an optional environment context.
849 :param reqts: A list of sometimes-wanted dependencies,
850 perhaps dependent on extras and environment.
851 :param extras: A list of optional components being requested.
852 :param env: An optional environment for marker evaluation.
858 extras
= get_extras(extras
or [], self
.extras
)
860 if 'extra' not in d
and 'environment' not in d
:
865 # Not extra-dependent - only environment-dependent
868 include
= d
.get('extra') in extras
870 # Not excluded because of extras, check environment
871 marker
= d
.get('environment')
873 include
= interpret(marker
, env
)
875 result
.extend(d
['requires'])
876 for key
in ('build', 'dev', 'test'):
880 # A recursive call, but it should terminate since 'test'
881 # has been removed from the extras
882 reqts
= self
._data
.get('%s_requires' % key
, [])
883 result
.extend(self
.get_requirements(reqts
, extras
=extras
,
888 def dictionary(self
):
890 return self
._from
_legacy
()
894 def dependencies(self
):
896 raise NotImplementedError
898 return extract_by_key(self
._data
, self
.DEPENDENCY_KEYS
)
901 def dependencies(self
, value
):
903 raise NotImplementedError
905 self
._data
.update(value
)
907 def _validate_mapping(self
, mapping
, scheme
):
908 if mapping
.get('metadata_version') != self
.METADATA_VERSION
:
909 raise MetadataUnrecognizedVersionError()
911 for key
, exclusions
in self
.MANDATORY_KEYS
.items():
912 if key
not in mapping
:
913 if scheme
not in exclusions
:
916 msg
= 'Missing metadata items: %s' % ', '.join(missing
)
917 raise MetadataMissingError(msg
)
918 for k
, v
in mapping
.items():
919 self
._validate
_value
(k
, v
, scheme
)
923 missing
, warnings
= self
._legacy
.check(True)
924 if missing
or warnings
:
925 logger
.warning('Metadata: missing: %s, warnings: %s',
928 self
._validate
_mapping
(self
._data
, self
.scheme
)
932 return self
._legacy
.todict(True)
934 result
= extract_by_key(self
._data
, self
.INDEX_KEYS
)
937 def _from_legacy(self
):
938 assert self
._legacy
and not self
._data
940 'metadata_version': self
.METADATA_VERSION
,
941 'generator': self
.GENERATOR
,
943 lmd
= self
._legacy
.todict(True) # skip missing ones
944 for k
in ('name', 'version', 'license', 'summary', 'description',
947 if k
== 'classifier':
952 kw
= lmd
.get('Keywords', [])
955 result
['keywords'] = kw
956 keys
= (('requires_dist', 'run_requires'),
957 ('setup_requires_dist', 'build_requires'))
959 if ok
in lmd
and lmd
[ok
]:
960 result
[nk
] = [{'requires': lmd[ok]}
]
961 result
['provides'] = self
.provides
968 'version': 'Version',
969 ('extensions', 'python.details', 'license'): 'License',
970 'summary': 'Summary',
971 'description': 'Description',
972 ('extensions', 'python.project', 'project_urls', 'Home'): 'Home-page',
973 ('extensions', 'python.project', 'contacts', 0, 'name'): 'Author',
974 ('extensions', 'python.project', 'contacts', 0, 'email'): 'Author-email',
975 'source_url': 'Download-URL',
976 ('extensions', 'python.details', 'classifiers'): 'Classifier',
979 def _to_legacy(self
):
980 def process_entries(entries
):
983 extra
= e
.get('extra')
984 env
= e
.get('environment')
985 rlist
= e
['requires']
987 if not env
and not extra
:
992 marker
= 'extra == "%s"' % extra
995 marker
= '(%s) and %s' % (env
, marker
)
998 reqts
.add(';'.join((r
, marker
)))
1001 assert self
._data
and not self
._legacy
1002 result
= LegacyMetadata()
1004 # import pdb; pdb.set_trace()
1005 for nk
, ok
in self
.LEGACY_MAPPING
.items():
1006 if not isinstance(nk
, tuple):
1008 result
[ok
] = nmd
[nk
]
1015 except (KeyError, IndexError):
1020 r1
= process_entries(self
.run_requires
+ self
.meta_requires
)
1021 r2
= process_entries(self
.build_requires
+ self
.dev_requires
)
1023 result
['Provides-Extra'] = sorted(self
.extras
)
1024 result
['Requires-Dist'] = sorted(r1
)
1025 result
['Setup-Requires-Dist'] = sorted(r2
)
1026 # TODO: any other fields wanted
1029 def write(self
, path
=None, fileobj
=None, legacy
=False, skip_unknown
=True):
1030 if [path
, fileobj
].count(None) != 1:
1031 raise ValueError('Exactly one of path and fileobj is needed')
1035 legacy_md
= self
._legacy
1037 legacy_md
= self
._to
_legacy
()
1039 legacy_md
.write(path
, skip_unknown
=skip_unknown
)
1041 legacy_md
.write_file(fileobj
, skip_unknown
=skip_unknown
)
1044 d
= self
._from
_legacy
()
1048 json
.dump(d
, fileobj
, ensure_ascii
=True, indent
=2,
1051 with codecs
.open(path
, 'w', 'utf-8') as f
:
1052 json
.dump(d
, f
, ensure_ascii
=True, indent
=2,
1055 def add_requirements(self
, requirements
):
1057 self
._legacy
.add_requirements(requirements
)
1059 run_requires
= self
._data
.setdefault('run_requires', [])
1061 for entry
in run_requires
:
1062 if 'environment' not in entry
and 'extra' not in entry
:
1066 always
= { 'requires': requirements }
1067 run_requires
.insert(0, always
)
1069 rset
= set(always
['requires']) |
set(requirements
)
1070 always
['requires'] = sorted(rset
)
1073 name
= self
.name
or '(no name)'
1074 version
= self
.version
or 'no version'
1075 return '<%s %s %s (%s)>' % (self
.__class
__.__name
__,
1076 self
.metadata_version
, name
, version
)