codekingpro/portable-devtools
114k
1"""2Handling of Core Metadata for Python packages (including reading and writing).3 4See: https://packaging.python.org/en/latest/specifications/core-metadata/5"""6 7import os8import stat9import textwrap10from email import message_from_file11from email.message import Message12from tempfile import NamedTemporaryFile13from typing import Optional, List14 15from distutils.util import rfc822_escape16 17from . import _normalization, _reqs18from .extern.packaging.markers import Marker19from .extern.packaging.requirements import Requirement20from .extern.packaging.utils import canonicalize_name21from .extern.packaging.version import Version22from .warnings import SetuptoolsDeprecationWarning23 24 25def get_metadata_version(self):26 mv = getattr(self, 'metadata_version', None)27 if mv is None:28 mv = Version('2.1')29 self.metadata_version = mv30 return mv31 32 33def rfc822_unescape(content: str) -> str:34 """Reverse RFC-822 escaping by removing leading whitespaces from content."""35 lines = content.splitlines()36 if len(lines) == 1:37 return lines[0].lstrip()38 return '\n'.join((lines[0].lstrip(), textwrap.dedent('\n'.join(lines[1:]))))39 40 41def _read_field_from_msg(msg: Message, field: str) -> Optional[str]:42 """Read Message header field."""43 value = msg[field]44 if value == 'UNKNOWN':45 return None46 return value47 48 49def _read_field_unescaped_from_msg(msg: Message, field: str) -> Optional[str]:50 """Read Message header field and apply rfc822_unescape."""51 value = _read_field_from_msg(msg, field)52 if value is None:53 return value54 return rfc822_unescape(value)55 56 57def _read_list_from_msg(msg: Message, field: str) -> Optional[List[str]]:58 """Read Message header field and return all results as list."""59 values = msg.get_all(field, None)60 if values == []:61 return None62 return values63 64 65def _read_payload_from_msg(msg: Message) -> Optional[str]:66 value = str(msg.get_payload()).strip()67 if value == 'UNKNOWN' or not value:68 return None69 return value70 71 72def read_pkg_file(self, file):73 """Reads the metadata values from a file object."""74 msg = message_from_file(file)75 76 self.metadata_version = Version(msg['metadata-version'])77 self.name = _read_field_from_msg(msg, 'name')78 self.version = _read_field_from_msg(msg, 'version')79 self.description = _read_field_from_msg(msg, 'summary')80 # we are filling author only.81 self.author = _read_field_from_msg(msg, 'author')82 self.maintainer = None83 self.author_email = _read_field_from_msg(msg, 'author-email')84 self.maintainer_email = None85 self.url = _read_field_from_msg(msg, 'home-page')86 self.download_url = _read_field_from_msg(msg, 'download-url')87 self.license = _read_field_unescaped_from_msg(msg, 'license')88 89 self.long_description = _read_field_unescaped_from_msg(msg, 'description')90 if self.long_description is None and self.metadata_version >= Version('2.1'):91 self.long_description = _read_payload_from_msg(msg)92 self.description = _read_field_from_msg(msg, 'summary')93 94 if 'keywords' in msg:95 self.keywords = _read_field_from_msg(msg, 'keywords').split(',')96 97 self.platforms = _read_list_from_msg(msg, 'platform')98 self.classifiers = _read_list_from_msg(msg, 'classifier')99 100 # PEP 314 - these fields only exist in 1.1101 if self.metadata_version == Version('1.1'):102 self.requires = _read_list_from_msg(msg, 'requires')103 self.provides = _read_list_from_msg(msg, 'provides')104 self.obsoletes = _read_list_from_msg(msg, 'obsoletes')105 else:106 self.requires = None107 self.provides = None108 self.obsoletes = None109 110 self.license_files = _read_list_from_msg(msg, 'license-file')111 112 113def single_line(val):114 """115 Quick and dirty validation for Summary pypa/setuptools#1390.116 """117 if '\n' in val:118 # TODO: Replace with `raise ValueError("newlines not allowed")`119 # after reviewing #2893.120 msg = "newlines are not allowed in `summary` and will break in the future"121 SetuptoolsDeprecationWarning.emit("Invalid config.", msg)122 # due_date is undefined. Controversial change, there was a lot of push back.123 val = val.strip().split('\n')[0]124 return val125 126 127def write_pkg_info(self, base_dir):128 """Write the PKG-INFO file into the release tree."""129 temp = ""130 final = os.path.join(base_dir, 'PKG-INFO')131 try:132 # Use a temporary file while writing to avoid race conditions133 # (e.g. `importlib.metadata` reading `.egg-info/PKG-INFO`):134 with NamedTemporaryFile("w", encoding="utf-8", dir=base_dir, delete=False) as f:135 temp = f.name136 self.write_pkg_file(f)137 permissions = stat.S_IMODE(os.lstat(temp).st_mode)138 os.chmod(temp, permissions | stat.S_IRGRP | stat.S_IROTH)139 os.replace(temp, final) # atomic operation.140 finally:141 if temp and os.path.exists(temp):142 os.remove(temp)143 144 145# Based on Python 3.5 version146def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME147 """Write the PKG-INFO format data to a file object."""148 version = self.get_metadata_version()149 150 def write_field(key, value):151 file.write("%s: %s\n" % (key, value))152 153 write_field('Metadata-Version', str(version))154 write_field('Name', self.get_name())155 write_field('Version', self.get_version())156 157 summary = self.get_description()158 if summary:159 write_field('Summary', single_line(summary))160 161 optional_fields = (162 ('Home-page', 'url'),163 ('Download-URL', 'download_url'),164 ('Author', 'author'),165 ('Author-email', 'author_email'),166 ('Maintainer', 'maintainer'),167 ('Maintainer-email', 'maintainer_email'),168 )169 170 for field, attr in optional_fields:171 attr_val = getattr(self, attr, None)172 if attr_val is not None:173 write_field(field, attr_val)174 175 license = self.get_license()176 if license:177 write_field('License', rfc822_escape(license))178 179 for project_url in self.project_urls.items():180 write_field('Project-URL', '%s, %s' % project_url)181 182 keywords = ','.join(self.get_keywords())183 if keywords:184 write_field('Keywords', keywords)185 186 platforms = self.get_platforms() or []187 for platform in platforms:188 write_field('Platform', platform)189 190 self._write_list(file, 'Classifier', self.get_classifiers())191 192 # PEP 314193 self._write_list(file, 'Requires', self.get_requires())194 self._write_list(file, 'Provides', self.get_provides())195 self._write_list(file, 'Obsoletes', self.get_obsoletes())196 197 # Setuptools specific for PEP 345198 if hasattr(self, 'python_requires'):199 write_field('Requires-Python', self.python_requires)200 201 # PEP 566202 if self.long_description_content_type:203 write_field('Description-Content-Type', self.long_description_content_type)204 205 self._write_list(file, 'License-File', self.license_files or [])206 _write_requirements(self, file)207 208 long_description = self.get_long_description()209 if long_description:210 file.write("\n%s" % long_description)211 if not long_description.endswith("\n"):212 file.write("\n")213 214 215def _write_requirements(self, file):216 for req in _reqs.parse(self.install_requires):217 file.write(f"Requires-Dist: {req}\n")218 219 processed_extras = {}220 for augmented_extra, reqs in self.extras_require.items():221 # Historically, setuptools allows "augmented extras": `<extra>:<condition>`222 unsafe_extra, _, condition = augmented_extra.partition(":")223 unsafe_extra = unsafe_extra.strip()224 extra = _normalization.safe_extra(unsafe_extra)225 226 if extra:227 _write_provides_extra(file, processed_extras, extra, unsafe_extra)228 for req in _reqs.parse_strings(reqs):229 r = _include_extra(req, extra, condition.strip())230 file.write(f"Requires-Dist: {r}\n")231 232 return processed_extras233 234 235def _include_extra(req: str, extra: str, condition: str) -> Requirement:236 r = Requirement(req) # create a fresh object that can be modified237 parts = (238 f"({r.marker})" if r.marker else None,239 f"({condition})" if condition else None,240 f"extra == {extra!r}" if extra else None,241 )242 r.marker = Marker(" and ".join(x for x in parts if x))243 return r244 245 246def _write_provides_extra(file, processed_extras, safe, unsafe):247 previous = processed_extras.get(safe)248 if previous == unsafe:249 SetuptoolsDeprecationWarning.emit(250 'Ambiguity during "extra" normalization for dependencies.',251 f"""252 {previous!r} and {unsafe!r} normalize to the same value:\n253 {safe!r}\n254 In future versions, setuptools might halt the build process.255 """,256 see_url="https://peps.python.org/pep-0685/",257 )258 else:259 processed_extras[safe] = unsafe260 file.write(f"Provides-Extra: {safe}\n")261 262 263# from pypa/distutils#244; needed only until that logic is always available264def get_fullname(self):265 return "{}-{}".format(266 canonicalize_name(self.get_name()).replace('-', '_'),267 self.get_version(),268 )269 