metadata.py 39 KB

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