_core_metadata.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. """
  2. Handling of Core Metadata for Python packages (including reading and writing).
  3. See: https://packaging.python.org/en/latest/specifications/core-metadata/
  4. """
  5. import os
  6. import stat
  7. import textwrap
  8. from email import message_from_file
  9. from email.message import Message
  10. from tempfile import NamedTemporaryFile
  11. from typing import Optional, List
  12. from distutils.util import rfc822_escape
  13. from . import _normalization, _reqs
  14. from .extern.packaging.markers import Marker
  15. from .extern.packaging.requirements import Requirement
  16. from .extern.packaging.version import Version
  17. from .warnings import SetuptoolsDeprecationWarning
  18. def get_metadata_version(self):
  19. mv = getattr(self, 'metadata_version', None)
  20. if mv is None:
  21. mv = Version('2.1')
  22. self.metadata_version = mv
  23. return mv
  24. def rfc822_unescape(content: str) -> str:
  25. """Reverse RFC-822 escaping by removing leading whitespaces from content."""
  26. lines = content.splitlines()
  27. if len(lines) == 1:
  28. return lines[0].lstrip()
  29. return '\n'.join((lines[0].lstrip(), textwrap.dedent('\n'.join(lines[1:]))))
  30. def _read_field_from_msg(msg: Message, field: str) -> Optional[str]:
  31. """Read Message header field."""
  32. value = msg[field]
  33. if value == 'UNKNOWN':
  34. return None
  35. return value
  36. def _read_field_unescaped_from_msg(msg: Message, field: str) -> Optional[str]:
  37. """Read Message header field and apply rfc822_unescape."""
  38. value = _read_field_from_msg(msg, field)
  39. if value is None:
  40. return value
  41. return rfc822_unescape(value)
  42. def _read_list_from_msg(msg: Message, field: str) -> Optional[List[str]]:
  43. """Read Message header field and return all results as list."""
  44. values = msg.get_all(field, None)
  45. if values == []:
  46. return None
  47. return values
  48. def _read_payload_from_msg(msg: Message) -> Optional[str]:
  49. value = msg.get_payload().strip()
  50. if value == 'UNKNOWN' or not value:
  51. return None
  52. return value
  53. def read_pkg_file(self, file):
  54. """Reads the metadata values from a file object."""
  55. msg = message_from_file(file)
  56. self.metadata_version = Version(msg['metadata-version'])
  57. self.name = _read_field_from_msg(msg, 'name')
  58. self.version = _read_field_from_msg(msg, 'version')
  59. self.description = _read_field_from_msg(msg, 'summary')
  60. # we are filling author only.
  61. self.author = _read_field_from_msg(msg, 'author')
  62. self.maintainer = None
  63. self.author_email = _read_field_from_msg(msg, 'author-email')
  64. self.maintainer_email = None
  65. self.url = _read_field_from_msg(msg, 'home-page')
  66. self.download_url = _read_field_from_msg(msg, 'download-url')
  67. self.license = _read_field_unescaped_from_msg(msg, 'license')
  68. self.long_description = _read_field_unescaped_from_msg(msg, 'description')
  69. if self.long_description is None and self.metadata_version >= Version('2.1'):
  70. self.long_description = _read_payload_from_msg(msg)
  71. self.description = _read_field_from_msg(msg, 'summary')
  72. if 'keywords' in msg:
  73. self.keywords = _read_field_from_msg(msg, 'keywords').split(',')
  74. self.platforms = _read_list_from_msg(msg, 'platform')
  75. self.classifiers = _read_list_from_msg(msg, 'classifier')
  76. # PEP 314 - these fields only exist in 1.1
  77. if self.metadata_version == Version('1.1'):
  78. self.requires = _read_list_from_msg(msg, 'requires')
  79. self.provides = _read_list_from_msg(msg, 'provides')
  80. self.obsoletes = _read_list_from_msg(msg, 'obsoletes')
  81. else:
  82. self.requires = None
  83. self.provides = None
  84. self.obsoletes = None
  85. self.license_files = _read_list_from_msg(msg, 'license-file')
  86. def single_line(val):
  87. """
  88. Quick and dirty validation for Summary pypa/setuptools#1390.
  89. """
  90. if '\n' in val:
  91. # TODO: Replace with `raise ValueError("newlines not allowed")`
  92. # after reviewing #2893.
  93. msg = "newlines are not allowed in `summary` and will break in the future"
  94. SetuptoolsDeprecationWarning.emit("Invalid config.", msg)
  95. # due_date is undefined. Controversial change, there was a lot of push back.
  96. val = val.strip().split('\n')[0]
  97. return val
  98. def write_pkg_info(self, base_dir):
  99. """Write the PKG-INFO file into the release tree."""
  100. temp = ""
  101. final = os.path.join(base_dir, 'PKG-INFO')
  102. try:
  103. # Use a temporary file while writing to avoid race conditions
  104. # (e.g. `importlib.metadata` reading `.egg-info/PKG-INFO`):
  105. with NamedTemporaryFile("w", encoding="utf-8", dir=base_dir, delete=False) as f:
  106. temp = f.name
  107. self.write_pkg_file(f)
  108. permissions = stat.S_IMODE(os.lstat(temp).st_mode)
  109. os.chmod(temp, permissions | stat.S_IRGRP | stat.S_IROTH)
  110. os.replace(temp, final) # atomic operation.
  111. finally:
  112. if temp and os.path.exists(temp):
  113. os.remove(temp)
  114. # Based on Python 3.5 version
  115. def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME
  116. """Write the PKG-INFO format data to a file object."""
  117. version = self.get_metadata_version()
  118. def write_field(key, value):
  119. file.write("%s: %s\n" % (key, value))
  120. write_field('Metadata-Version', str(version))
  121. write_field('Name', self.get_name())
  122. write_field('Version', self.get_version())
  123. summary = self.get_description()
  124. if summary:
  125. write_field('Summary', single_line(summary))
  126. optional_fields = (
  127. ('Home-page', 'url'),
  128. ('Download-URL', 'download_url'),
  129. ('Author', 'author'),
  130. ('Author-email', 'author_email'),
  131. ('Maintainer', 'maintainer'),
  132. ('Maintainer-email', 'maintainer_email'),
  133. )
  134. for field, attr in optional_fields:
  135. attr_val = getattr(self, attr, None)
  136. if attr_val is not None:
  137. write_field(field, attr_val)
  138. license = self.get_license()
  139. if license:
  140. write_field('License', rfc822_escape(license))
  141. for project_url in self.project_urls.items():
  142. write_field('Project-URL', '%s, %s' % project_url)
  143. keywords = ','.join(self.get_keywords())
  144. if keywords:
  145. write_field('Keywords', keywords)
  146. platforms = self.get_platforms() or []
  147. for platform in platforms:
  148. write_field('Platform', platform)
  149. self._write_list(file, 'Classifier', self.get_classifiers())
  150. # PEP 314
  151. self._write_list(file, 'Requires', self.get_requires())
  152. self._write_list(file, 'Provides', self.get_provides())
  153. self._write_list(file, 'Obsoletes', self.get_obsoletes())
  154. # Setuptools specific for PEP 345
  155. if hasattr(self, 'python_requires'):
  156. write_field('Requires-Python', self.python_requires)
  157. # PEP 566
  158. if self.long_description_content_type:
  159. write_field('Description-Content-Type', self.long_description_content_type)
  160. self._write_list(file, 'License-File', self.license_files or [])
  161. _write_requirements(self, file)
  162. long_description = self.get_long_description()
  163. if long_description:
  164. file.write("\n%s" % long_description)
  165. if not long_description.endswith("\n"):
  166. file.write("\n")
  167. def _write_requirements(self, file):
  168. for req in _reqs.parse(self.install_requires):
  169. file.write(f"Requires-Dist: {req}\n")
  170. processed_extras = {}
  171. for augmented_extra, reqs in self.extras_require.items():
  172. # Historically, setuptools allows "augmented extras": `<extra>:<condition>`
  173. unsafe_extra, _, condition = augmented_extra.partition(":")
  174. unsafe_extra = unsafe_extra.strip()
  175. extra = _normalization.safe_extra(unsafe_extra)
  176. if extra:
  177. _write_provides_extra(file, processed_extras, extra, unsafe_extra)
  178. for req in _reqs.parse_strings(reqs):
  179. r = _include_extra(req, extra, condition.strip())
  180. file.write(f"Requires-Dist: {r}\n")
  181. return processed_extras
  182. def _include_extra(req: str, extra: str, condition: str) -> Requirement:
  183. r = Requirement(req) # create a fresh object that can be modified
  184. parts = (
  185. f"({r.marker})" if r.marker else None,
  186. f"({condition})" if condition else None,
  187. f"extra == {extra!r}" if extra else None,
  188. )
  189. r.marker = Marker(" and ".join(x for x in parts if x))
  190. return r
  191. def _write_provides_extra(file, processed_extras, safe, unsafe):
  192. previous = processed_extras.get(safe)
  193. if previous == unsafe:
  194. SetuptoolsDeprecationWarning.emit(
  195. 'Ambiguity during "extra" normalization for dependencies.',
  196. f"""
  197. {previous!r} and {unsafe!r} normalize to the same value:\n
  198. {safe!r}\n
  199. In future versions, setuptools might halt the build process.
  200. """,
  201. see_url="https://peps.python.org/pep-0685/",
  202. )
  203. else:
  204. processed_extras[safe] = unsafe
  205. file.write(f"Provides-Extra: {safe}\n")