Skip to content

Commit 680c31a

Browse files
jaracobrettcannon
andauthored
Refactor canonicalize_version (pypa#793)
* In canonicalize_version, re-use Version.__str__. * Utilize singledispatch to separate concerns in canonicalize_version. --------- Co-authored-by: Brett Cannon <brett@python.org>
1 parent c385b58 commit 680c31a

File tree

2 files changed

+42
-36
lines changed

2 files changed

+42
-36
lines changed

src/packaging/utils.py

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
from __future__ import annotations
66

7+
import functools
78
import re
89
from typing import NewType, Tuple, Union, cast
910

1011
from .tags import Tag, parse_tag
11-
from .version import InvalidVersion, Version
12+
from .version import InvalidVersion, Version, _TrimmedRelease
1213

1314
BuildTag = Union[Tuple[()], Tuple[int, str]]
1415
NormalizedName = NewType("NormalizedName", str)
@@ -54,52 +55,40 @@ def is_normalized_name(name: str) -> bool:
5455
return _normalized_regex.match(name) is not None
5556

5657

58+
@functools.singledispatch
5759
def canonicalize_version(
5860
version: Version | str, *, strip_trailing_zero: bool = True
5961
) -> str:
6062
"""
61-
This is very similar to Version.__str__, but has one subtle difference
62-
with the way it handles the release segment.
63-
"""
64-
if isinstance(version, str):
65-
try:
66-
parsed = Version(version)
67-
except InvalidVersion:
68-
# Legacy versions cannot be normalized
69-
return version
70-
else:
71-
parsed = version
72-
73-
parts = []
63+
Return a canonical form of a version as a string.
7464
75-
# Epoch
76-
if parsed.epoch != 0:
77-
parts.append(f"{parsed.epoch}!")
65+
>>> canonicalize_version('1.0.1')
66+
'1.0.1'
7867
79-
# Release segment
80-
release_segment = ".".join(str(x) for x in parsed.release)
81-
if strip_trailing_zero:
82-
# NB: This strips trailing '.0's to normalize
83-
release_segment = re.sub(r"(\.0)+$", "", release_segment)
84-
parts.append(release_segment)
68+
Per PEP 625, versions may have multiple canonical forms, differing
69+
only by trailing zeros.
8570
86-
# Pre-release
87-
if parsed.pre is not None:
88-
parts.append("".join(str(x) for x in parsed.pre))
71+
>>> canonicalize_version('1.0.0')
72+
'1'
73+
>>> canonicalize_version('1.0.0', strip_trailing_zero=False)
74+
'1.0.0'
8975
90-
# Post-release
91-
if parsed.post is not None:
92-
parts.append(f".post{parsed.post}")
76+
Invalid versions are returned unaltered.
9377
94-
# Development release
95-
if parsed.dev is not None:
96-
parts.append(f".dev{parsed.dev}")
78+
>>> canonicalize_version('foo bar baz')
79+
'foo bar baz'
80+
"""
81+
return str(_TrimmedRelease(str(version)) if strip_trailing_zero else version)
9782

98-
# Local version segment
99-
if parsed.local is not None:
100-
parts.append(f"+{parsed.local}")
10183

102-
return "".join(parts)
84+
@canonicalize_version.register
85+
def _(version: str, *, strip_trailing_zero: bool = True) -> str:
86+
try:
87+
parsed = Version(version)
88+
except InvalidVersion:
89+
# Legacy versions cannot be normalized
90+
return version
91+
return canonicalize_version(parsed, strip_trailing_zero=strip_trailing_zero)
10392

10493

10594
def parse_wheel_filename(

src/packaging/version.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,23 @@ def micro(self) -> int:
451451
return self.release[2] if len(self.release) >= 3 else 0
452452

453453

454+
class _TrimmedRelease(Version):
455+
@property
456+
def release(self) -> tuple[int, ...]:
457+
"""
458+
Release segment without any trailing zeros.
459+
460+
>>> _TrimmedRelease('1.0.0').release
461+
(1,)
462+
>>> _TrimmedRelease('0.0').release
463+
(0,)
464+
"""
465+
rel = super().release
466+
nonzeros = (index for index, val in enumerate(rel) if val)
467+
last_nonzero = max(nonzeros, default=0)
468+
return rel[: last_nonzero + 1]
469+
470+
454471
def _parse_letter_version(
455472
letter: str | None, number: str | bytes | SupportsInt | None
456473
) -> tuple[str, int] | None:

0 commit comments

Comments
 (0)