Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/packaging/utils.py: 5%

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

84 statements  

1# This file is dual licensed under the terms of the Apache License, Version 

2# 2.0, and the BSD License. See the LICENSE file in the root of this repository 

3# for complete details. 

4 

5from __future__ import annotations 

6 

7import re 

8from typing import NewType, cast 

9 

10from .tags import InvalidTag, Tag, UnsortedTagsError, parse_tag 

11from .version import InvalidVersion, Version, _TrimmedRelease 

12 

13__all__ = [ 

14 "BuildTag", 

15 "InvalidName", 

16 "InvalidSdistFilename", 

17 "InvalidWheelFilename", 

18 "NormalizedName", 

19 "canonicalize_name", 

20 "canonicalize_version", 

21 "is_normalized_name", 

22 "parse_sdist_filename", 

23 "parse_wheel_filename", 

24] 

25 

26 

27def __dir__() -> list[str]: 

28 return __all__ 

29 

30 

31BuildTag = tuple[()] | tuple[int, str] 

32""" 

33A wheel build tag: an empty tuple, or a ``(build number, build tag suffix)`` pair. 

34 

35.. versionadded:: 20.9 

36""" 

37 

38NormalizedName = NewType("NormalizedName", str) 

39""" 

40A :class:`typing.NewType` of :class:`str`, representing a normalized name. 

41 

42.. versionadded:: 20.4 

43""" 

44 

45 

46class InvalidName(ValueError): 

47 """ 

48 An invalid distribution name; users should refer to the packaging user guide. 

49 

50 .. versionadded:: 23.2 

51 """ 

52 

53 

54class InvalidWheelFilename(ValueError): 

55 """ 

56 An invalid wheel filename was found, users should refer to PEP 427. 

57 

58 .. versionadded:: 20.9 

59 """ 

60 

61 

62class InvalidSdistFilename(ValueError): 

63 """ 

64 An invalid sdist filename was found, users should refer to the packaging user guide. 

65 

66 .. versionadded:: 20.9 

67 """ 

68 

69 

70# Core metadata spec for `Name` 

71_validate_regex = re.compile( 

72 r"[a-z0-9]|[a-z0-9][a-z0-9._-]*[a-z0-9]", re.IGNORECASE | re.ASCII 

73) 

74_normalized_regex = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*", re.ASCII) 

75# PEP 427: The build number must start with a digit. 

76_build_tag_regex = re.compile(r"(\d+)(.*)", re.ASCII) 

77# PEP 427: Valid characters for an escaped project name in a wheel filename. 

78# Requires at least one character so an empty project name is rejected. 

79_wheel_name_regex = re.compile(r"^[\w._]+\Z", re.UNICODE) 

80 

81 

82def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: 

83 """ 

84 This function takes a valid Python package or extra name, and returns the 

85 normalized form of it. 

86 

87 The return type is typed as :class:`NormalizedName`. This allows type 

88 checkers to help require that a string has passed through this function 

89 before use. 

90 

91 If **validate** is true, then the function will check if **name** is a valid 

92 distribution name before normalizing. 

93 

94 :param str name: The name to normalize. 

95 :param bool validate: Check whether the name is a valid distribution name. 

96 :raises InvalidName: If **validate** is true and the name is not an 

97 acceptable distribution name. 

98 

99 >>> from packaging.utils import canonicalize_name 

100 >>> canonicalize_name("Django") 

101 'django' 

102 >>> canonicalize_name("oslo.concurrency") 

103 'oslo-concurrency' 

104 >>> canonicalize_name("requests") 

105 'requests' 

106 

107 .. versionadded:: 16.2 

108 

109 .. versionchanged:: 20.4 

110 The return type was changed to :class:`NormalizedName`. 

111 

112 .. versionchanged:: 23.2 

113 Added the *validate* keyword parameter. 

114 """ 

115 if validate and not _validate_regex.fullmatch(name): 

116 raise InvalidName(f"name is invalid: {name!r}") 

117 # Ensure all ``.`` and ``_`` are ``-`` 

118 # Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503 

119 # Much faster than re, and even faster than str.translate 

120 value = name.lower().replace("_", "-").replace(".", "-") 

121 # Condense repeats (faster than regex) 

122 while "--" in value: 

123 value = value.replace("--", "-") 

124 return cast("NormalizedName", value) 

125 

126 

127def is_normalized_name(name: str) -> bool: 

128 """ 

129 Check if a name is a normalized project name (i.e. a valid name that 

130 :func:`canonicalize_name` would roundtrip to the same value). 

131 

132 The roundtrip only characterizes normalized names for *valid* names. A name 

133 must start and end with an ASCII letter or digit, which 

134 :func:`canonicalize_name` does not enforce: it leaves a leading or trailing 

135 hyphen in place, so such a name roundtrips without being normalized. 

136 

137 :param str name: The name to check. 

138 

139 >>> from packaging.utils import canonicalize_name, is_normalized_name 

140 >>> is_normalized_name("requests") 

141 True 

142 >>> is_normalized_name("Django") 

143 False 

144 >>> canonicalize_name("_not_legal") 

145 '-not-legal' 

146 >>> is_normalized_name("-not-legal") # roundtrips, but not a valid name 

147 False 

148 

149 .. versionadded:: 23.2 

150 """ 

151 return _normalized_regex.fullmatch(name) is not None 

152 

153 

154def canonicalize_version( 

155 version: Version | str, *, strip_trailing_zero: bool = True 

156) -> str: 

157 """Return a canonical form of a version as a string. 

158 

159 This function takes a string representing a package version (or a 

160 :class:`~packaging.version.Version` instance), and returns the 

161 normalized form of it. By default, it strips trailing zeros from 

162 the release segment. 

163 

164 >>> from packaging.utils import canonicalize_version 

165 >>> canonicalize_version('1.0.1') 

166 '1.0.1' 

167 

168 Per PEP 625, versions may have multiple canonical forms, differing 

169 only by trailing zeros. 

170 

171 >>> canonicalize_version('1.0.0') 

172 '1' 

173 >>> canonicalize_version('1.0.0', strip_trailing_zero=False) 

174 '1.0.0' 

175 

176 Invalid versions are returned unaltered. 

177 

178 >>> canonicalize_version('foo bar baz') 

179 'foo bar baz' 

180 

181 >>> canonicalize_version('1.4.0.0.0') 

182 '1.4' 

183 

184 .. versionadded:: 17.1 

185 

186 .. versionchanged:: 21.0 

187 The return type was narrowed to :class:`str`. 

188 

189 .. versionchanged:: 22.0 

190 Added the *strip_trailing_zero* keyword parameter. 

191 """ 

192 if isinstance(version, str): 

193 try: 

194 version = Version(version) 

195 except InvalidVersion: 

196 return str(version) 

197 return str(_TrimmedRelease(version) if strip_trailing_zero else version) 

198 

199 

200def parse_wheel_filename( 

201 filename: str, 

202 *, 

203 validate_order: bool = False, 

204) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]: 

205 """ 

206 This function takes the filename of a wheel file, and parses it, 

207 returning a tuple of name, version, build number, and tags. 

208 

209 The name part of the tuple is normalized and typed as 

210 :class:`NormalizedName`. The version portion is an instance of 

211 :class:`~packaging.version.Version`. The build number is ``()`` if 

212 there is no build number in the wheel filename, otherwise a 

213 two-item tuple of an integer for the leading digits and 

214 a string for the rest of the build number. The tags portion is a 

215 frozen set of :class:`~packaging.tags.Tag` instances (as the tag 

216 string format allows multiple tags to be combined into a single 

217 string). 

218 

219 If **validate_order** is true, compressed tag set components are 

220 checked to be in sorted order as required by PEP 425. 

221 

222 :param str filename: The name of the wheel file. 

223 :param bool validate_order: Check whether compressed tag set components 

224 are in sorted order. 

225 :raises InvalidWheelFilename: If the filename in question 

226 does not follow the :ref:`wheel specification 

227 <pypug:binary-distribution-format>`. 

228 

229 >>> from packaging.utils import parse_wheel_filename 

230 >>> from packaging.tags import Tag 

231 >>> from packaging.version import Version 

232 >>> name, ver, build, tags = parse_wheel_filename("foo-1.0-py3-none-any.whl") 

233 >>> name 

234 'foo' 

235 >>> ver == Version('1.0') 

236 True 

237 >>> tags == {Tag("py3", "none", "any")} 

238 True 

239 >>> not build 

240 True 

241 

242 .. versionadded:: 20.9 

243 

244 .. versionchanged:: 23.2 

245 Raises :class:`InvalidWheelFilename` when the version component is invalid. 

246 

247 .. versionadded:: 26.1 

248 The *validate_order* parameter. 

249 

250 .. versionchanged:: 26.3 

251 Raises :class:`InvalidWheelFilename` when an interpreter component is 

252 not an identifier, a tag set component is empty, or the project name is 

253 empty. 

254 """ 

255 if not filename.endswith(".whl"): 

256 raise InvalidWheelFilename( 

257 f"Invalid wheel filename (extension must be '.whl'): {filename!r}" 

258 ) 

259 

260 filename = filename[:-4] 

261 dashes = filename.count("-") 

262 if dashes not in (4, 5): 

263 raise InvalidWheelFilename( 

264 f"Invalid wheel filename (wrong number of parts): {filename!r}" 

265 ) 

266 

267 parts = filename.split("-", dashes - 2) 

268 name_part = parts[0] 

269 # See PEP 427 for the rules on escaping the project name. 

270 if "__" in name_part or _wheel_name_regex.match(name_part) is None: 

271 raise InvalidWheelFilename(f"Invalid project name: {filename!r}") 

272 name = canonicalize_name(name_part) 

273 

274 try: 

275 version = Version(parts[1]) 

276 except InvalidVersion as e: 

277 raise InvalidWheelFilename( 

278 f"Invalid wheel filename (invalid version): {filename!r}" 

279 ) from e 

280 

281 if dashes == 5: 

282 build_part = parts[2] 

283 build_match = _build_tag_regex.match(build_part) 

284 if build_match is None: 

285 raise InvalidWheelFilename( 

286 f"Invalid build number: {build_part} in {filename!r}" 

287 ) 

288 build = cast("BuildTag", (int(build_match.group(1)), build_match.group(2))) 

289 else: 

290 build = () 

291 tag_str = parts[-1] 

292 try: 

293 tags = parse_tag(tag_str, validate_order=validate_order) 

294 except UnsortedTagsError: 

295 raise InvalidWheelFilename( 

296 f"Invalid wheel filename (compressed tag set components must be in " 

297 f"sorted order per PEP 425): {filename!r}" 

298 ) from None 

299 except InvalidTag: 

300 raise InvalidWheelFilename( 

301 f"Invalid wheel filename (invalid tag component): {filename!r}" 

302 ) from None 

303 return (name, version, build, tags) 

304 

305 

306def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]: 

307 """ 

308 This function takes the filename of a sdist file (as specified 

309 in the `Source distribution format`_ documentation), and parses 

310 it, returning a tuple of the normalized name and version as 

311 represented by an instance of :class:`~packaging.version.Version`. 

312 

313 :param str filename: The name of the sdist file. 

314 :raises InvalidSdistFilename: If the filename does not end 

315 with an sdist extension (``.zip`` or ``.tar.gz``), if it does not 

316 contain a dash separating the name and the version of the distribution, 

317 if the project name is empty, or if the version portion is not a valid 

318 version. 

319 

320 >>> from packaging.utils import parse_sdist_filename 

321 >>> from packaging.version import Version 

322 >>> name, ver = parse_sdist_filename("foo-1.0.tar.gz") 

323 >>> name 

324 'foo' 

325 >>> ver == Version('1.0') 

326 True 

327 

328 .. versionadded:: 20.9 

329 

330 .. versionchanged:: 21.0 

331 Added support for ``.zip`` source distributions. 

332 

333 .. versionchanged:: 23.2 

334 Raises :class:`InvalidSdistFilename` when the version component is invalid. 

335 

336 .. versionchanged:: 26.3 

337 Raises :class:`InvalidSdistFilename` on an empty project name. 

338 

339 .. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name 

340 """ 

341 if filename.endswith(".tar.gz"): 

342 file_stem = filename[: -len(".tar.gz")] 

343 elif filename.endswith(".zip"): 

344 file_stem = filename[: -len(".zip")] 

345 else: 

346 raise InvalidSdistFilename( 

347 f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):" 

348 f" {filename!r}" 

349 ) 

350 

351 # We are requiring a PEP 440 version, which cannot contain dashes, 

352 # so we split on the last dash. 

353 name_part, sep, version_part = file_stem.rpartition("-") 

354 if not sep: 

355 raise InvalidSdistFilename(f"Invalid sdist filename: {filename!r}") 

356 if not name_part: 

357 raise InvalidSdistFilename( 

358 f"Invalid sdist filename (empty project name): {filename!r}" 

359 ) 

360 

361 name = canonicalize_name(name_part) 

362 

363 try: 

364 version = Version(version_part) 

365 except InvalidVersion as e: 

366 raise InvalidSdistFilename( 

367 f"Invalid sdist filename (invalid version): {filename!r}" 

368 ) from e 

369 

370 return (name, version)