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.

141 lines
4.6 KiB

4 years ago
  1. """
  2. Tools for converting old- to new-style metadata.
  3. """
  4. import os.path
  5. import re
  6. import textwrap
  7. import pkg_resources
  8. from .pkginfo import read_pkg_info
  9. # Wheel itself is probably the only program that uses non-extras markers
  10. # in METADATA/PKG-INFO. Support its syntax with the extra at the end only.
  11. EXTRA_RE = re.compile(
  12. r"""^(?P<package>.*?)(;\s*(?P<condition>.*?)(extra == '(?P<extra>.*?)')?)$""")
  13. def requires_to_requires_dist(requirement):
  14. """Return the version specifier for a requirement in PEP 345/566 fashion."""
  15. if getattr(requirement, 'url', None):
  16. return " @ " + requirement.url
  17. requires_dist = []
  18. for op, ver in requirement.specs:
  19. requires_dist.append(op + ver)
  20. if not requires_dist:
  21. return ''
  22. return " (%s)" % ','.join(sorted(requires_dist))
  23. def convert_requirements(requirements):
  24. """Yield Requires-Dist: strings for parsed requirements strings."""
  25. for req in requirements:
  26. parsed_requirement = pkg_resources.Requirement.parse(req)
  27. spec = requires_to_requires_dist(parsed_requirement)
  28. extras = ",".join(parsed_requirement.extras)
  29. if extras:
  30. extras = "[%s]" % extras
  31. yield (parsed_requirement.project_name + extras + spec)
  32. def generate_requirements(extras_require):
  33. """
  34. Convert requirements from a setup()-style dictionary to ('Requires-Dist', 'requirement')
  35. and ('Provides-Extra', 'extra') tuples.
  36. extras_require is a dictionary of {extra: [requirements]} as passed to setup(),
  37. using the empty extra {'': [requirements]} to hold install_requires.
  38. """
  39. for extra, depends in extras_require.items():
  40. condition = ''
  41. extra = extra or ''
  42. if ':' in extra: # setuptools extra:condition syntax
  43. extra, condition = extra.split(':', 1)
  44. extra = pkg_resources.safe_extra(extra)
  45. if extra:
  46. yield 'Provides-Extra', extra
  47. if condition:
  48. condition = "(" + condition + ") and "
  49. condition += "extra == '%s'" % extra
  50. if condition:
  51. condition = ' ; ' + condition
  52. for new_req in convert_requirements(depends):
  53. yield 'Requires-Dist', new_req + condition
  54. def pkginfo_to_metadata(egg_info_path, pkginfo_path):
  55. """
  56. Convert .egg-info directory with PKG-INFO to the Metadata 2.1 format
  57. """
  58. pkg_info = read_pkg_info(pkginfo_path)
  59. pkg_info.replace_header('Metadata-Version', '2.1')
  60. # Those will be regenerated from `requires.txt`.
  61. del pkg_info['Provides-Extra']
  62. del pkg_info['Requires-Dist']
  63. requires_path = os.path.join(egg_info_path, 'requires.txt')
  64. if os.path.exists(requires_path):
  65. with open(requires_path) as requires_file:
  66. requires = requires_file.read()
  67. parsed_requirements = sorted(pkg_resources.split_sections(requires),
  68. key=lambda x: x[0] or '')
  69. for extra, reqs in parsed_requirements:
  70. for key, value in generate_requirements({extra: reqs}):
  71. if (key, value) not in pkg_info.items():
  72. pkg_info[key] = value
  73. description = pkg_info['Description']
  74. if description:
  75. pkg_info.set_payload(dedent_description(pkg_info))
  76. del pkg_info['Description']
  77. return pkg_info
  78. def pkginfo_unicode(pkg_info, field):
  79. """Hack to coax Unicode out of an email Message() - Python 3.3+"""
  80. text = pkg_info[field]
  81. field = field.lower()
  82. if not isinstance(text, str):
  83. if not hasattr(pkg_info, 'raw_items'): # Python 3.2
  84. return str(text)
  85. for item in pkg_info.raw_items():
  86. if item[0].lower() == field:
  87. text = item[1].encode('ascii', 'surrogateescape') \
  88. .decode('utf-8')
  89. break
  90. return text
  91. def dedent_description(pkg_info):
  92. """
  93. Dedent and convert pkg_info['Description'] to Unicode.
  94. """
  95. description = pkg_info['Description']
  96. # Python 3 Unicode handling, sorta.
  97. surrogates = False
  98. if not isinstance(description, str):
  99. surrogates = True
  100. description = pkginfo_unicode(pkg_info, 'Description')
  101. description_lines = description.splitlines()
  102. description_dedent = '\n'.join(
  103. # if the first line of long_description is blank,
  104. # the first line here will be indented.
  105. (description_lines[0].lstrip(),
  106. textwrap.dedent('\n'.join(description_lines[1:])),
  107. '\n'))
  108. if surrogates:
  109. description_dedent = description_dedent \
  110. .encode("utf8") \
  111. .decode("ascii", "surrogateescape")
  112. return description_dedent