metadata.py 38 KB

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