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

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, Union, 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 = Union[tuple[()], tuple[int, str]] 

32""" 

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

34""" 

35 

36NormalizedName = NewType("NormalizedName", str) 

37""" 

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

39""" 

40 

41 

42class InvalidName(ValueError): 

43 """ 

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

45 """ 

46 

47 

48class InvalidWheelFilename(ValueError): 

49 """ 

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

51 """ 

52 

53 

54class InvalidSdistFilename(ValueError): 

55 """ 

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

57 """ 

58 

59 

60# Core metadata spec for `Name` 

61_validate_regex = re.compile( 

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

63) 

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

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

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

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

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

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

70 

71 

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

73 """ 

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

75 normalized form of it. 

76 

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

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

79 before use. 

80 

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

82 distribution name before normalizing. 

83 

84 :param str name: The name to normalize. 

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

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

87 acceptable distribution name. 

88 

89 >>> from packaging.utils import canonicalize_name 

90 >>> canonicalize_name("Django") 

91 'django' 

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

93 'oslo-concurrency' 

94 >>> canonicalize_name("requests") 

95 'requests' 

96 

97 .. versionadded:: 16.2 

98 

99 .. versionchanged:: 20.4 

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

101 """ 

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

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

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

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

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

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

108 # Condense repeats (faster than regex) 

109 while "--" in value: 

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

111 return cast("NormalizedName", value) 

112 

113 

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

115 """ 

116 Check if a name is already normalized (i.e. :func:`canonicalize_name` would 

117 roundtrip to the same value). 

118 

119 :param str name: The name to check. 

120 

121 >>> from packaging.utils import is_normalized_name 

122 >>> is_normalized_name("requests") 

123 True 

124 >>> is_normalized_name("Django") 

125 False 

126 """ 

127 return _normalized_regex.fullmatch(name) is not None 

128 

129 

130def canonicalize_version( 

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

132) -> str: 

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

134 

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

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

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

138 the release segment. 

139 

140 >>> from packaging.utils import canonicalize_version 

141 >>> canonicalize_version('1.0.1') 

142 '1.0.1' 

143 

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

145 only by trailing zeros. 

146 

147 >>> canonicalize_version('1.0.0') 

148 '1' 

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

150 '1.0.0' 

151 

152 Invalid versions are returned unaltered. 

153 

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

155 'foo bar baz' 

156 

157 >>> canonicalize_version('1.4.0.0.0') 

158 '1.4' 

159 """ 

160 if isinstance(version, str): 

161 try: 

162 version = Version(version) 

163 except InvalidVersion: 

164 return str(version) 

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

166 

167 

168def parse_wheel_filename( 

169 filename: str, 

170 *, 

171 validate_order: bool = False, 

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

173 """ 

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

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

176 

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

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

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

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

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

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

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

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

185 string). 

186 

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

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

189 

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

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

192 are in sorted order. 

193 :raises InvalidWheelFilename: If the filename in question 

194 does not follow the :ref:`wheel specification 

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

196 

197 >>> from packaging.utils import parse_wheel_filename 

198 >>> from packaging.tags import Tag 

199 >>> from packaging.version import Version 

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

201 >>> name 

202 'foo' 

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

204 True 

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

206 True 

207 >>> not build 

208 True 

209 

210 .. versionadded:: 26.1 

211 The *validate_order* parameter. 

212 

213 .. versionchanged:: 26.3 

214 Raises :class:`InvalidWheelFilename` on empty tag set components or an 

215 empty project name. 

216 """ 

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

218 raise InvalidWheelFilename( 

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

220 ) 

221 

222 filename = filename[:-4] 

223 dashes = filename.count("-") 

224 if dashes not in (4, 5): 

225 raise InvalidWheelFilename( 

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

227 ) 

228 

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

230 name_part = parts[0] 

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

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

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

234 name = canonicalize_name(name_part) 

235 

236 try: 

237 version = Version(parts[1]) 

238 except InvalidVersion as e: 

239 raise InvalidWheelFilename( 

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

241 ) from e 

242 

243 if dashes == 5: 

244 build_part = parts[2] 

245 build_match = _build_tag_regex.match(build_part) 

246 if build_match is None: 

247 raise InvalidWheelFilename( 

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

249 ) 

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

251 else: 

252 build = () 

253 tag_str = parts[-1] 

254 try: 

255 tags = parse_tag(tag_str, validate_order=validate_order) 

256 except UnsortedTagsError: 

257 raise InvalidWheelFilename( 

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

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

260 ) from None 

261 except InvalidTag: 

262 raise InvalidWheelFilename( 

263 f"Invalid wheel filename (empty tag component): {filename!r}" 

264 ) from None 

265 return (name, version, build, tags) 

266 

267 

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

269 """ 

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

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

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

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

274 

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

276 :raises InvalidSdistFilename: If the filename does not end 

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

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

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

280 version. 

281 

282 >>> from packaging.utils import parse_sdist_filename 

283 >>> from packaging.version import Version 

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

285 >>> name 

286 'foo' 

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

288 True 

289 

290 .. versionchanged:: 26.3 

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

292 

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

294 """ 

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

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

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

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

299 else: 

300 raise InvalidSdistFilename( 

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

302 f" {filename!r}" 

303 ) 

304 

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

306 # so we split on the last dash. 

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

308 if not sep: 

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

310 if not name_part: 

311 raise InvalidSdistFilename( 

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

313 ) 

314 

315 name = canonicalize_name(name_part) 

316 

317 try: 

318 version = Version(version_part) 

319 except InvalidVersion as e: 

320 raise InvalidSdistFilename( 

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

322 ) from e 

323 

324 return (name, version)