Development of an internal social media platform with personalised dashboards for students
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.

metadata.py 39KB

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