~ibmcharmers/charms/xenial/ibm-cinder-storwize-svc/trunk

« back to all changes in this revision

Viewing changes to .tox/py35/lib/python3.5/site-packages/pip/_vendor/distlib/metadata.py

  • Committer: Ankammarao
  • Date: 2017-03-06 05:11:42 UTC
  • Revision ID: achittet@in.ibm.com-20170306051142-dpg27z4es1k56hfn
Marked tests folder executable

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
_ALL_FIELDS = set()
 
95
_ALL_FIELDS.update(_241_FIELDS)
 
96
_ALL_FIELDS.update(_314_FIELDS)
 
97
_ALL_FIELDS.update(_345_FIELDS)
 
98
_ALL_FIELDS.update(_426_FIELDS)
 
99
 
 
100
EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
 
101
 
 
102
 
 
103
def _version2fieldlist(version):
 
104
    if version == '1.0':
 
105
        return _241_FIELDS
 
106
    elif version == '1.1':
 
107
        return _314_FIELDS
 
108
    elif version == '1.2':
 
109
        return _345_FIELDS
 
110
    elif version == '2.0':
 
111
        return _426_FIELDS
 
112
    raise MetadataUnrecognizedVersionError(version)
 
113
 
 
114
 
 
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:
 
119
            if marker in keys:
 
120
                return True
 
121
        return False
 
122
 
 
123
    keys = []
 
124
    for key, value in fields.items():
 
125
        if value in ([], 'UNKNOWN', None):
 
126
            continue
 
127
        keys.append(key)
 
128
 
 
129
    possible_versions = ['1.0', '1.1', '1.2', '2.0']
 
130
 
 
131
    # first let's try to see if a field is not part of one of the version
 
132
    for key in keys:
 
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')
 
141
 
 
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')
 
147
 
 
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')
 
154
 
 
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
 
157
    #   - 1.1 is to avoid
 
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
 
164
    if is_1_1:
 
165
        return '1.1'
 
166
    if is_1_2:
 
167
        return '1.2'
 
168
 
 
169
    return '2.0'
 
170
 
 
171
_ATTR2FIELD = {
 
172
    'metadata_version': 'Metadata-Version',
 
173
    'name': 'Name',
 
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',
 
181
    'author': 'Author',
 
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',
 
202
}
 
203
 
 
204
_PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist')
 
205
_VERSIONS_FIELDS = ('Requires-Python',)
 
206
_VERSION_FIELDS = ('Version',)
 
207
_LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes',
 
208
               'Requires', 'Provides', 'Obsoletes-Dist',
 
209
               'Provides-Dist', 'Requires-Dist', 'Requires-External',
 
210
               'Project-URL', 'Supported-Platform', 'Setup-Requires-Dist',
 
211
               'Provides-Extra', 'Extension')
 
212
_LISTTUPLEFIELDS = ('Project-URL',)
 
213
 
 
214
_ELEMENTSFIELD = ('Keywords',)
 
215
 
 
216
_UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description')
 
217
 
 
218
_MISSING = object()
 
219
 
 
220
_FILESAFE = re.compile('[^A-Za-z0-9.]+')
 
221
 
 
222
 
 
223
def _get_name_and_version(name, version, for_filename=False):
 
224
    """Return the distribution name with version.
 
225
 
 
226
    If for_filename is true, return a filename-escaped form."""
 
227
    if for_filename:
 
228
        # For both name and version any runs of non-alphanumeric or '.'
 
229
        # characters are replaced with a single '-'.  Additionally any
 
230
        # spaces in the version string become '.'
 
231
        name = _FILESAFE.sub('-', name)
 
232
        version = _FILESAFE.sub('-', version.replace(' ', '.'))
 
233
    return '%s-%s' % (name, version)
 
234
 
 
235
 
 
236
class LegacyMetadata(object):
 
237
    """The legacy metadata of a release.
 
238
 
 
239
    Supports versions 1.0, 1.1 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
 
245
    """
 
246
    # TODO document the mapping API and UNKNOWN default key
 
247
 
 
248
    def __init__(self, path=None, fileobj=None, mapping=None,
 
249
                 scheme='default'):
 
250
        if [path, fileobj, mapping].count(None) < 2:
 
251
            raise TypeError('path, fileobj and mapping are exclusive')
 
252
        self._fields = {}
 
253
        self.requires_files = []
 
254
        self._dependencies = None
 
255
        self.scheme = scheme
 
256
        if path is not None:
 
257
            self.read(path)
 
258
        elif fileobj is not None:
 
259
            self.read_file(fileobj)
 
260
        elif mapping is not None:
 
261
            self.update(mapping)
 
262
            self.set_metadata_version()
 
263
 
 
264
    def set_metadata_version(self):
 
265
        self._fields['Metadata-Version'] = _best_version(self._fields)
 
266
 
 
267
    def _write_field(self, fileobj, name, value):
 
268
        fileobj.write('%s: %s\n' % (name, value))
 
269
 
 
270
    def __getitem__(self, name):
 
271
        return self.get(name)
 
272
 
 
273
    def __setitem__(self, name, value):
 
274
        return self.set(name, value)
 
275
 
 
276
    def __delitem__(self, name):
 
277
        field_name = self._convert_name(name)
 
278
        try:
 
279
            del self._fields[field_name]
 
280
        except KeyError:
 
281
            raise KeyError(name)
 
282
 
 
283
    def __contains__(self, name):
 
284
        return (name in self._fields or
 
285
                self._convert_name(name) in self._fields)
 
286
 
 
287
    def _convert_name(self, name):
 
288
        if name in _ALL_FIELDS:
 
289
            return name
 
290
        name = name.replace('-', '_').lower()
 
291
        return _ATTR2FIELD.get(name, name)
 
292
 
 
293
    def _default_value(self, name):
 
294
        if name in _LISTFIELDS or name in _ELEMENTSFIELD:
 
295
            return []
 
296
        return 'UNKNOWN'
 
297
 
 
298
    def _remove_line_prefix(self, value):
 
299
        if self.metadata_version in ('1.0', '1.1'):
 
300
            return _LINE_PREFIX_PRE_1_2.sub('\n', value)
 
301
        else:
 
302
            return _LINE_PREFIX_1_2.sub('\n', value)
 
303
 
 
304
    def __getattr__(self, name):
 
305
        if name in _ATTR2FIELD:
 
306
            return self[name]
 
307
        raise AttributeError(name)
 
308
 
 
309
    #
 
310
    # Public API
 
311
    #
 
312
 
 
313
#    dependencies = property(_get_dependencies, _set_dependencies)
 
314
 
 
315
    def get_fullname(self, filesafe=False):
 
316
        """Return the distribution name with version.
 
317
 
 
318
        If filesafe is true, return a filename-escaped form."""
 
319
        return _get_name_and_version(self['Name'], self['Version'], filesafe)
 
320
 
 
321
    def is_field(self, name):
 
322
        """return True if name is a valid metadata key"""
 
323
        name = self._convert_name(name)
 
324
        return name in _ALL_FIELDS
 
325
 
 
326
    def is_multi_field(self, name):
 
327
        name = self._convert_name(name)
 
328
        return name in _LISTFIELDS
 
329
 
 
330
    def read(self, filepath):
 
331
        """Read the metadata values from a file path."""
 
332
        fp = codecs.open(filepath, 'r', encoding='utf-8')
 
333
        try:
 
334
            self.read_file(fp)
 
335
        finally:
 
336
            fp.close()
 
337
 
 
338
    def read_file(self, fileob):
 
339
        """Read the metadata values from a file object."""
 
340
        msg = message_from_file(fileob)
 
341
        self._fields['Metadata-Version'] = msg['metadata-version']
 
342
 
 
343
        # When reading, get all the fields we can
 
344
        for field in _ALL_FIELDS:
 
345
            if field not in msg:
 
346
                continue
 
347
            if field in _LISTFIELDS:
 
348
                # we can have multiple lines
 
349
                values = msg.get_all(field)
 
350
                if field in _LISTTUPLEFIELDS and values is not None:
 
351
                    values = [tuple(value.split(',')) for value in values]
 
352
                self.set(field, values)
 
353
            else:
 
354
                # single line
 
355
                value = msg[field]
 
356
                if value is not None and value != 'UNKNOWN':
 
357
                    self.set(field, value)
 
358
        self.set_metadata_version()
 
359
 
 
360
    def write(self, filepath, skip_unknown=False):
 
361
        """Write the metadata fields to filepath."""
 
362
        fp = codecs.open(filepath, 'w', encoding='utf-8')
 
363
        try:
 
364
            self.write_file(fp, skip_unknown)
 
365
        finally:
 
366
            fp.close()
 
367
 
 
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()
 
371
 
 
372
        for field in _version2fieldlist(self['Metadata-Version']):
 
373
            values = self.get(field)
 
374
            if skip_unknown and values in ('UNKNOWN', [], ['UNKNOWN']):
 
375
                continue
 
376
            if field in _ELEMENTSFIELD:
 
377
                self._write_field(fileobject, field, ','.join(values))
 
378
                continue
 
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        ')
 
383
                    else:
 
384
                        values = values.replace('\n', '\n       |')
 
385
                values = [values]
 
386
 
 
387
            if field in _LISTTUPLEFIELDS:
 
388
                values = [','.join(value) for value in values]
 
389
 
 
390
            for value in values:
 
391
                self._write_field(fileobject, field, value)
 
392
 
 
393
    def update(self, other=None, **kwargs):
 
394
        """Set metadata values from the given iterable `other` and kwargs.
 
395
 
 
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.
 
399
 
 
400
        Keys that don't match a metadata field or that have an empty value are
 
401
        dropped.
 
402
        """
 
403
        def _set(key, value):
 
404
            if key in _ATTR2FIELD and value:
 
405
                self.set(self._convert_name(key), value)
 
406
 
 
407
        if not other:
 
408
            # other is None or empty container
 
409
            pass
 
410
        elif hasattr(other, 'keys'):
 
411
            for k in other.keys():
 
412
                _set(k, other[k])
 
413
        else:
 
414
            for k, v in other:
 
415
                _set(k, v)
 
416
 
 
417
        if kwargs:
 
418
            for k, v in kwargs.items():
 
419
                _set(k, v)
 
420
 
 
421
    def set(self, name, value):
 
422
        """Control then set a metadata field."""
 
423
        name = self._convert_name(name)
 
424
 
 
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(',')]
 
429
            else:
 
430
                value = []
 
431
        elif (name in _LISTFIELDS and
 
432
              not isinstance(value, (list, tuple))):
 
433
            if isinstance(value, string_types):
 
434
                value = [value]
 
435
            else:
 
436
                value = []
 
437
 
 
438
        if logger.isEnabledFor(logging.WARNING):
 
439
            project_name = self['Name']
 
440
 
 
441
            scheme = get_scheme(self.scheme)
 
442
            if name in _PREDICATE_FIELDS and value is not None:
 
443
                for v in value:
 
444
                    # check that the values are valid
 
445
                    if not scheme.is_valid_matcher(v.split(';')[0]):
 
446
                        logger.warning(
 
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)
 
458
 
 
459
        if name in _UNICODEFIELDS:
 
460
            if name == 'Description':
 
461
                value = self._remove_line_prefix(value)
 
462
 
 
463
        self._fields[name] = value
 
464
 
 
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)
 
471
            return default
 
472
        if name in _UNICODEFIELDS:
 
473
            value = self._fields[name]
 
474
            return value
 
475
        elif name in _LISTFIELDS:
 
476
            value = self._fields[name]
 
477
            if value is None:
 
478
                return []
 
479
            res = []
 
480
            for val in value:
 
481
                if name not in _LISTTUPLEFIELDS:
 
482
                    res.append(val)
 
483
                else:
 
484
                    # That's for Project-URL
 
485
                    res.append((val[0], val[1]))
 
486
            return res
 
487
 
 
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]
 
493
 
 
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()
 
498
 
 
499
        # XXX should check the versions (if the file was loaded)
 
500
        missing, warnings = [], []
 
501
 
 
502
        for attr in ('Name', 'Version'):  # required by PEP 345
 
503
            if attr not in self:
 
504
                missing.append(attr)
 
505
 
 
506
        if strict and missing != []:
 
507
            msg = 'missing required metadata: %s' % ', '.join(missing)
 
508
            raise MetadataMissingError(msg)
 
509
 
 
510
        for attr in ('Home-page', 'Author'):
 
511
            if attr not in self:
 
512
                missing.append(attr)
 
513
 
 
514
        # checking metadata 1.2 (XXX needs to check 1.1, 1.0)
 
515
        if self['Metadata-Version'] != '1.2':
 
516
            return missing, warnings
 
517
 
 
518
        scheme = get_scheme(self.scheme)
 
519
 
 
520
        def are_valid_constraints(value):
 
521
            for v in value:
 
522
                if not scheme.is_valid_matcher(v.split(';')[0]):
 
523
                    return False
 
524
            return True
 
525
 
 
526
        for fields, controller in ((_PREDICATE_FIELDS, are_valid_constraints),
 
527
                                   (_VERSIONS_FIELDS,
 
528
                                    scheme.is_valid_constraint_list),
 
529
                                   (_VERSION_FIELDS,
 
530
                                    scheme.is_valid_version)):
 
531
            for field in fields:
 
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))
 
535
 
 
536
        return missing, warnings
 
537
 
 
538
    def todict(self, skip_missing=False):
 
539
        """Return fields as a dict.
 
540
 
 
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).
 
543
        """
 
544
        self.set_metadata_version()
 
545
 
 
546
        mapping_1_0 = (
 
547
            ('metadata_version', 'Metadata-Version'),
 
548
            ('name', 'Name'),
 
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'),
 
560
        )
 
561
 
 
562
        data = {}
 
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]
 
566
 
 
567
        if self['Metadata-Version'] == '1.2':
 
568
            mapping_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'),
 
577
            )
 
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]
 
582
                    else:
 
583
                        data[key] = [','.join(u) for u in self[field_name]]
 
584
 
 
585
        elif self['Metadata-Version'] == '1.1':
 
586
            mapping_1_1 = (
 
587
                ('provides', 'Provides'),
 
588
                ('requires', 'Requires'),
 
589
                ('obsoletes', 'Obsoletes'),
 
590
            )
 
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]
 
594
 
 
595
        return data
 
596
 
 
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'):
 
601
                if field in self:
 
602
                    del self[field]
 
603
        self['Requires-Dist'] += requirements
 
604
 
 
605
    # Mapping API
 
606
    # TODO could add iter* variants
 
607
 
 
608
    def keys(self):
 
609
        return list(_version2fieldlist(self['Metadata-Version']))
 
610
 
 
611
    def __iter__(self):
 
612
        for key in self.keys():
 
613
            yield key
 
614
 
 
615
    def values(self):
 
616
        return [self[key] for key in self.keys()]
 
617
 
 
618
    def items(self):
 
619
        return [(key, self[key]) for key in self.keys()]
 
620
 
 
621
    def __repr__(self):
 
622
        return '<%s %s %s>' % (self.__class__.__name__, self.name,
 
623
                               self.version)
 
624
 
 
625
 
 
626
METADATA_FILENAME = 'pydist.json'
 
627
WHEEL_METADATA_FILENAME = 'metadata.json'
 
628
 
 
629
 
 
630
class Metadata(object):
 
631
    """
 
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.
 
635
    """
 
636
 
 
637
    METADATA_VERSION_MATCHER = re.compile('^\d+(\.\d+)*$')
 
638
 
 
639
    NAME_MATCHER = re.compile('^[0-9A-Z]([0-9A-Z_.-]*[0-9A-Z])?$', re.I)
 
640
 
 
641
    VERSION_MATCHER = PEP440_VERSION_RE
 
642
 
 
643
    SUMMARY_MATCHER = re.compile('.{1,2047}')
 
644
 
 
645
    METADATA_VERSION = '2.0'
 
646
 
 
647
    GENERATOR = 'distlib (%s)' % __version__
 
648
 
 
649
    MANDATORY_KEYS = {
 
650
        'name': (),
 
651
        'version': (),
 
652
        'summary': ('legacy',),
 
653
    }
 
654
 
 
655
    INDEX_KEYS = ('name version license summary description author '
 
656
                  'author_email keywords platform home_page classifiers '
 
657
                  'download_url')
 
658
 
 
659
    DEPENDENCY_KEYS = ('extras run_requires test_requires build_requires '
 
660
                       'dev_requires provides meta_requires obsoleted_by '
 
661
                       'supports_environments')
 
662
 
 
663
    SYNTAX_VALIDATORS = {
 
664
        'metadata_version': (METADATA_VERSION_MATCHER, ()),
 
665
        'name': (NAME_MATCHER, ('legacy',)),
 
666
        'version': (VERSION_MATCHER, ('legacy',)),
 
667
        'summary': (SUMMARY_MATCHER, ('legacy',)),
 
668
    }
 
669
 
 
670
    __slots__ = ('_legacy', '_data', 'scheme')
 
671
 
 
672
    def __init__(self, path=None, fileobj=None, mapping=None,
 
673
                 scheme='default'):
 
674
        if [path, fileobj, mapping].count(None) < 2:
 
675
            raise TypeError('path, fileobj and mapping are exclusive')
 
676
        self._legacy = None
 
677
        self._data = None
 
678
        self.scheme = scheme
 
679
        #import pdb; pdb.set_trace()
 
680
        if mapping is not None:
 
681
            try:
 
682
                self._validate_mapping(mapping, scheme)
 
683
                self._data = mapping
 
684
            except MetadataUnrecognizedVersionError:
 
685
                self._legacy = LegacyMetadata(mapping=mapping, scheme=scheme)
 
686
                self.validate()
 
687
        else:
 
688
            data = None
 
689
            if path:
 
690
                with open(path, 'rb') as f:
 
691
                    data = f.read()
 
692
            elif fileobj:
 
693
                data = fileobj.read()
 
694
            if data is None:
 
695
                # Initialised with no args - to be added
 
696
                self._data = {
 
697
                    'metadata_version': self.METADATA_VERSION,
 
698
                    'generator': self.GENERATOR,
 
699
                }
 
700
            else:
 
701
                if not isinstance(data, text_type):
 
702
                    data = data.decode('utf-8')
 
703
                try:
 
704
                    self._data = json.loads(data)
 
705
                    self._validate_mapping(self._data, scheme)
 
706
                except ValueError:
 
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
 
712
                    # that to propagate
 
713
                    self._legacy = LegacyMetadata(fileobj=StringIO(data),
 
714
                                                  scheme=scheme)
 
715
                    self.validate()
 
716
 
 
717
    common_keys = set(('name', 'version', 'license', 'keywords', 'summary'))
 
718
 
 
719
    none_list = (None, list)
 
720
    none_dict = (None, dict)
 
721
 
 
722
    mapped_keys = {
 
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),
 
736
    }
 
737
 
 
738
    del none_list, none_dict
 
739
 
 
740
    def __getattribute__(self, key):
 
741
        common = object.__getattribute__(self, 'common_keys')
 
742
        mapped = object.__getattribute__(self, 'mapped_keys')
 
743
        if key in mapped:
 
744
            lk, maker = mapped[key]
 
745
            if self._legacy:
 
746
                if lk is None:
 
747
                    result = None if maker is None else maker()
 
748
                else:
 
749
                    result = self._legacy.get(lk)
 
750
            else:
 
751
                value = None if maker is None else maker()
 
752
                if key not in ('commands', 'exports', 'modules', 'namespaces',
 
753
                               'classifiers'):
 
754
                    result = self._data.get(key, value)
 
755
                else:
 
756
                    # special cases for PEP 459
 
757
                    sentinel = object()
 
758
                    result = sentinel
 
759
                    d = self._data.get('extensions')
 
760
                    if d:
 
761
                        if key == 'commands':
 
762
                            result = d.get('python.commands', value)
 
763
                        elif key == 'classifiers':
 
764
                            d = d.get('python.details')
 
765
                            if d:
 
766
                                result = d.get(key, value)
 
767
                        else:
 
768
                            d = d.get('python.exports')
 
769
                            if not d:
 
770
                                d = self._data.get('python.exports')
 
771
                            if d:
 
772
                                result = d.get(key, value)
 
773
                    if result is sentinel:
 
774
                        result = value
 
775
        elif key not in common:
 
776
            result = object.__getattribute__(self, key)
 
777
        elif self._legacy:
 
778
            result = self._legacy.get(key)
 
779
        else:
 
780
            result = self._data.get(key)
 
781
        return result
 
782
 
 
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)
 
788
                if not m:
 
789
                    raise MetadataInvalidError("'%s' is an invalid value for "
 
790
                                               "the '%s' property" % (value,
 
791
                                                                    key))
 
792
 
 
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')
 
797
        if key in mapped:
 
798
            lk, _ = mapped[key]
 
799
            if self._legacy:
 
800
                if lk is None:
 
801
                    raise NotImplementedError
 
802
                self._legacy[lk] = value
 
803
            elif key not in ('commands', 'exports', 'modules', 'namespaces',
 
804
                             'classifiers'):
 
805
                self._data[key] = value
 
806
            else:
 
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', {})
 
813
                    d[key] = value
 
814
                else:
 
815
                    d = d.setdefault('python.exports', {})
 
816
                    d[key] = value
 
817
        elif key not in common:
 
818
            object.__setattr__(self, key, value)
 
819
        else:
 
820
            if key == 'keywords':
 
821
                if isinstance(value, string_types):
 
822
                    value = value.strip()
 
823
                    if value:
 
824
                        value = value.split()
 
825
                    else:
 
826
                        value = []
 
827
            if self._legacy:
 
828
                self._legacy[key] = value
 
829
            else:
 
830
                self._data[key] = value
 
831
 
 
832
    @property
 
833
    def name_and_version(self):
 
834
        return _get_name_and_version(self.name, self.version, True)
 
835
 
 
836
    @property
 
837
    def provides(self):
 
838
        if self._legacy:
 
839
            result = self._legacy['Provides-Dist']
 
840
        else:
 
841
            result = self._data.setdefault('provides', [])
 
842
        s = '%s (%s)' % (self.name, self.version)
 
843
        if s not in result:
 
844
            result.append(s)
 
845
        return result
 
846
 
 
847
    @provides.setter
 
848
    def provides(self, value):
 
849
        if self._legacy:
 
850
            self._legacy['Provides-Dist'] = value
 
851
        else:
 
852
            self._data['provides'] = value
 
853
 
 
854
    def get_requirements(self, reqts, extras=None, env=None):
 
855
        """
 
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.
 
862
        """
 
863
        if self._legacy:
 
864
            result = reqts
 
865
        else:
 
866
            result = []
 
867
            extras = get_extras(extras or [], self.extras)
 
868
            for d in reqts:
 
869
                if 'extra' not in d and 'environment' not in d:
 
870
                    # unconditional
 
871
                    include = True
 
872
                else:
 
873
                    if 'extra' not in d:
 
874
                        # Not extra-dependent - only environment-dependent
 
875
                        include = True
 
876
                    else:
 
877
                        include = d.get('extra') in extras
 
878
                    if include:
 
879
                        # Not excluded because of extras, check environment
 
880
                        marker = d.get('environment')
 
881
                        if marker:
 
882
                            include = interpret(marker, env)
 
883
                if include:
 
884
                    result.extend(d['requires'])
 
885
            for key in ('build', 'dev', 'test'):
 
886
                e = ':%s:' % key
 
887
                if e in extras:
 
888
                    extras.remove(e)
 
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,
 
893
                                                        env=env))
 
894
        return result
 
895
 
 
896
    @property
 
897
    def dictionary(self):
 
898
        if self._legacy:
 
899
            return self._from_legacy()
 
900
        return self._data
 
901
 
 
902
    @property
 
903
    def dependencies(self):
 
904
        if self._legacy:
 
905
            raise NotImplementedError
 
906
        else:
 
907
            return extract_by_key(self._data, self.DEPENDENCY_KEYS)
 
908
 
 
909
    @dependencies.setter
 
910
    def dependencies(self, value):
 
911
        if self._legacy:
 
912
            raise NotImplementedError
 
913
        else:
 
914
            self._data.update(value)
 
915
 
 
916
    def _validate_mapping(self, mapping, scheme):
 
917
        if mapping.get('metadata_version') != self.METADATA_VERSION:
 
918
            raise MetadataUnrecognizedVersionError()
 
919
        missing = []
 
920
        for key, exclusions in self.MANDATORY_KEYS.items():
 
921
            if key not in mapping:
 
922
                if scheme not in exclusions:
 
923
                    missing.append(key)
 
924
        if missing:
 
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)
 
929
 
 
930
    def validate(self):
 
931
        if self._legacy:
 
932
            missing, warnings = self._legacy.check(True)
 
933
            if missing or warnings:
 
934
                logger.warning('Metadata: missing: %s, warnings: %s',
 
935
                               missing, warnings)
 
936
        else:
 
937
            self._validate_mapping(self._data, self.scheme)
 
938
 
 
939
    def todict(self):
 
940
        if self._legacy:
 
941
            return self._legacy.todict(True)
 
942
        else:
 
943
            result = extract_by_key(self._data, self.INDEX_KEYS)
 
944
            return result
 
945
 
 
946
    def _from_legacy(self):
 
947
        assert self._legacy and not self._data
 
948
        result = {
 
949
            'metadata_version': self.METADATA_VERSION,
 
950
            'generator': self.GENERATOR,
 
951
        }
 
952
        lmd = self._legacy.todict(True)     # skip missing ones
 
953
        for k in ('name', 'version', 'license', 'summary', 'description',
 
954
                  'classifier'):
 
955
            if k in lmd:
 
956
                if k == 'classifier':
 
957
                    nk = 'classifiers'
 
958
                else:
 
959
                    nk = k
 
960
                result[nk] = lmd[k]
 
961
        kw = lmd.get('Keywords', [])
 
962
        if kw == ['']:
 
963
            kw = []
 
964
        result['keywords'] = kw
 
965
        keys = (('requires_dist', 'run_requires'),
 
966
                ('setup_requires_dist', 'build_requires'))
 
967
        for ok, nk in keys:
 
968
            if ok in lmd and lmd[ok]:
 
969
                result[nk] = [{'requires': lmd[ok]}]
 
970
        result['provides'] = self.provides
 
971
        author = {}
 
972
        maintainer = {}
 
973
        return result
 
974
 
 
975
    LEGACY_MAPPING = {
 
976
        'name': 'Name',
 
977
        'version': 'Version',
 
978
        'license': 'License',
 
979
        'summary': 'Summary',
 
980
        'description': 'Description',
 
981
        'classifiers': 'Classifier',
 
982
    }
 
983
 
 
984
    def _to_legacy(self):
 
985
        def process_entries(entries):
 
986
            reqts = set()
 
987
            for e in entries:
 
988
                extra = e.get('extra')
 
989
                env = e.get('environment')
 
990
                rlist = e['requires']
 
991
                for r in rlist:
 
992
                    if not env and not extra:
 
993
                        reqts.add(r)
 
994
                    else:
 
995
                        marker = ''
 
996
                        if extra:
 
997
                            marker = 'extra == "%s"' % extra
 
998
                        if env:
 
999
                            if marker:
 
1000
                                marker = '(%s) and %s' % (env, marker)
 
1001
                            else:
 
1002
                                marker = env
 
1003
                        reqts.add(';'.join((r, marker)))
 
1004
            return reqts
 
1005
 
 
1006
        assert self._data and not self._legacy
 
1007
        result = LegacyMetadata()
 
1008
        nmd = self._data
 
1009
        for nk, ok in self.LEGACY_MAPPING.items():
 
1010
            if nk in nmd:
 
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)
 
1014
        if self.extras:
 
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
 
1019
        return result
 
1020
 
 
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')
 
1024
        self.validate()
 
1025
        if legacy:
 
1026
            if self._legacy:
 
1027
                legacy_md = self._legacy
 
1028
            else:
 
1029
                legacy_md = self._to_legacy()
 
1030
            if path:
 
1031
                legacy_md.write(path, skip_unknown=skip_unknown)
 
1032
            else:
 
1033
                legacy_md.write_file(fileobj, skip_unknown=skip_unknown)
 
1034
        else:
 
1035
            if self._legacy:
 
1036
                d = self._from_legacy()
 
1037
            else:
 
1038
                d = self._data
 
1039
            if fileobj:
 
1040
                json.dump(d, fileobj, ensure_ascii=True, indent=2,
 
1041
                          sort_keys=True)
 
1042
            else:
 
1043
                with codecs.open(path, 'w', 'utf-8') as f:
 
1044
                    json.dump(d, f, ensure_ascii=True, indent=2,
 
1045
                              sort_keys=True)
 
1046
 
 
1047
    def add_requirements(self, requirements):
 
1048
        if self._legacy:
 
1049
            self._legacy.add_requirements(requirements)
 
1050
        else:
 
1051
            run_requires = self._data.setdefault('run_requires', [])
 
1052
            always = None
 
1053
            for entry in run_requires:
 
1054
                if 'environment' not in entry and 'extra' not in entry:
 
1055
                    always = entry
 
1056
                    break
 
1057
            if always is None:
 
1058
                always = { 'requires': requirements }
 
1059
                run_requires.insert(0, always)
 
1060
            else:
 
1061
                rset = set(always['requires']) | set(requirements)
 
1062
                always['requires'] = sorted(rset)
 
1063
 
 
1064
    def __repr__(self):
 
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)