m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1068 lines
39 KiB

6 months ago
  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. Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1 and 2.2).
  8. """
  9. from __future__ import unicode_literals
  10. import codecs
  11. from email import message_from_file
  12. import json
  13. import logging
  14. import re
  15. from . import DistlibException, __version__
  16. from .compat import StringIO, string_types, text_type
  17. from .markers import interpret
  18. from .util import extract_by_key, get_extras
  19. from .version import get_scheme, PEP440_VERSION_RE
  20. logger = logging.getLogger(__name__)
  21. class MetadataMissingError(DistlibException):
  22. """A required metadata is missing"""
  23. class MetadataConflictError(DistlibException):
  24. """Attempt to read or write metadata fields that are conflictual."""
  25. class MetadataUnrecognizedVersionError(DistlibException):
  26. """Unknown metadata version number."""
  27. class MetadataInvalidError(DistlibException):
  28. """A metadata value is invalid"""
  29. # public API of this module
  30. __all__ = ['Metadata', 'PKG_INFO_ENCODING', 'PKG_INFO_PREFERRED_VERSION']
  31. # Encoding used for the PKG-INFO files
  32. PKG_INFO_ENCODING = 'utf-8'
  33. # preferred version. Hopefully will be changed
  34. # to 1.2 once PEP 345 is supported everywhere
  35. PKG_INFO_PREFERRED_VERSION = '1.1'
  36. _LINE_PREFIX_1_2 = re.compile('\n \\|')
  37. _LINE_PREFIX_PRE_1_2 = re.compile('\n ')
  38. _241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
  39. 'Summary', 'Description',
  40. 'Keywords', 'Home-page', 'Author', 'Author-email',
  41. 'License')
  42. _314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
  43. 'Supported-Platform', 'Summary', 'Description',
  44. 'Keywords', 'Home-page', 'Author', 'Author-email',
  45. 'License', 'Classifier', 'Download-URL', 'Obsoletes',
  46. 'Provides', 'Requires')
  47. _314_MARKERS = ('Obsoletes', 'Provides', 'Requires', 'Classifier',
  48. 'Download-URL')
  49. _345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
  50. 'Supported-Platform', 'Summary', 'Description',
  51. 'Keywords', 'Home-page', 'Author', 'Author-email',
  52. 'Maintainer', 'Maintainer-email', 'License',
  53. 'Classifier', 'Download-URL', 'Obsoletes-Dist',
  54. 'Project-URL', 'Provides-Dist', 'Requires-Dist',
  55. 'Requires-Python', 'Requires-External')
  56. _345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python',
  57. 'Obsoletes-Dist', 'Requires-External', 'Maintainer',
  58. 'Maintainer-email', 'Project-URL')
  59. _426_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
  60. 'Supported-Platform', 'Summary', 'Description',
  61. 'Keywords', 'Home-page', 'Author', 'Author-email',
  62. 'Maintainer', 'Maintainer-email', 'License',
  63. 'Classifier', 'Download-URL', 'Obsoletes-Dist',
  64. 'Project-URL', 'Provides-Dist', 'Requires-Dist',
  65. 'Requires-Python', 'Requires-External', 'Private-Version',
  66. 'Obsoleted-By', 'Setup-Requires-Dist', 'Extension',
  67. 'Provides-Extra')
  68. _426_MARKERS = ('Private-Version', 'Provides-Extra', 'Obsoleted-By',
  69. 'Setup-Requires-Dist', 'Extension')
  70. # See issue #106: Sometimes 'Requires' and 'Provides' occur wrongly in
  71. # the metadata. Include them in the tuple literal below to allow them
  72. # (for now).
  73. # Ditto for Obsoletes - see issue #140.
  74. _566_FIELDS = _426_FIELDS + ('Description-Content-Type',
  75. 'Requires', 'Provides', 'Obsoletes')
  76. _566_MARKERS = ('Description-Content-Type',)
  77. _643_MARKERS = ('Dynamic', 'License-File')
  78. _643_FIELDS = _566_FIELDS + _643_MARKERS
  79. _ALL_FIELDS = set()
  80. _ALL_FIELDS.update(_241_FIELDS)
  81. _ALL_FIELDS.update(_314_FIELDS)
  82. _ALL_FIELDS.update(_345_FIELDS)
  83. _ALL_FIELDS.update(_426_FIELDS)
  84. _ALL_FIELDS.update(_566_FIELDS)
  85. _ALL_FIELDS.update(_643_FIELDS)
  86. EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
  87. def _version2fieldlist(version):
  88. if version == '1.0':
  89. return _241_FIELDS
  90. elif version == '1.1':
  91. return _314_FIELDS
  92. elif version == '1.2':
  93. return _345_FIELDS
  94. elif version in ('1.3', '2.1'):
  95. # avoid adding field names if already there
  96. return _345_FIELDS + tuple(f for f in _566_FIELDS if f not in _345_FIELDS)
  97. elif version == '2.0':
  98. raise ValueError('Metadata 2.0 is withdrawn and not supported')
  99. # return _426_FIELDS
  100. elif version == '2.2':
  101. return _643_FIELDS
  102. raise MetadataUnrecognizedVersionError(version)
  103. def _best_version(fields):
  104. """Detect the best version depending on the fields used."""
  105. def _has_marker(keys, markers):
  106. return any(marker in keys for marker in markers)
  107. keys = [key for key, value in fields.items() if value not in ([], 'UNKNOWN', None)]
  108. possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2'] # 2.0 removed
  109. # first let's try to see if a field is not part of one of the version
  110. for key in keys:
  111. if key not in _241_FIELDS and '1.0' in possible_versions:
  112. possible_versions.remove('1.0')
  113. logger.debug('Removed 1.0 due to %s', key)
  114. if key not in _314_FIELDS and '1.1' in possible_versions:
  115. possible_versions.remove('1.1')
  116. logger.debug('Removed 1.1 due to %s', key)
  117. if key not in _345_FIELDS and '1.2' in possible_versions:
  118. possible_versions.remove('1.2')
  119. logger.debug('Removed 1.2 due to %s', key)
  120. if key not in _566_FIELDS and '1.3' in possible_versions:
  121. possible_versions.remove('1.3')
  122. logger.debug('Removed 1.3 due to %s', key)
  123. if key not in _566_FIELDS and '2.1' in possible_versions:
  124. if key != 'Description': # In 2.1, description allowed after headers
  125. possible_versions.remove('2.1')
  126. logger.debug('Removed 2.1 due to %s', key)
  127. if key not in _643_FIELDS and '2.2' in possible_versions:
  128. possible_versions.remove('2.2')
  129. logger.debug('Removed 2.2 due to %s', key)
  130. # if key not in _426_FIELDS and '2.0' in possible_versions:
  131. # possible_versions.remove('2.0')
  132. # logger.debug('Removed 2.0 due to %s', key)
  133. # possible_version contains qualified versions
  134. if len(possible_versions) == 1:
  135. return possible_versions[0] # found !
  136. elif len(possible_versions) == 0:
  137. logger.debug('Out of options - unknown metadata set: %s', fields)
  138. raise MetadataConflictError('Unknown metadata set')
  139. # let's see if one unique marker is found
  140. is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS)
  141. is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS)
  142. is_2_1 = '2.1' in possible_versions and _has_marker(keys, _566_MARKERS)
  143. # is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
  144. is_2_2 = '2.2' in possible_versions and _has_marker(keys, _643_MARKERS)
  145. if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_2) > 1:
  146. raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2 fields')
  147. # we have the choice, 1.0, or 1.2, 2.1 or 2.2
  148. # - 1.0 has a broken Summary field but works with all tools
  149. # - 1.1 is to avoid
  150. # - 1.2 fixes Summary but has little adoption
  151. # - 2.1 adds more features
  152. # - 2.2 is the latest
  153. if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_2:
  154. # we couldn't find any specific marker
  155. if PKG_INFO_PREFERRED_VERSION in possible_versions:
  156. return PKG_INFO_PREFERRED_VERSION
  157. if is_1_1:
  158. return '1.1'
  159. if is_1_2:
  160. return '1.2'
  161. if is_2_1:
  162. return '2.1'
  163. # if is_2_2:
  164. # return '2.2'
  165. return '2.2'
  166. # This follows the rules about transforming keys as described in
  167. # https://www.python.org/dev/peps/pep-0566/#id17
  168. _ATTR2FIELD = {
  169. name.lower().replace("-", "_"): name for name in _ALL_FIELDS
  170. }
  171. _FIELD2ATTR = {field: attr for attr, field in _ATTR2FIELD.items()}
  172. _PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist')
  173. _VERSIONS_FIELDS = ('Requires-Python',)
  174. _VERSION_FIELDS = ('Version',)
  175. _LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes',
  176. 'Requires', 'Provides', 'Obsoletes-Dist',
  177. 'Provides-Dist', 'Requires-Dist', 'Requires-External',
  178. 'Project-URL', 'Supported-Platform', 'Setup-Requires-Dist',
  179. 'Provides-Extra', 'Extension', 'License-File')
  180. _LISTTUPLEFIELDS = ('Project-URL',)
  181. _ELEMENTSFIELD = ('Keywords',)
  182. _UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description')
  183. _MISSING = object()
  184. _FILESAFE = re.compile('[^A-Za-z0-9.]+')
  185. def _get_name_and_version(name, version, for_filename=False):
  186. """Return the distribution name with version.
  187. If for_filename is true, return a filename-escaped form."""
  188. if for_filename:
  189. # For both name and version any runs of non-alphanumeric or '.'
  190. # characters are replaced with a single '-'. Additionally any
  191. # spaces in the version string become '.'
  192. name = _FILESAFE.sub('-', name)
  193. version = _FILESAFE.sub('-', version.replace(' ', '.'))
  194. return '%s-%s' % (name, version)
  195. class LegacyMetadata(object):
  196. """The legacy metadata of a release.
  197. Supports versions 1.0, 1.1, 1.2, 2.0 and 1.3/2.1 (auto-detected). You can
  198. instantiate the class with one of these arguments (or none):
  199. - *path*, the path to a metadata file
  200. - *fileobj* give a file-like object with metadata as content
  201. - *mapping* is a dict-like object
  202. - *scheme* is a version scheme name
  203. """
  204. # TODO document the mapping API and UNKNOWN default key
  205. def __init__(self, path=None, fileobj=None, mapping=None,
  206. scheme='default'):
  207. if [path, fileobj, mapping].count(None) < 2:
  208. raise TypeError('path, fileobj and mapping are exclusive')
  209. self._fields = {}
  210. self.requires_files = []
  211. self._dependencies = None
  212. self.scheme = scheme
  213. if path is not None:
  214. self.read(path)
  215. elif fileobj is not None:
  216. self.read_file(fileobj)
  217. elif mapping is not None:
  218. self.update(mapping)
  219. self.set_metadata_version()
  220. def set_metadata_version(self):
  221. self._fields['Metadata-Version'] = _best_version(self._fields)
  222. def _write_field(self, fileobj, name, value):
  223. fileobj.write('%s: %s\n' % (name, value))
  224. def __getitem__(self, name):
  225. return self.get(name)
  226. def __setitem__(self, name, value):
  227. return self.set(name, value)
  228. def __delitem__(self, name):
  229. field_name = self._convert_name(name)
  230. try:
  231. del self._fields[field_name]
  232. except KeyError:
  233. raise KeyError(name)
  234. def __contains__(self, name):
  235. return (name in self._fields or
  236. self._convert_name(name) in self._fields)
  237. def _convert_name(self, name):
  238. if name in _ALL_FIELDS:
  239. return name
  240. name = name.replace('-', '_').lower()
  241. return _ATTR2FIELD.get(name, name)
  242. def _default_value(self, name):
  243. if name in _LISTFIELDS or name in _ELEMENTSFIELD:
  244. return []
  245. return 'UNKNOWN'
  246. def _remove_line_prefix(self, value):
  247. if self.metadata_version in ('1.0', '1.1'):
  248. return _LINE_PREFIX_PRE_1_2.sub('\n', value)
  249. else:
  250. return _LINE_PREFIX_1_2.sub('\n', value)
  251. def __getattr__(self, name):
  252. if name in _ATTR2FIELD:
  253. return self[name]
  254. raise AttributeError(name)
  255. #
  256. # Public API
  257. #
  258. # dependencies = property(_get_dependencies, _set_dependencies)
  259. def get_fullname(self, filesafe=False):
  260. """Return the distribution name with version.
  261. If filesafe is true, return a filename-escaped form."""
  262. return _get_name_and_version(self['Name'], self['Version'], filesafe)
  263. def is_field(self, name):
  264. """return True if name is a valid metadata key"""
  265. name = self._convert_name(name)
  266. return name in _ALL_FIELDS
  267. def is_multi_field(self, name):
  268. name = self._convert_name(name)
  269. return name in _LISTFIELDS
  270. def read(self, filepath):
  271. """Read the metadata values from a file path."""
  272. fp = codecs.open(filepath, 'r', encoding='utf-8')
  273. try:
  274. self.read_file(fp)
  275. finally:
  276. fp.close()
  277. def read_file(self, fileob):
  278. """Read the metadata values from a file object."""
  279. msg = message_from_file(fileob)
  280. self._fields['Metadata-Version'] = msg['metadata-version']
  281. # When reading, get all the fields we can
  282. for field in _ALL_FIELDS:
  283. if field not in msg:
  284. continue
  285. if field in _LISTFIELDS:
  286. # we can have multiple lines
  287. values = msg.get_all(field)
  288. if field in _LISTTUPLEFIELDS and values is not None:
  289. values = [tuple(value.split(',')) for value in values]
  290. self.set(field, values)
  291. else:
  292. # single line
  293. value = msg[field]
  294. if value is not None and value != 'UNKNOWN':
  295. self.set(field, value)
  296. # PEP 566 specifies that the body be used for the description, if
  297. # available
  298. body = msg.get_payload()
  299. self["Description"] = body if body else self["Description"]
  300. # logger.debug('Attempting to set metadata for %s', self)
  301. # self.set_metadata_version()
  302. def write(self, filepath, skip_unknown=False):
  303. """Write the metadata fields to filepath."""
  304. fp = codecs.open(filepath, 'w', encoding='utf-8')
  305. try:
  306. self.write_file(fp, skip_unknown)
  307. finally:
  308. fp.close()
  309. def write_file(self, fileobject, skip_unknown=False):
  310. """Write the PKG-INFO format data to a file object."""
  311. self.set_metadata_version()
  312. for field in _version2fieldlist(self['Metadata-Version']):
  313. values = self.get(field)
  314. if skip_unknown and values in ('UNKNOWN', [], ['UNKNOWN']):
  315. continue
  316. if field in _ELEMENTSFIELD:
  317. self._write_field(fileobject, field, ','.join(values))
  318. continue
  319. if field not in _LISTFIELDS:
  320. if field == 'Description':
  321. if self.metadata_version in ('1.0', '1.1'):
  322. values = values.replace('\n', '\n ')
  323. else:
  324. values = values.replace('\n', '\n |')
  325. values = [values]
  326. if field in _LISTTUPLEFIELDS:
  327. values = [','.join(value) for value in values]
  328. for value in values:
  329. self._write_field(fileobject, field, value)
  330. def update(self, other=None, **kwargs):
  331. """Set metadata values from the given iterable `other` and kwargs.
  332. Behavior is like `dict.update`: If `other` has a ``keys`` method,
  333. they are looped over and ``self[key]`` is assigned ``other[key]``.
  334. Else, ``other`` is an iterable of ``(key, value)`` iterables.
  335. Keys that don't match a metadata field or that have an empty value are
  336. dropped.
  337. """
  338. def _set(key, value):
  339. if key in _ATTR2FIELD and value:
  340. self.set(self._convert_name(key), value)
  341. if not other:
  342. # other is None or empty container
  343. pass
  344. elif hasattr(other, 'keys'):
  345. for k in other.keys():
  346. _set(k, other[k])
  347. else:
  348. for k, v in other:
  349. _set(k, v)
  350. if kwargs:
  351. for k, v in kwargs.items():
  352. _set(k, v)
  353. def set(self, name, value):
  354. """Control then set a metadata field."""
  355. name = self._convert_name(name)
  356. if ((name in _ELEMENTSFIELD or name == 'Platform') and
  357. not isinstance(value, (list, tuple))):
  358. if isinstance(value, string_types):
  359. value = [v.strip() for v in value.split(',')]
  360. else:
  361. value = []
  362. elif (name in _LISTFIELDS and
  363. not isinstance(value, (list, tuple))):
  364. if isinstance(value, string_types):
  365. value = [value]
  366. else:
  367. value = []
  368. if logger.isEnabledFor(logging.WARNING):
  369. project_name = self['Name']
  370. scheme = get_scheme(self.scheme)
  371. if name in _PREDICATE_FIELDS and value is not None:
  372. for v in value:
  373. # check that the values are valid
  374. if not scheme.is_valid_matcher(v.split(';')[0]):
  375. logger.warning(
  376. "'%s': '%s' is not valid (field '%s')",
  377. project_name, v, name)
  378. # FIXME this rejects UNKNOWN, is that right?
  379. elif name in _VERSIONS_FIELDS and value is not None:
  380. if not scheme.is_valid_constraint_list(value):
  381. logger.warning("'%s': '%s' is not a valid version (field '%s')",
  382. project_name, value, name)
  383. elif name in _VERSION_FIELDS and value is not None:
  384. if not scheme.is_valid_version(value):
  385. logger.warning("'%s': '%s' is not a valid version (field '%s')",
  386. project_name, value, name)
  387. if name in _UNICODEFIELDS:
  388. if name == 'Description':
  389. value = self._remove_line_prefix(value)
  390. self._fields[name] = value
  391. def get(self, name, default=_MISSING):
  392. """Get a metadata field."""
  393. name = self._convert_name(name)
  394. if name not in self._fields:
  395. if default is _MISSING:
  396. default = self._default_value(name)
  397. return default
  398. if name in _UNICODEFIELDS:
  399. value = self._fields[name]
  400. return value
  401. elif name in _LISTFIELDS:
  402. value = self._fields[name]
  403. if value is None:
  404. return []
  405. res = []
  406. for val in value:
  407. if name not in _LISTTUPLEFIELDS:
  408. res.append(val)
  409. else:
  410. # That's for Project-URL
  411. res.append((val[0], val[1]))
  412. return res
  413. elif name in _ELEMENTSFIELD:
  414. value = self._fields[name]
  415. if isinstance(value, string_types):
  416. return value.split(',')
  417. return self._fields[name]
  418. def check(self, strict=False):
  419. """Check if the metadata is compliant. If strict is True then raise if
  420. no Name or Version are provided"""
  421. self.set_metadata_version()
  422. # XXX should check the versions (if the file was loaded)
  423. missing, warnings = [], []
  424. for attr in ('Name', 'Version'): # required by PEP 345
  425. if attr not in self:
  426. missing.append(attr)
  427. if strict and missing != []:
  428. msg = 'missing required metadata: %s' % ', '.join(missing)
  429. raise MetadataMissingError(msg)
  430. for attr in ('Home-page', 'Author'):
  431. if attr not in self:
  432. missing.append(attr)
  433. # checking metadata 1.2 (XXX needs to check 1.1, 1.0)
  434. if self['Metadata-Version'] != '1.2':
  435. return missing, warnings
  436. scheme = get_scheme(self.scheme)
  437. def are_valid_constraints(value):
  438. for v in value:
  439. if not scheme.is_valid_matcher(v.split(';')[0]):
  440. return False
  441. return True
  442. for fields, controller in ((_PREDICATE_FIELDS, are_valid_constraints),
  443. (_VERSIONS_FIELDS,
  444. scheme.is_valid_constraint_list),
  445. (_VERSION_FIELDS,
  446. scheme.is_valid_version)):
  447. for field in fields:
  448. value = self.get(field, None)
  449. if value is not None and not controller(value):
  450. warnings.append("Wrong value for '%s': %s" % (field, value))
  451. return missing, warnings
  452. def todict(self, skip_missing=False):
  453. """Return fields as a dict.
  454. Field names will be converted to use the underscore-lowercase style
  455. instead of hyphen-mixed case (i.e. home_page instead of Home-page).
  456. This is as per https://www.python.org/dev/peps/pep-0566/#id17.
  457. """
  458. self.set_metadata_version()
  459. fields = _version2fieldlist(self['Metadata-Version'])
  460. data = {}
  461. for field_name in fields:
  462. if not skip_missing or field_name in self._fields:
  463. key = _FIELD2ATTR[field_name]
  464. if key != 'project_url':
  465. data[key] = self[field_name]
  466. else:
  467. data[key] = [','.join(u) for u in self[field_name]]
  468. return data
  469. def add_requirements(self, requirements):
  470. if self['Metadata-Version'] == '1.1':
  471. # we can't have 1.1 metadata *and* Setuptools requires
  472. for field in ('Obsoletes', 'Requires', 'Provides'):
  473. if field in self:
  474. del self[field]
  475. self['Requires-Dist'] += requirements
  476. # Mapping API
  477. # TODO could add iter* variants
  478. def keys(self):
  479. return list(_version2fieldlist(self['Metadata-Version']))
  480. def __iter__(self):
  481. for key in self.keys():
  482. yield key
  483. def values(self):
  484. return [self[key] for key in self.keys()]
  485. def items(self):
  486. return [(key, self[key]) for key in self.keys()]
  487. def __repr__(self):
  488. return '<%s %s %s>' % (self.__class__.__name__, self.name,
  489. self.version)
  490. METADATA_FILENAME = 'pydist.json'
  491. WHEEL_METADATA_FILENAME = 'metadata.json'
  492. LEGACY_METADATA_FILENAME = 'METADATA'
  493. class Metadata(object):
  494. """
  495. The metadata of a release. This implementation uses 2.1
  496. metadata where possible. If not possible, it wraps a LegacyMetadata
  497. instance which handles the key-value metadata format.
  498. """
  499. METADATA_VERSION_MATCHER = re.compile(r'^\d+(\.\d+)*$')
  500. NAME_MATCHER = re.compile('^[0-9A-Z]([0-9A-Z_.-]*[0-9A-Z])?$', re.I)
  501. FIELDNAME_MATCHER = re.compile('^[A-Z]([0-9A-Z-]*[0-9A-Z])?$', re.I)
  502. VERSION_MATCHER = PEP440_VERSION_RE
  503. SUMMARY_MATCHER = re.compile('.{1,2047}')
  504. METADATA_VERSION = '2.0'
  505. GENERATOR = 'distlib (%s)' % __version__
  506. MANDATORY_KEYS = {
  507. 'name': (),
  508. 'version': (),
  509. 'summary': ('legacy',),
  510. }
  511. INDEX_KEYS = ('name version license summary description author '
  512. 'author_email keywords platform home_page classifiers '
  513. 'download_url')
  514. DEPENDENCY_KEYS = ('extras run_requires test_requires build_requires '
  515. 'dev_requires provides meta_requires obsoleted_by '
  516. 'supports_environments')
  517. SYNTAX_VALIDATORS = {
  518. 'metadata_version': (METADATA_VERSION_MATCHER, ()),
  519. 'name': (NAME_MATCHER, ('legacy',)),
  520. 'version': (VERSION_MATCHER, ('legacy',)),
  521. 'summary': (SUMMARY_MATCHER, ('legacy',)),
  522. 'dynamic': (FIELDNAME_MATCHER, ('legacy',)),
  523. }
  524. __slots__ = ('_legacy', '_data', 'scheme')
  525. def __init__(self, path=None, fileobj=None, mapping=None,
  526. scheme='default'):
  527. if [path, fileobj, mapping].count(None) < 2:
  528. raise TypeError('path, fileobj and mapping are exclusive')
  529. self._legacy = None
  530. self._data = None
  531. self.scheme = scheme
  532. #import pdb; pdb.set_trace()
  533. if mapping is not None:
  534. try:
  535. self._validate_mapping(mapping, scheme)
  536. self._data = mapping
  537. except MetadataUnrecognizedVersionError:
  538. self._legacy = LegacyMetadata(mapping=mapping, scheme=scheme)
  539. self.validate()
  540. else:
  541. data = None
  542. if path:
  543. with open(path, 'rb') as f:
  544. data = f.read()
  545. elif fileobj:
  546. data = fileobj.read()
  547. if data is None:
  548. # Initialised with no args - to be added
  549. self._data = {
  550. 'metadata_version': self.METADATA_VERSION,
  551. 'generator': self.GENERATOR,
  552. }
  553. else:
  554. if not isinstance(data, text_type):
  555. data = data.decode('utf-8')
  556. try:
  557. self._data = json.loads(data)
  558. self._validate_mapping(self._data, scheme)
  559. except ValueError:
  560. # Note: MetadataUnrecognizedVersionError does not
  561. # inherit from ValueError (it's a DistlibException,
  562. # which should not inherit from ValueError).
  563. # The ValueError comes from the json.load - if that
  564. # succeeds and we get a validation error, we want
  565. # that to propagate
  566. self._legacy = LegacyMetadata(fileobj=StringIO(data),
  567. scheme=scheme)
  568. self.validate()
  569. common_keys = set(('name', 'version', 'license', 'keywords', 'summary'))
  570. none_list = (None, list)
  571. none_dict = (None, dict)
  572. mapped_keys = {
  573. 'run_requires': ('Requires-Dist', list),
  574. 'build_requires': ('Setup-Requires-Dist', list),
  575. 'dev_requires': none_list,
  576. 'test_requires': none_list,
  577. 'meta_requires': none_list,
  578. 'extras': ('Provides-Extra', list),
  579. 'modules': none_list,
  580. 'namespaces': none_list,
  581. 'exports': none_dict,
  582. 'commands': none_dict,
  583. 'classifiers': ('Classifier', list),
  584. 'source_url': ('Download-URL', None),
  585. 'metadata_version': ('Metadata-Version', None),
  586. }
  587. del none_list, none_dict
  588. def __getattribute__(self, key):
  589. common = object.__getattribute__(self, 'common_keys')
  590. mapped = object.__getattribute__(self, 'mapped_keys')
  591. if key in mapped:
  592. lk, maker = mapped[key]
  593. if self._legacy:
  594. if lk is None:
  595. result = None if maker is None else maker()
  596. else:
  597. result = self._legacy.get(lk)
  598. else:
  599. value = None if maker is None else maker()
  600. if key not in ('commands', 'exports', 'modules', 'namespaces',
  601. 'classifiers'):
  602. result = self._data.get(key, value)
  603. else:
  604. # special cases for PEP 459
  605. sentinel = object()
  606. result = sentinel
  607. d = self._data.get('extensions')
  608. if d:
  609. if key == 'commands':
  610. result = d.get('python.commands', value)
  611. elif key == 'classifiers':
  612. d = d.get('python.details')
  613. if d:
  614. result = d.get(key, value)
  615. else:
  616. d = d.get('python.exports')
  617. if not d:
  618. d = self._data.get('python.exports')
  619. if d:
  620. result = d.get(key, value)
  621. if result is sentinel:
  622. result = value
  623. elif key not in common:
  624. result = object.__getattribute__(self, key)
  625. elif self._legacy:
  626. result = self._legacy.get(key)
  627. else:
  628. result = self._data.get(key)
  629. return result
  630. def _validate_value(self, key, value, scheme=None):
  631. if key in self.SYNTAX_VALIDATORS:
  632. pattern, exclusions = self.SYNTAX_VALIDATORS[key]
  633. if (scheme or self.scheme) not in exclusions:
  634. m = pattern.match(value)
  635. if not m:
  636. raise MetadataInvalidError("'%s' is an invalid value for "
  637. "the '%s' property" % (value,
  638. key))
  639. def __setattr__(self, key, value):
  640. self._validate_value(key, value)
  641. common = object.__getattribute__(self, 'common_keys')
  642. mapped = object.__getattribute__(self, 'mapped_keys')
  643. if key in mapped:
  644. lk, _ = mapped[key]
  645. if self._legacy:
  646. if lk is None:
  647. raise NotImplementedError
  648. self._legacy[lk] = value
  649. elif key not in ('commands', 'exports', 'modules', 'namespaces',
  650. 'classifiers'):
  651. self._data[key] = value
  652. else:
  653. # special cases for PEP 459
  654. d = self._data.setdefault('extensions', {})
  655. if key == 'commands':
  656. d['python.commands'] = value
  657. elif key == 'classifiers':
  658. d = d.setdefault('python.details', {})
  659. d[key] = value
  660. else:
  661. d = d.setdefault('python.exports', {})
  662. d[key] = value
  663. elif key not in common:
  664. object.__setattr__(self, key, value)
  665. else:
  666. if key == 'keywords':
  667. if isinstance(value, string_types):
  668. value = value.strip()
  669. if value:
  670. value = value.split()
  671. else:
  672. value = []
  673. if self._legacy:
  674. self._legacy[key] = value
  675. else:
  676. self._data[key] = value
  677. @property
  678. def name_and_version(self):
  679. return _get_name_and_version(self.name, self.version, True)
  680. @property
  681. def provides(self):
  682. if self._legacy:
  683. result = self._legacy['Provides-Dist']
  684. else:
  685. result = self._data.setdefault('provides', [])
  686. s = '%s (%s)' % (self.name, self.version)
  687. if s not in result:
  688. result.append(s)
  689. return result
  690. @provides.setter
  691. def provides(self, value):
  692. if self._legacy:
  693. self._legacy['Provides-Dist'] = value
  694. else:
  695. self._data['provides'] = value
  696. def get_requirements(self, reqts, extras=None, env=None):
  697. """
  698. Base method to get dependencies, given a set of extras
  699. to satisfy and an optional environment context.
  700. :param reqts: A list of sometimes-wanted dependencies,
  701. perhaps dependent on extras and environment.
  702. :param extras: A list of optional components being requested.
  703. :param env: An optional environment for marker evaluation.
  704. """
  705. if self._legacy:
  706. result = reqts
  707. else:
  708. result = []
  709. extras = get_extras(extras or [], self.extras)
  710. for d in reqts:
  711. if 'extra' not in d and 'environment' not in d:
  712. # unconditional
  713. include = True
  714. else:
  715. if 'extra' not in d:
  716. # Not extra-dependent - only environment-dependent
  717. include = True
  718. else:
  719. include = d.get('extra') in extras
  720. if include:
  721. # Not excluded because of extras, check environment
  722. marker = d.get('environment')
  723. if marker:
  724. include = interpret(marker, env)
  725. if include:
  726. result.extend(d['requires'])
  727. for key in ('build', 'dev', 'test'):
  728. e = ':%s:' % key
  729. if e in extras:
  730. extras.remove(e)
  731. # A recursive call, but it should terminate since 'test'
  732. # has been removed from the extras
  733. reqts = self._data.get('%s_requires' % key, [])
  734. result.extend(self.get_requirements(reqts, extras=extras,
  735. env=env))
  736. return result
  737. @property
  738. def dictionary(self):
  739. if self._legacy:
  740. return self._from_legacy()
  741. return self._data
  742. @property
  743. def dependencies(self):
  744. if self._legacy:
  745. raise NotImplementedError
  746. else:
  747. return extract_by_key(self._data, self.DEPENDENCY_KEYS)
  748. @dependencies.setter
  749. def dependencies(self, value):
  750. if self._legacy:
  751. raise NotImplementedError
  752. else:
  753. self._data.update(value)
  754. def _validate_mapping(self, mapping, scheme):
  755. if mapping.get('metadata_version') != self.METADATA_VERSION:
  756. raise MetadataUnrecognizedVersionError()
  757. missing = []
  758. for key, exclusions in self.MANDATORY_KEYS.items():
  759. if key not in mapping:
  760. if scheme not in exclusions:
  761. missing.append(key)
  762. if missing:
  763. msg = 'Missing metadata items: %s' % ', '.join(missing)
  764. raise MetadataMissingError(msg)
  765. for k, v in mapping.items():
  766. self._validate_value(k, v, scheme)
  767. def validate(self):
  768. if self._legacy:
  769. missing, warnings = self._legacy.check(True)
  770. if missing or warnings:
  771. logger.warning('Metadata: missing: %s, warnings: %s',
  772. missing, warnings)
  773. else:
  774. self._validate_mapping(self._data, self.scheme)
  775. def todict(self):
  776. if self._legacy:
  777. return self._legacy.todict(True)
  778. else:
  779. result = extract_by_key(self._data, self.INDEX_KEYS)
  780. return result
  781. def _from_legacy(self):
  782. assert self._legacy and not self._data
  783. result = {
  784. 'metadata_version': self.METADATA_VERSION,
  785. 'generator': self.GENERATOR,
  786. }
  787. lmd = self._legacy.todict(True) # skip missing ones
  788. for k in ('name', 'version', 'license', 'summary', 'description',
  789. 'classifier'):
  790. if k in lmd:
  791. if k == 'classifier':
  792. nk = 'classifiers'
  793. else:
  794. nk = k
  795. result[nk] = lmd[k]
  796. kw = lmd.get('Keywords', [])
  797. if kw == ['']:
  798. kw = []
  799. result['keywords'] = kw
  800. keys = (('requires_dist', 'run_requires'),
  801. ('setup_requires_dist', 'build_requires'))
  802. for ok, nk in keys:
  803. if ok in lmd and lmd[ok]:
  804. result[nk] = [{'requires': lmd[ok]}]
  805. result['provides'] = self.provides
  806. author = {}
  807. maintainer = {}
  808. return result
  809. LEGACY_MAPPING = {
  810. 'name': 'Name',
  811. 'version': 'Version',
  812. ('extensions', 'python.details', 'license'): 'License',
  813. 'summary': 'Summary',
  814. 'description': 'Description',
  815. ('extensions', 'python.project', 'project_urls', 'Home'): 'Home-page',
  816. ('extensions', 'python.project', 'contacts', 0, 'name'): 'Author',
  817. ('extensions', 'python.project', 'contacts', 0, 'email'): 'Author-email',
  818. 'source_url': 'Download-URL',
  819. ('extensions', 'python.details', 'classifiers'): 'Classifier',
  820. }
  821. def _to_legacy(self):
  822. def process_entries(entries):
  823. reqts = set()
  824. for e in entries:
  825. extra = e.get('extra')
  826. env = e.get('environment')
  827. rlist = e['requires']
  828. for r in rlist:
  829. if not env and not extra:
  830. reqts.add(r)
  831. else:
  832. marker = ''
  833. if extra:
  834. marker = 'extra == "%s"' % extra
  835. if env:
  836. if marker:
  837. marker = '(%s) and %s' % (env, marker)
  838. else:
  839. marker = env
  840. reqts.add(';'.join((r, marker)))
  841. return reqts
  842. assert self._data and not self._legacy
  843. result = LegacyMetadata()
  844. nmd = self._data
  845. # import pdb; pdb.set_trace()
  846. for nk, ok in self.LEGACY_MAPPING.items():
  847. if not isinstance(nk, tuple):
  848. if nk in nmd:
  849. result[ok] = nmd[nk]
  850. else:
  851. d = nmd
  852. found = True
  853. for k in nk:
  854. try:
  855. d = d[k]
  856. except (KeyError, IndexError):
  857. found = False
  858. break
  859. if found:
  860. result[ok] = d
  861. r1 = process_entries(self.run_requires + self.meta_requires)
  862. r2 = process_entries(self.build_requires + self.dev_requires)
  863. if self.extras:
  864. result['Provides-Extra'] = sorted(self.extras)
  865. result['Requires-Dist'] = sorted(r1)
  866. result['Setup-Requires-Dist'] = sorted(r2)
  867. # TODO: any other fields wanted
  868. return result
  869. def write(self, path=None, fileobj=None, legacy=False, skip_unknown=True):
  870. if [path, fileobj].count(None) != 1:
  871. raise ValueError('Exactly one of path and fileobj is needed')
  872. self.validate()
  873. if legacy:
  874. if self._legacy:
  875. legacy_md = self._legacy
  876. else:
  877. legacy_md = self._to_legacy()
  878. if path:
  879. legacy_md.write(path, skip_unknown=skip_unknown)
  880. else:
  881. legacy_md.write_file(fileobj, skip_unknown=skip_unknown)
  882. else:
  883. if self._legacy:
  884. d = self._from_legacy()
  885. else:
  886. d = self._data
  887. if fileobj:
  888. json.dump(d, fileobj, ensure_ascii=True, indent=2,
  889. sort_keys=True)
  890. else:
  891. with codecs.open(path, 'w', 'utf-8') as f:
  892. json.dump(d, f, ensure_ascii=True, indent=2,
  893. sort_keys=True)
  894. def add_requirements(self, requirements):
  895. if self._legacy:
  896. self._legacy.add_requirements(requirements)
  897. else:
  898. run_requires = self._data.setdefault('run_requires', [])
  899. always = None
  900. for entry in run_requires:
  901. if 'environment' not in entry and 'extra' not in entry:
  902. always = entry
  903. break
  904. if always is None:
  905. always = { 'requires': requirements }
  906. run_requires.insert(0, always)
  907. else:
  908. rset = set(always['requires']) | set(requirements)
  909. always['requires'] = sorted(rset)
  910. def __repr__(self):
  911. name = self.name or '(no name)'
  912. version = self.version or 'no version'
  913. return '<%s %s %s (%s)>' % (self.__class__.__name__,
  914. self.metadata_version, name, version)