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

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

74 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, Tuple, Union, cast 

9 

10from .tags import Tag, 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 

33NormalizedName = NewType("NormalizedName", str) 

34""" 

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

36""" 

37 

38 

39class InvalidName(ValueError): 

40 """ 

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

42 """ 

43 

44 

45class InvalidWheelFilename(ValueError): 

46 """ 

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

48 """ 

49 

50 

51class InvalidSdistFilename(ValueError): 

52 """ 

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

54 """ 

55 

56 

57# Core metadata spec for `Name` 

58_validate_regex = re.compile( 

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

60) 

61_normalized_regex = re.compile(r"[a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9]", re.ASCII) 

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

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

64 

65 

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

67 """ 

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

69 normalized form of it. 

70 

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

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

73 before use. 

74 

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

76 distribution name before normalizing. 

77 

78 :param str name: The name to normalize. 

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

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

81 acceptable distribution name. 

82 

83 >>> from packaging.utils import canonicalize_name 

84 >>> canonicalize_name("Django") 

85 'django' 

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

87 'oslo-concurrency' 

88 >>> canonicalize_name("requests") 

89 'requests' 

90 """ 

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

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

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

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

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

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

97 # Condense repeats (faster than regex) 

98 while "--" in value: 

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

100 return cast("NormalizedName", value) 

101 

102 

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

104 """ 

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

106 roundtrip to the same value). 

107 

108 :param str name: The name to check. 

109 

110 >>> from packaging.utils import is_normalized_name 

111 >>> is_normalized_name("requests") 

112 True 

113 >>> is_normalized_name("Django") 

114 False 

115 """ 

116 return _normalized_regex.fullmatch(name) is not None 

117 

118 

119def canonicalize_version( 

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

121) -> str: 

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

123 

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

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

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

127 the release segment. 

128 

129 >>> from packaging.utils import canonicalize_version 

130 >>> canonicalize_version('1.0.1') 

131 '1.0.1' 

132 

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

134 only by trailing zeros. 

135 

136 >>> canonicalize_version('1.0.0') 

137 '1' 

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

139 '1.0.0' 

140 

141 Invalid versions are returned unaltered. 

142 

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

144 'foo bar baz' 

145 

146 >>> canonicalize_version('1.4.0.0.0') 

147 '1.4' 

148 """ 

149 if isinstance(version, str): 

150 try: 

151 version = Version(version) 

152 except InvalidVersion: 

153 return str(version) 

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

155 

156 

157def parse_wheel_filename( 

158 filename: str, 

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

160 """ 

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

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

163 

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

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

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

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

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

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

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

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

172 string). 

173 

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

175 :raises InvalidWheelFilename: If the filename in question 

176 does not follow the :ref:`wheel specification 

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

178 

179 >>> from packaging.utils import parse_wheel_filename 

180 >>> from packaging.tags import Tag 

181 >>> from packaging.version import Version 

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

183 >>> name 

184 'foo' 

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

186 True 

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

188 True 

189 >>> not build 

190 True 

191 """ 

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

193 raise InvalidWheelFilename( 

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

195 ) 

196 

197 filename = filename[:-4] 

198 dashes = filename.count("-") 

199 if dashes not in (4, 5): 

200 raise InvalidWheelFilename( 

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

202 ) 

203 

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

205 name_part = parts[0] 

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

207 if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: 

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

209 name = canonicalize_name(name_part) 

210 

211 try: 

212 version = Version(parts[1]) 

213 except InvalidVersion as e: 

214 raise InvalidWheelFilename( 

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

216 ) from e 

217 

218 if dashes == 5: 

219 build_part = parts[2] 

220 build_match = _build_tag_regex.match(build_part) 

221 if build_match is None: 

222 raise InvalidWheelFilename( 

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

224 ) 

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

226 else: 

227 build = () 

228 tags = parse_tag(parts[-1]) 

229 return (name, version, build, tags) 

230 

231 

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

233 """ 

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

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

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

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

238 

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

240 :raises InvalidSdistFilename: If the filename does not end 

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

242 contain a dash separating the name and the version of the distribution. 

243 

244 >>> from packaging.utils import parse_sdist_filename 

245 >>> from packaging.version import Version 

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

247 >>> name 

248 'foo' 

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

250 True 

251 

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

253 """ 

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

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

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

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

258 else: 

259 raise InvalidSdistFilename( 

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

261 f" {filename!r}" 

262 ) 

263 

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

265 # so we split on the last dash. 

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

267 if not sep: 

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

269 

270 name = canonicalize_name(name_part) 

271 

272 try: 

273 version = Version(version_part) 

274 except InvalidVersion as e: 

275 raise InvalidSdistFilename( 

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

277 ) from e 

278 

279 return (name, version)