Skip to content

Commit 0fbafa1

Browse files
authored
Merge branch 'main' into support-===foobar
2 parents a09de11 + 5037c93 commit 0fbafa1

File tree

3 files changed

+86
-72
lines changed

3 files changed

+86
-72
lines changed

src/packaging/specifiers.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ class Specifier(BaseSpecifier):
226226
"""
227227

228228
_regex = re.compile(
229-
r"^\s*" + _operator_regex_str + _version_regex_str + r"\s*$",
229+
r"\s*" + _operator_regex_str + _version_regex_str + r"\s*",
230230
re.VERBOSE | re.IGNORECASE,
231231
)
232232

@@ -254,7 +254,7 @@ def __init__(self, spec: str = "", prereleases: bool | None = None) -> None:
254254
:raises InvalidSpecifier:
255255
If the given specifier is invalid (i.e. bad syntax).
256256
"""
257-
match = self._regex.search(spec)
257+
match = self._regex.fullmatch(spec)
258258
if not match:
259259
raise InvalidSpecifier(f"Invalid specifier: {spec!r}")
260260

@@ -658,7 +658,7 @@ def filter(
658658
yield from prereleases_versions
659659

660660

661-
_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
661+
_prefix_regex = re.compile(r"([0-9]+)((?:a|b|c|rc)[0-9]+)")
662662

663663

664664
def _version_split(version: str) -> list[str]:
@@ -675,7 +675,7 @@ def _version_split(version: str) -> list[str]:
675675
result.append(epoch or "0")
676676

677677
for item in rest.split("."):
678-
match = _prefix_regex.search(item)
678+
match = _prefix_regex.fullmatch(item)
679679
if match:
680680
result.extend(match.groups())
681681
else:

src/packaging/utils.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from __future__ import annotations
66

7-
import functools
87
import re
98
from typing import NewType, Tuple, Union, cast
109

@@ -55,7 +54,6 @@ def is_normalized_name(name: str) -> bool:
5554
return _normalized_regex.match(name) is not None
5655

5756

58-
@functools.singledispatch
5957
def canonicalize_version(
6058
version: Version | str, *, strip_trailing_zero: bool = True
6159
) -> str:
@@ -78,17 +76,12 @@ def canonicalize_version(
7876
>>> canonicalize_version('foo bar baz')
7977
'foo bar baz'
8078
"""
81-
return str(_TrimmedRelease(str(version)) if strip_trailing_zero else version)
82-
83-
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)
79+
if isinstance(version, str):
80+
try:
81+
version = Version(version)
82+
except InvalidVersion:
83+
return str(version)
84+
return str(_TrimmedRelease(version) if strip_trailing_zero else version)
9285

9386

9487
def parse_wheel_filename(

src/packaging/version.py

Lines changed: 76 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from __future__ import annotations
1111

1212
import re
13-
from typing import Any, Callable, NamedTuple, SupportsInt, Tuple, Union
13+
import sys
14+
from typing import Any, Callable, SupportsInt, Tuple, Union
1415

1516
from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
1617

@@ -34,15 +35,6 @@
3435
VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
3536

3637

37-
class _Version(NamedTuple):
38-
epoch: int
39-
release: tuple[int, ...]
40-
dev: tuple[str, int] | None
41-
pre: tuple[str, int] | None
42-
post: tuple[str, int] | None
43-
local: LocalType | None
44-
45-
4638
def parse(version: str) -> Version:
4739
"""Parse the given version string.
4840
@@ -66,7 +58,9 @@ class InvalidVersion(ValueError):
6658

6759

6860
class _BaseVersion:
69-
_key: tuple[Any, ...]
61+
@property
62+
def _key(self) -> tuple[Any, ...]:
63+
raise NotImplementedError # pragma: no cover
7064

7165
def __hash__(self) -> int:
7266
return hash(self._key)
@@ -113,17 +107,19 @@ def __ne__(self, other: object) -> bool:
113107

114108
# Deliberately not anchored to the start and end of the string, to make it
115109
# easier for 3rd party code to reuse
110+
111+
# Note that ++ doesn't behave identically on CPython and PyPy, so not using it here
116112
_VERSION_PATTERN = r"""
117-
v? # optional leading v
113+
v?+ # optional leading v
118114
(?:
119-
(?:(?P<epoch>[0-9]+)!)? # epoch
120-
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
115+
(?:(?P<epoch>[0-9]+)!)?+ # epoch
116+
(?P<release>[0-9]+(?:\.[0-9]+)*+) # release segment
121117
(?P<pre> # pre-release
122-
[._-]?
118+
[._-]?+
123119
(?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
124-
[._-]?
120+
[._-]?+
125121
(?P<pre_n>[0-9]+)?
126-
)?
122+
)?+
127123
(?P<post> # post release
128124
(?:-(?P<post_n1>[0-9]+))
129125
|
@@ -133,23 +129,27 @@ def __ne__(self, other: object) -> bool:
133129
[._-]?
134130
(?P<post_n2>[0-9]+)?
135131
)
136-
)?
132+
)?+
137133
(?P<dev> # dev release
138-
[._-]?
134+
[._-]?+
139135
(?P<dev_l>dev)
140-
[._-]?
136+
[._-]?+
141137
(?P<dev_n>[0-9]+)?
142-
)?
138+
)?+
143139
)
144140
(?:\+
145141
(?P<local> # local version
146142
[a-z0-9]+
147-
(?:[._-][a-z0-9]+)*
143+
(?:[._-][a-z0-9]+)*+
148144
)
149-
)?
145+
)?+
150146
"""
151147

152-
VERSION_PATTERN = _VERSION_PATTERN
148+
_VERSION_PATTERN_OLD = _VERSION_PATTERN.replace("*+", "*").replace("?+", "?")
149+
150+
VERSION_PATTERN = (
151+
_VERSION_PATTERN_OLD if sys.version_info < (3, 11) else _VERSION_PATTERN
152+
)
153153
"""
154154
A string containing the regular expression used to match a valid version.
155155
@@ -186,9 +186,16 @@ class Version(_BaseVersion):
186186
True
187187
"""
188188

189+
_epoch: int
190+
_release: tuple[int, ...]
191+
_dev: tuple[str, int] | None
192+
_pre: tuple[str, int] | None
193+
_post: tuple[str, int] | None
194+
_local: LocalType | None
195+
196+
_key_cache: CmpKey | None
197+
189198
_regex = re.compile(r"\s*" + VERSION_PATTERN + r"\s*", re.VERBOSE | re.IGNORECASE)
190-
_version: _Version
191-
_key: CmpKey
192199

193200
def __init__(self, version: str) -> None:
194201
"""Initialize a Version object.
@@ -200,33 +207,34 @@ def __init__(self, version: str) -> None:
200207
If the ``version`` does not conform to PEP 440 in any way then this
201208
exception will be raised.
202209
"""
203-
204210
# Validate the version and parse it into pieces
205211
match = self._regex.fullmatch(version)
206212
if not match:
207213
raise InvalidVersion(f"Invalid version: {version!r}")
208-
209-
# Store the parsed out pieces of the version
210-
self._version = _Version(
211-
epoch=int(match.group("epoch")) if match.group("epoch") else 0,
212-
release=tuple(int(i) for i in match.group("release").split(".")),
213-
pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
214-
post=_parse_letter_version(
215-
match.group("post_l"), match.group("post_n1") or match.group("post_n2")
216-
),
217-
dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
218-
local=_parse_local_version(match.group("local")),
214+
self._epoch = int(match.group("epoch")) if match.group("epoch") else 0
215+
self._release = tuple(map(int, match.group("release").split(".")))
216+
self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n"))
217+
self._post = _parse_letter_version(
218+
match.group("post_l"), match.group("post_n1") or match.group("post_n2")
219219
)
220+
self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n"))
221+
self._local = _parse_local_version(match.group("local"))
220222

221-
# Generate a key which will be used for sorting
222-
self._key = _cmpkey(
223-
self._version.epoch,
224-
self._version.release,
225-
self._version.pre,
226-
self._version.post,
227-
self._version.dev,
228-
self._version.local,
229-
)
223+
# Key which will be used for sorting
224+
self._key_cache = None
225+
226+
@property
227+
def _key(self) -> CmpKey:
228+
if self._key_cache is None:
229+
self._key_cache = _cmpkey(
230+
self._epoch,
231+
self._release,
232+
self._pre,
233+
self._post,
234+
self._dev,
235+
self._local,
236+
)
237+
return self._key_cache
230238

231239
def __repr__(self) -> str:
232240
"""A representation of the Version that shows all internal state.
@@ -246,7 +254,7 @@ def __str__(self) -> str:
246254

247255
# Pre-release
248256
if self.pre is not None:
249-
parts.append("".join(str(x) for x in self.pre))
257+
parts.append("".join(map(str, self.pre)))
250258

251259
# Post-release
252260
if self.post is not None:
@@ -271,7 +279,7 @@ def epoch(self) -> int:
271279
>>> Version("1!2.0.0").epoch
272280
1
273281
"""
274-
return self._version.epoch
282+
return self._epoch
275283

276284
@property
277285
def release(self) -> tuple[int, ...]:
@@ -287,7 +295,7 @@ def release(self) -> tuple[int, ...]:
287295
Includes trailing zeroes but not the epoch or any pre-release / development /
288296
post-release suffixes.
289297
"""
290-
return self._version.release
298+
return self._release
291299

292300
@property
293301
def pre(self) -> tuple[str, int] | None:
@@ -302,7 +310,7 @@ def pre(self) -> tuple[str, int] | None:
302310
>>> Version("1.2.3rc1").pre
303311
('rc', 1)
304312
"""
305-
return self._version.pre
313+
return self._pre
306314

307315
@property
308316
def post(self) -> int | None:
@@ -313,7 +321,7 @@ def post(self) -> int | None:
313321
>>> Version("1.2.3.post1").post
314322
1
315323
"""
316-
return self._version.post[1] if self._version.post else None
324+
return self._post[1] if self._post else None
317325

318326
@property
319327
def dev(self) -> int | None:
@@ -324,7 +332,7 @@ def dev(self) -> int | None:
324332
>>> Version("1.2.3.dev1").dev
325333
1
326334
"""
327-
return self._version.dev[1] if self._version.dev else None
335+
return self._dev[1] if self._dev else None
328336

329337
@property
330338
def local(self) -> str | None:
@@ -335,8 +343,8 @@ def local(self) -> str | None:
335343
>>> Version("1.2.3+abc").local
336344
'abc'
337345
"""
338-
if self._version.local:
339-
return ".".join(str(x) for x in self._version.local)
346+
if self._local:
347+
return ".".join(str(x) for x in self._local)
340348
else:
341349
return None
342350

@@ -442,6 +450,18 @@ def micro(self) -> int:
442450

443451

444452
class _TrimmedRelease(Version):
453+
def __init__(self, version: str | Version) -> None:
454+
if isinstance(version, Version):
455+
self._epoch = version._epoch
456+
self._release = version._release
457+
self._dev = version._dev
458+
self._pre = version._pre
459+
self._post = version._post
460+
self._local = version._local
461+
self._key_cache = version._key_cache
462+
return
463+
super().__init__(version) # pragma: no cover
464+
445465
@property
446466
def release(self) -> tuple[int, ...]:
447467
"""
@@ -452,6 +472,7 @@ def release(self) -> tuple[int, ...]:
452472
>>> _TrimmedRelease('0.0').release
453473
(0,)
454474
"""
475+
# Unlike _strip_trailing_zeros, this leaves one 0.
455476
rel = super().release
456477
i = len(rel)
457478
while i > 1 and rel[i - 1] == 0:

0 commit comments

Comments
 (0)