Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/setuptools/_core_metadata.py: 18%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

162 statements  

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 

7from __future__ import annotations 

8 

9import os 

10import stat 

11import textwrap 

12from email import message_from_file 

13from email.message import Message 

14from tempfile import NamedTemporaryFile 

15 

16from packaging.markers import Marker 

17from packaging.requirements import Requirement 

18from packaging.utils import canonicalize_name, canonicalize_version 

19from packaging.version import Version 

20 

21from . import _normalization, _reqs 

22from .warnings import SetuptoolsDeprecationWarning 

23 

24from distutils.util import rfc822_escape 

25 

26 

27def get_metadata_version(self): 

28 mv = getattr(self, 'metadata_version', None) 

29 if mv is None: 

30 mv = Version('2.1') 

31 self.metadata_version = mv 

32 return mv 

33 

34 

35def rfc822_unescape(content: str) -> str: 

36 """Reverse RFC-822 escaping by removing leading whitespaces from content.""" 

37 lines = content.splitlines() 

38 if len(lines) == 1: 

39 return lines[0].lstrip() 

40 return '\n'.join((lines[0].lstrip(), textwrap.dedent('\n'.join(lines[1:])))) 

41 

42 

43def _read_field_from_msg(msg: Message, field: str) -> str | None: 

44 """Read Message header field.""" 

45 value = msg[field] 

46 if value == 'UNKNOWN': 

47 return None 

48 return value 

49 

50 

51def _read_field_unescaped_from_msg(msg: Message, field: str) -> str | None: 

52 """Read Message header field and apply rfc822_unescape.""" 

53 value = _read_field_from_msg(msg, field) 

54 if value is None: 

55 return value 

56 return rfc822_unescape(value) 

57 

58 

59def _read_list_from_msg(msg: Message, field: str) -> list[str] | None: 

60 """Read Message header field and return all results as list.""" 

61 values = msg.get_all(field, None) 

62 if values == []: 

63 return None 

64 return values 

65 

66 

67def _read_payload_from_msg(msg: Message) -> str | None: 

68 value = str(msg.get_payload()).strip() 

69 if value == 'UNKNOWN' or not value: 

70 return None 

71 return value 

72 

73 

74def read_pkg_file(self, file): 

75 """Reads the metadata values from a file object.""" 

76 msg = message_from_file(file) 

77 

78 self.metadata_version = Version(msg['metadata-version']) 

79 self.name = _read_field_from_msg(msg, 'name') 

80 self.version = _read_field_from_msg(msg, 'version') 

81 self.description = _read_field_from_msg(msg, 'summary') 

82 # we are filling author only. 

83 self.author = _read_field_from_msg(msg, 'author') 

84 self.maintainer = None 

85 self.author_email = _read_field_from_msg(msg, 'author-email') 

86 self.maintainer_email = None 

87 self.url = _read_field_from_msg(msg, 'home-page') 

88 self.download_url = _read_field_from_msg(msg, 'download-url') 

89 self.license = _read_field_unescaped_from_msg(msg, 'license') 

90 

91 self.long_description = _read_field_unescaped_from_msg(msg, 'description') 

92 if self.long_description is None and self.metadata_version >= Version('2.1'): 

93 self.long_description = _read_payload_from_msg(msg) 

94 self.description = _read_field_from_msg(msg, 'summary') 

95 

96 if 'keywords' in msg: 

97 self.keywords = _read_field_from_msg(msg, 'keywords').split(',') 

98 

99 self.platforms = _read_list_from_msg(msg, 'platform') 

100 self.classifiers = _read_list_from_msg(msg, 'classifier') 

101 

102 # PEP 314 - these fields only exist in 1.1 

103 if self.metadata_version == Version('1.1'): 

104 self.requires = _read_list_from_msg(msg, 'requires') 

105 self.provides = _read_list_from_msg(msg, 'provides') 

106 self.obsoletes = _read_list_from_msg(msg, 'obsoletes') 

107 else: 

108 self.requires = None 

109 self.provides = None 

110 self.obsoletes = None 

111 

112 self.license_files = _read_list_from_msg(msg, 'license-file') 

113 

114 

115def single_line(val): 

116 """ 

117 Quick and dirty validation for Summary pypa/setuptools#1390. 

118 """ 

119 if '\n' in val: 

120 # TODO: Replace with `raise ValueError("newlines not allowed")` 

121 # after reviewing #2893. 

122 msg = "newlines are not allowed in `summary` and will break in the future" 

123 SetuptoolsDeprecationWarning.emit("Invalid config.", msg) 

124 # due_date is undefined. Controversial change, there was a lot of push back. 

125 val = val.strip().split('\n')[0] 

126 return val 

127 

128 

129def write_pkg_info(self, base_dir): 

130 """Write the PKG-INFO file into the release tree.""" 

131 temp = "" 

132 final = os.path.join(base_dir, 'PKG-INFO') 

133 try: 

134 # Use a temporary file while writing to avoid race conditions 

135 # (e.g. `importlib.metadata` reading `.egg-info/PKG-INFO`): 

136 with NamedTemporaryFile("w", encoding="utf-8", dir=base_dir, delete=False) as f: 

137 temp = f.name 

138 self.write_pkg_file(f) 

139 permissions = stat.S_IMODE(os.lstat(temp).st_mode) 

140 os.chmod(temp, permissions | stat.S_IRGRP | stat.S_IROTH) 

141 os.replace(temp, final) # atomic operation. 

142 finally: 

143 if temp and os.path.exists(temp): 

144 os.remove(temp) 

145 

146 

147# Based on Python 3.5 version 

148def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME 

149 """Write the PKG-INFO format data to a file object.""" 

150 version = self.get_metadata_version() 

151 

152 def write_field(key, value): 

153 file.write("%s: %s\n" % (key, value)) 

154 

155 write_field('Metadata-Version', str(version)) 

156 write_field('Name', self.get_name()) 

157 write_field('Version', self.get_version()) 

158 

159 summary = self.get_description() 

160 if summary: 

161 write_field('Summary', single_line(summary)) 

162 

163 optional_fields = ( 

164 ('Home-page', 'url'), 

165 ('Download-URL', 'download_url'), 

166 ('Author', 'author'), 

167 ('Author-email', 'author_email'), 

168 ('Maintainer', 'maintainer'), 

169 ('Maintainer-email', 'maintainer_email'), 

170 ) 

171 

172 for field, attr in optional_fields: 

173 attr_val = getattr(self, attr, None) 

174 if attr_val is not None: 

175 write_field(field, attr_val) 

176 

177 license = self.get_license() 

178 if license: 

179 write_field('License', rfc822_escape(license)) 

180 

181 for project_url in self.project_urls.items(): 

182 write_field('Project-URL', '%s, %s' % project_url) 

183 

184 keywords = ','.join(self.get_keywords()) 

185 if keywords: 

186 write_field('Keywords', keywords) 

187 

188 platforms = self.get_platforms() or [] 

189 for platform in platforms: 

190 write_field('Platform', platform) 

191 

192 self._write_list(file, 'Classifier', self.get_classifiers()) 

193 

194 # PEP 314 

195 self._write_list(file, 'Requires', self.get_requires()) 

196 self._write_list(file, 'Provides', self.get_provides()) 

197 self._write_list(file, 'Obsoletes', self.get_obsoletes()) 

198 

199 # Setuptools specific for PEP 345 

200 if hasattr(self, 'python_requires'): 

201 write_field('Requires-Python', self.python_requires) 

202 

203 # PEP 566 

204 if self.long_description_content_type: 

205 write_field('Description-Content-Type', self.long_description_content_type) 

206 

207 self._write_list(file, 'License-File', self.license_files or []) 

208 _write_requirements(self, file) 

209 

210 long_description = self.get_long_description() 

211 if long_description: 

212 file.write("\n%s" % long_description) 

213 if not long_description.endswith("\n"): 

214 file.write("\n") 

215 

216 

217def _write_requirements(self, file): 

218 for req in _reqs.parse(self.install_requires): 

219 file.write(f"Requires-Dist: {req}\n") 

220 

221 processed_extras = {} 

222 for augmented_extra, reqs in self.extras_require.items(): 

223 # Historically, setuptools allows "augmented extras": `<extra>:<condition>` 

224 unsafe_extra, _, condition = augmented_extra.partition(":") 

225 unsafe_extra = unsafe_extra.strip() 

226 extra = _normalization.safe_extra(unsafe_extra) 

227 

228 if extra: 

229 _write_provides_extra(file, processed_extras, extra, unsafe_extra) 

230 for req in _reqs.parse_strings(reqs): 

231 r = _include_extra(req, extra, condition.strip()) 

232 file.write(f"Requires-Dist: {r}\n") 

233 

234 return processed_extras 

235 

236 

237def _include_extra(req: str, extra: str, condition: str) -> Requirement: 

238 r = Requirement(req) # create a fresh object that can be modified 

239 parts = ( 

240 f"({r.marker})" if r.marker else None, 

241 f"({condition})" if condition else None, 

242 f"extra == {extra!r}" if extra else None, 

243 ) 

244 r.marker = Marker(" and ".join(x for x in parts if x)) 

245 return r 

246 

247 

248def _write_provides_extra(file, processed_extras, safe, unsafe): 

249 previous = processed_extras.get(safe) 

250 if previous == unsafe: 

251 SetuptoolsDeprecationWarning.emit( 

252 'Ambiguity during "extra" normalization for dependencies.', 

253 f""" 

254 {previous!r} and {unsafe!r} normalize to the same value:\n 

255 {safe!r}\n 

256 In future versions, setuptools might halt the build process. 

257 """, 

258 see_url="https://peps.python.org/pep-0685/", 

259 ) 

260 else: 

261 processed_extras[safe] = unsafe 

262 file.write(f"Provides-Extra: {safe}\n") 

263 

264 

265# from pypa/distutils#244; needed only until that logic is always available 

266def get_fullname(self): 

267 return _distribution_fullname(self.get_name(), self.get_version()) 

268 

269 

270def _distribution_fullname(name: str, version: str) -> str: 

271 """ 

272 >>> _distribution_fullname('setup.tools', '1.0-2') 

273 'setup_tools-1.0.post2' 

274 >>> _distribution_fullname('setup-tools', '1.2post2') 

275 'setup_tools-1.2.post2' 

276 >>> _distribution_fullname('setup-tools', '1.0-r2') 

277 'setup_tools-1.0.post2' 

278 >>> _distribution_fullname('setup.tools', '1.0.post') 

279 'setup_tools-1.0.post0' 

280 >>> _distribution_fullname('setup.tools', '1.0+ubuntu-1') 

281 'setup_tools-1.0+ubuntu.1' 

282 """ 

283 return "{}-{}".format( 

284 canonicalize_name(name).replace('-', '_'), 

285 canonicalize_version(version, strip_trailing_zero=False), 

286 )