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

85 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 

7__lazy_modules__ = {f"{__spec__.parent}.tags", f"{__spec__.parent}.version"} 

8 

9import re 

10from typing import NewType, cast 

11 

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

13from .version import InvalidVersion, Version, _TrimmedRelease 

14 

15__all__ = [ 

16 "BuildTag", 

17 "InvalidName", 

18 "InvalidSdistFilename", 

19 "InvalidWheelFilename", 

20 "NormalizedName", 

21 "canonicalize_name", 

22 "canonicalize_version", 

23 "is_normalized_name", 

24 "parse_sdist_filename", 

25 "parse_wheel_filename", 

26] 

27 

28 

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

30 return __all__ 

31 

32 

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

34""" 

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

36 

37.. versionadded:: 20.9 

38""" 

39 

40NormalizedName = NewType("NormalizedName", str) 

41""" 

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

43 

44.. versionadded:: 20.4 

45""" 

46 

47 

48class InvalidName(ValueError): 

49 """ 

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

51 

52 .. versionadded:: 23.2 

53 """ 

54 

55 

56class InvalidWheelFilename(ValueError): 

57 """ 

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

59 

60 .. versionadded:: 20.9 

61 """ 

62 

63 

64class InvalidSdistFilename(ValueError): 

65 """ 

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

67 

68 .. versionadded:: 20.9 

69 """ 

70 

71 

72# Core metadata spec for `Name` 

73_validate_regex = re.compile( 

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

75) 

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

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

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

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

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

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

82 

83 

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

85 """ 

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

87 normalized form of it. 

88 

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

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

91 before use. 

92 

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

94 distribution name before normalizing. 

95 

96 :param str name: The name to normalize. 

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

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

99 acceptable distribution name. 

100 

101 >>> from packaging.utils import canonicalize_name 

102 >>> canonicalize_name("Django") 

103 'django' 

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

105 'oslo-concurrency' 

106 >>> canonicalize_name("requests") 

107 'requests' 

108 

109 .. versionadded:: 16.2 

110 

111 .. versionchanged:: 20.4 

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

113 

114 .. versionchanged:: 23.2 

115 Added the *validate* keyword parameter. 

116 """ 

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

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

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

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

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

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

123 # Condense repeats (faster than regex) 

124 while "--" in value: 

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

126 return cast("NormalizedName", value) 

127 

128 

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

130 """ 

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

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

133 

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

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

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

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

138 

139 :param str name: The name to check. 

140 

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

142 >>> is_normalized_name("requests") 

143 True 

144 >>> is_normalized_name("Django") 

145 False 

146 >>> canonicalize_name("_not_legal") 

147 '-not-legal' 

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

149 False 

150 

151 .. versionadded:: 23.2 

152 """ 

153 return _normalized_regex.fullmatch(name) is not None 

154 

155 

156def canonicalize_version( 

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

158) -> str: 

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

160 

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

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

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

164 the release segment. 

165 

166 >>> from packaging.utils import canonicalize_version 

167 >>> canonicalize_version('1.0.1') 

168 '1.0.1' 

169 

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

171 only by trailing zeros. 

172 

173 >>> canonicalize_version('1.0.0') 

174 '1' 

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

176 '1.0.0' 

177 

178 Invalid versions are returned unaltered. 

179 

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

181 'foo bar baz' 

182 

183 >>> canonicalize_version('1.4.0.0.0') 

184 '1.4' 

185 

186 .. versionadded:: 17.1 

187 

188 .. versionchanged:: 21.0 

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

190 

191 .. versionchanged:: 22.0 

192 Added the *strip_trailing_zero* keyword parameter. 

193 """ 

194 if isinstance(version, str): 

195 try: 

196 version = Version(version) 

197 except InvalidVersion: 

198 return str(version) 

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

200 

201 

202def parse_wheel_filename( 

203 filename: str, 

204 *, 

205 validate_order: bool = False, 

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

207 """ 

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

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

210 

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

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

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

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

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

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

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

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

219 string). 

220 

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

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

223 

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

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

226 are in sorted order. 

227 :raises InvalidWheelFilename: If the filename in question 

228 does not follow the :ref:`wheel specification 

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

230 

231 >>> from packaging.utils import parse_wheel_filename 

232 >>> from packaging.tags import Tag 

233 >>> from packaging.version import Version 

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

235 >>> name 

236 'foo' 

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

238 True 

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

240 True 

241 >>> not build 

242 True 

243 

244 .. versionadded:: 20.9 

245 

246 .. versionchanged:: 23.2 

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

248 

249 .. versionadded:: 26.1 

250 The *validate_order* parameter. 

251 

252 .. versionchanged:: 26.3 

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

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

255 empty. 

256 """ 

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

258 raise InvalidWheelFilename( 

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

260 ) 

261 

262 filename = filename[:-4] 

263 dashes = filename.count("-") 

264 if dashes not in (4, 5): 

265 raise InvalidWheelFilename( 

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

267 ) 

268 

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

270 name_part = parts[0] 

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

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

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

274 name = canonicalize_name(name_part) 

275 

276 try: 

277 version = Version(parts[1]) 

278 except InvalidVersion as e: 

279 raise InvalidWheelFilename( 

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

281 ) from e 

282 

283 if dashes == 5: 

284 build_part = parts[2] 

285 build_match = _build_tag_regex.match(build_part) 

286 if build_match is None: 

287 raise InvalidWheelFilename( 

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

289 ) 

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

291 else: 

292 build = () 

293 tag_str = parts[-1] 

294 try: 

295 tags = parse_tag(tag_str, validate_order=validate_order) 

296 except UnsortedTagsError: 

297 raise InvalidWheelFilename( 

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

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

300 ) from None 

301 except InvalidTag: 

302 raise InvalidWheelFilename( 

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

304 ) from None 

305 return (name, version, build, tags) 

306 

307 

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

309 """ 

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

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

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

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

314 

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

316 :raises InvalidSdistFilename: If the filename does not end 

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

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

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

320 version. 

321 

322 >>> from packaging.utils import parse_sdist_filename 

323 >>> from packaging.version import Version 

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

325 >>> name 

326 'foo' 

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

328 True 

329 

330 .. versionadded:: 20.9 

331 

332 .. versionchanged:: 21.0 

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

334 

335 .. versionchanged:: 23.2 

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

337 

338 .. versionchanged:: 26.3 

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

340 

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

342 """ 

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

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

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

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

347 else: 

348 raise InvalidSdistFilename( 

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

350 f" {filename!r}" 

351 ) 

352 

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

354 # so we split on the last dash. 

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

356 if not sep: 

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

358 if not name_part: 

359 raise InvalidSdistFilename( 

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

361 ) 

362 

363 name = canonicalize_name(name_part) 

364 

365 try: 

366 version = Version(version_part) 

367 except InvalidVersion as e: 

368 raise InvalidSdistFilename( 

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

370 ) from e 

371 

372 return (name, version)