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.

338 lines
11 KiB

7 years ago
  1. """
  2. Tools for converting old- to new-style metadata.
  3. """
  4. import email.parser
  5. import os.path
  6. import re
  7. import textwrap
  8. from collections import namedtuple, OrderedDict
  9. import pkg_resources
  10. from . import __version__ as wheel_version
  11. from .pkginfo import read_pkg_info
  12. from .util import OrderedDefaultDict
  13. METADATA_VERSION = "2.0"
  14. PLURAL_FIELDS = {"classifier": "classifiers",
  15. "provides_dist": "provides",
  16. "provides_extra": "extras"}
  17. SKIP_FIELDS = set()
  18. CONTACT_FIELDS = (({"email": "author_email", "name": "author"},
  19. "author"),
  20. ({"email": "maintainer_email", "name": "maintainer"},
  21. "maintainer"))
  22. # commonly filled out as "UNKNOWN" by distutils:
  23. UNKNOWN_FIELDS = {"author", "author_email", "platform", "home_page", "license"}
  24. # Wheel itself is probably the only program that uses non-extras markers
  25. # in METADATA/PKG-INFO. Support its syntax with the extra at the end only.
  26. EXTRA_RE = re.compile("""^(?P<package>.*?)(;\s*(?P<condition>.*?)(extra == '(?P<extra>.*?)')?)$""")
  27. KEYWORDS_RE = re.compile("[\0-,]+")
  28. MayRequiresKey = namedtuple('MayRequiresKey', ('condition', 'extra'))
  29. def unique(iterable):
  30. """
  31. Yield unique values in iterable, preserving order.
  32. """
  33. seen = set()
  34. for value in iterable:
  35. if value not in seen:
  36. seen.add(value)
  37. yield value
  38. def handle_requires(metadata, pkg_info, key):
  39. """
  40. Place the runtime requirements from pkg_info into metadata.
  41. """
  42. may_requires = OrderedDefaultDict(list)
  43. for value in sorted(pkg_info.get_all(key)):
  44. extra_match = EXTRA_RE.search(value)
  45. if extra_match:
  46. groupdict = extra_match.groupdict()
  47. condition = groupdict['condition']
  48. extra = groupdict['extra']
  49. package = groupdict['package']
  50. if condition.endswith(' and '):
  51. condition = condition[:-5]
  52. else:
  53. condition, extra = None, None
  54. package = value
  55. key = MayRequiresKey(condition, extra)
  56. may_requires[key].append(package)
  57. if may_requires:
  58. metadata['run_requires'] = []
  59. def sort_key(item):
  60. # Both condition and extra could be None, which can't be compared
  61. # against strings in Python 3.
  62. key, value = item
  63. if key.condition is None:
  64. return ''
  65. return key.condition
  66. for key, value in sorted(may_requires.items(), key=sort_key):
  67. may_requirement = OrderedDict((('requires', value),))
  68. if key.extra:
  69. may_requirement['extra'] = key.extra
  70. if key.condition:
  71. may_requirement['environment'] = key.condition
  72. metadata['run_requires'].append(may_requirement)
  73. if 'extras' not in metadata:
  74. metadata['extras'] = []
  75. metadata['extras'].extend([key.extra for key in may_requires.keys() if key.extra])
  76. def pkginfo_to_dict(path, distribution=None):
  77. """
  78. Convert PKG-INFO to a prototype Metadata 2.0 (PEP 426) dict.
  79. The description is included under the key ['description'] rather than
  80. being written to a separate file.
  81. path: path to PKG-INFO file
  82. distribution: optional distutils Distribution()
  83. """
  84. metadata = OrderedDefaultDict(
  85. lambda: OrderedDefaultDict(lambda: OrderedDefaultDict(OrderedDict)))
  86. metadata["generator"] = "bdist_wheel (" + wheel_version + ")"
  87. try:
  88. unicode
  89. pkg_info = read_pkg_info(path)
  90. except NameError:
  91. with open(path, 'rb') as pkg_info_file:
  92. pkg_info = email.parser.Parser().parsestr(pkg_info_file.read().decode('utf-8'))
  93. description = None
  94. if pkg_info['Summary']:
  95. metadata['summary'] = pkginfo_unicode(pkg_info, 'Summary')
  96. del pkg_info['Summary']
  97. if pkg_info['Description']:
  98. description = dedent_description(pkg_info)
  99. del pkg_info['Description']
  100. else:
  101. payload = pkg_info.get_payload()
  102. if isinstance(payload, bytes):
  103. # Avoid a Python 2 Unicode error.
  104. # We still suffer ? glyphs on Python 3.
  105. payload = payload.decode('utf-8')
  106. if payload:
  107. description = payload
  108. if description:
  109. pkg_info['description'] = description
  110. for key in sorted(unique(k.lower() for k in pkg_info.keys())):
  111. low_key = key.replace('-', '_')
  112. if low_key in SKIP_FIELDS:
  113. continue
  114. if low_key in UNKNOWN_FIELDS and pkg_info.get(key) == 'UNKNOWN':
  115. continue
  116. if low_key in sorted(PLURAL_FIELDS):
  117. metadata[PLURAL_FIELDS[low_key]] = pkg_info.get_all(key)
  118. elif low_key == "requires_dist":
  119. handle_requires(metadata, pkg_info, key)
  120. elif low_key == 'provides_extra':
  121. if 'extras' not in metadata:
  122. metadata['extras'] = []
  123. metadata['extras'].extend(pkg_info.get_all(key))
  124. elif low_key == 'home_page':
  125. metadata['extensions']['python.details']['project_urls'] = {'Home': pkg_info[key]}
  126. elif low_key == 'keywords':
  127. metadata['keywords'] = KEYWORDS_RE.split(pkg_info[key])
  128. else:
  129. metadata[low_key] = pkg_info[key]
  130. metadata['metadata_version'] = METADATA_VERSION
  131. if 'extras' in metadata:
  132. metadata['extras'] = sorted(set(metadata['extras']))
  133. # include more information if distribution is available
  134. if distribution:
  135. for requires, attr in (('test_requires', 'tests_require'),):
  136. try:
  137. requirements = getattr(distribution, attr)
  138. if isinstance(requirements, list):
  139. new_requirements = sorted(convert_requirements(requirements))
  140. metadata[requires] = [{'requires': new_requirements}]
  141. except AttributeError:
  142. pass
  143. # handle contacts
  144. contacts = []
  145. for contact_type, role in CONTACT_FIELDS:
  146. contact = OrderedDict()
  147. for key in sorted(contact_type):
  148. if contact_type[key] in metadata:
  149. contact[key] = metadata.pop(contact_type[key])
  150. if contact:
  151. contact['role'] = role
  152. contacts.append(contact)
  153. if contacts:
  154. metadata['extensions']['python.details']['contacts'] = contacts
  155. # convert entry points to exports
  156. try:
  157. with open(os.path.join(os.path.dirname(path), "entry_points.txt"), "r") as ep_file:
  158. ep_map = pkg_resources.EntryPoint.parse_map(ep_file.read())
  159. exports = OrderedDict()
  160. for group, items in sorted(ep_map.items()):
  161. exports[group] = OrderedDict()
  162. for item in sorted(map(str, items.values())):
  163. name, export = item.split(' = ', 1)
  164. exports[group][name] = export
  165. if exports:
  166. metadata['extensions']['python.exports'] = exports
  167. except IOError:
  168. pass
  169. # copy console_scripts entry points to commands
  170. if 'python.exports' in metadata['extensions']:
  171. for (ep_script, wrap_script) in (('console_scripts', 'wrap_console'),
  172. ('gui_scripts', 'wrap_gui')):
  173. if ep_script in metadata['extensions']['python.exports']:
  174. metadata['extensions']['python.commands'][wrap_script] = \
  175. metadata['extensions']['python.exports'][ep_script]
  176. return metadata
  177. def requires_to_requires_dist(requirement):
  178. """Compose the version predicates for requirement in PEP 345 fashion."""
  179. requires_dist = []
  180. for op, ver in requirement.specs:
  181. requires_dist.append(op + ver)
  182. if not requires_dist:
  183. return ''
  184. return " (%s)" % ','.join(sorted(requires_dist))
  185. def convert_requirements(requirements):
  186. """Yield Requires-Dist: strings for parsed requirements strings."""
  187. for req in requirements:
  188. parsed_requirement = pkg_resources.Requirement.parse(req)
  189. spec = requires_to_requires_dist(parsed_requirement)
  190. extras = ",".join(parsed_requirement.extras)
  191. if extras:
  192. extras = "[%s]" % extras
  193. yield (parsed_requirement.project_name + extras + spec)
  194. def generate_requirements(extras_require):
  195. """
  196. Convert requirements from a setup()-style dictionary to ('Requires-Dist', 'requirement')
  197. and ('Provides-Extra', 'extra') tuples.
  198. extras_require is a dictionary of {extra: [requirements]} as passed to setup(),
  199. using the empty extra {'': [requirements]} to hold install_requires.
  200. """
  201. for extra, depends in extras_require.items():
  202. condition = ''
  203. if extra and ':' in extra: # setuptools extra:condition syntax
  204. extra, condition = extra.split(':', 1)
  205. extra = pkg_resources.safe_extra(extra)
  206. if extra:
  207. yield ('Provides-Extra', extra)
  208. if condition:
  209. condition += " and "
  210. condition += "extra == '%s'" % extra
  211. if condition:
  212. condition = '; ' + condition
  213. for new_req in convert_requirements(depends):
  214. yield ('Requires-Dist', new_req + condition)
  215. def pkginfo_to_metadata(egg_info_path, pkginfo_path):
  216. """
  217. Convert .egg-info directory with PKG-INFO to the Metadata 1.3 aka
  218. old-draft Metadata 2.0 format.
  219. """
  220. pkg_info = read_pkg_info(pkginfo_path)
  221. pkg_info.replace_header('Metadata-Version', '2.0')
  222. requires_path = os.path.join(egg_info_path, 'requires.txt')
  223. if os.path.exists(requires_path):
  224. with open(requires_path) as requires_file:
  225. requires = requires_file.read()
  226. for extra, reqs in sorted(pkg_resources.split_sections(requires),
  227. key=lambda x: x[0] or ''):
  228. for item in generate_requirements({extra: reqs}):
  229. pkg_info[item[0]] = item[1]
  230. description = pkg_info['Description']
  231. if description:
  232. pkg_info.set_payload(dedent_description(pkg_info))
  233. del pkg_info['Description']
  234. return pkg_info
  235. def pkginfo_unicode(pkg_info, field):
  236. """Hack to coax Unicode out of an email Message() - Python 3.3+"""
  237. text = pkg_info[field]
  238. field = field.lower()
  239. if not isinstance(text, str):
  240. if not hasattr(pkg_info, 'raw_items'): # Python 3.2
  241. return str(text)
  242. for item in pkg_info.raw_items():
  243. if item[0].lower() == field:
  244. text = item[1].encode('ascii', 'surrogateescape') \
  245. .decode('utf-8')
  246. break
  247. return text
  248. def dedent_description(pkg_info):
  249. """
  250. Dedent and convert pkg_info['Description'] to Unicode.
  251. """
  252. description = pkg_info['Description']
  253. # Python 3 Unicode handling, sorta.
  254. surrogates = False
  255. if not isinstance(description, str):
  256. surrogates = True
  257. description = pkginfo_unicode(pkg_info, 'Description')
  258. description_lines = description.splitlines()
  259. description_dedent = '\n'.join(
  260. # if the first line of long_description is blank,
  261. # the first line here will be indented.
  262. (description_lines[0].lstrip(),
  263. textwrap.dedent('\n'.join(description_lines[1:])),
  264. '\n'))
  265. if surrogates:
  266. description_dedent = description_dedent \
  267. .encode("utf8") \
  268. .decode("ascii", "surrogateescape")
  269. return description_dedent
  270. if __name__ == "__main__":
  271. import sys
  272. import pprint
  273. pprint.pprint(pkginfo_to_dict(sys.argv[1]))

Powered by TurnKey Linux.