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
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
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.
5from __future__ import annotations
7__lazy_modules__ = {f"{__spec__.parent}.tags", f"{__spec__.parent}.version"}
9import re
10from typing import NewType, cast
12from .tags import InvalidTag, Tag, UnsortedTagsError, parse_tag
13from .version import InvalidVersion, Version, _TrimmedRelease
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]
29def __dir__() -> list[str]:
30 return __all__
33BuildTag = tuple[()] | tuple[int, str]
34"""
35A wheel build tag: an empty tuple, or a ``(build number, build tag suffix)`` pair.
37.. versionadded:: 20.9
38"""
40NormalizedName = NewType("NormalizedName", str)
41"""
42A :class:`typing.NewType` of :class:`str`, representing a normalized name.
44.. versionadded:: 20.4
45"""
48class InvalidName(ValueError):
49 """
50 An invalid distribution name; users should refer to the packaging user guide.
52 .. versionadded:: 23.2
53 """
56class InvalidWheelFilename(ValueError):
57 """
58 An invalid wheel filename was found, users should refer to PEP 427.
60 .. versionadded:: 20.9
61 """
64class InvalidSdistFilename(ValueError):
65 """
66 An invalid sdist filename was found, users should refer to the packaging user guide.
68 .. versionadded:: 20.9
69 """
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)
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.
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.
93 If **validate** is true, then the function will check if **name** is a valid
94 distribution name before normalizing.
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.
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'
109 .. versionadded:: 16.2
111 .. versionchanged:: 20.4
112 The return type was changed to :class:`NormalizedName`.
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)
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).
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.
139 :param str name: The name to check.
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
151 .. versionadded:: 23.2
152 """
153 return _normalized_regex.fullmatch(name) is not None
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.
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.
166 >>> from packaging.utils import canonicalize_version
167 >>> canonicalize_version('1.0.1')
168 '1.0.1'
170 Per PEP 625, versions may have multiple canonical forms, differing
171 only by trailing zeros.
173 >>> canonicalize_version('1.0.0')
174 '1'
175 >>> canonicalize_version('1.0.0', strip_trailing_zero=False)
176 '1.0.0'
178 Invalid versions are returned unaltered.
180 >>> canonicalize_version('foo bar baz')
181 'foo bar baz'
183 >>> canonicalize_version('1.4.0.0.0')
184 '1.4'
186 .. versionadded:: 17.1
188 .. versionchanged:: 21.0
189 The return type was narrowed to :class:`str`.
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)
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.
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).
221 If **validate_order** is true, compressed tag set components are
222 checked to be in sorted order as required by PEP 425.
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>`.
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
244 .. versionadded:: 20.9
246 .. versionchanged:: 23.2
247 Raises :class:`InvalidWheelFilename` when the version component is invalid.
249 .. versionadded:: 26.1
250 The *validate_order* parameter.
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 )
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 )
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)
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
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)
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`.
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.
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
330 .. versionadded:: 20.9
332 .. versionchanged:: 21.0
333 Added support for ``.zip`` source distributions.
335 .. versionchanged:: 23.2
336 Raises :class:`InvalidSdistFilename` when the version component is invalid.
338 .. versionchanged:: 26.3
339 Raises :class:`InvalidSdistFilename` on an empty project name.
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 )
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 )
363 name = canonicalize_name(name_part)
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
372 return (name, version)