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