Skip to content

Commit dea1495

Browse files
committed
wip: pretty print single wildcard range
1 parent 5bfc9ad commit dea1495

File tree

5 files changed

+84
-9
lines changed

5 files changed

+84
-9
lines changed

src/poetry/core/constraints/version/version_range.py

+49
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from contextlib import suppress
34
from typing import TYPE_CHECKING
45

56
from poetry.core.constraints.version.empty_constraint import EmptyConstraint
@@ -336,6 +337,51 @@ def difference(self, other: VersionConstraint) -> VersionConstraint:
336337
def flatten(self) -> list[VersionRangeConstraint]:
337338
return [self]
338339

340+
def _single_wildcard_range_string(self) -> str:
341+
if not self.is_single_wildcard_range():
342+
raise ValueError("Not a valid wildcard range")
343+
344+
assert self.max is not None
345+
parts = list(self.max.parts)
346+
347+
# remove trailing zeros from max
348+
while parts and parts[-1] == 0:
349+
del parts[-1]
350+
351+
parts[-1] = parts[-1] - 1
352+
353+
return f"=={'.'.join(str(part) for part in parts)}.*"
354+
355+
def is_single_wildcard_range(self) -> bool:
356+
# e.g.
357+
# - "1.*" equals ">=1.0.dev0, <2" (equivalent to ">=1.0.dev0, <2.0.dev0")
358+
# - "1.0.*" equals ">=1.0.dev0, <1.1"
359+
# - "1.2.*" equals ">=1.2.dev0, <1.3"
360+
if self.min is None or self.max is None:
361+
return False
362+
if not self.include_min or self.include_max:
363+
return False
364+
if not self.min.is_devrelease() or self.min.first_devrelease() != self.min:
365+
return False
366+
if (
367+
self.max.is_postrelease()
368+
or self.max.is_prerelease()
369+
or self.max.is_local()
370+
or (self.max.is_devrelease() and self.max.first_devrelease() != self.max)
371+
):
372+
return False
373+
parts_min = list(self.min.parts)
374+
parts_max = list(self.max.parts)
375+
376+
# remove trailing zeros from max
377+
while parts_max and parts_max[-1] == 0:
378+
del parts_max[-1]
379+
380+
if set(parts_min[len(parts_max) :]) not in [set(), {0}]:
381+
return False
382+
parts_min = parts_min[: len(parts_max)]
383+
return parts_min[:-1] == parts_max[:-1] and parts_min[-1] + 1 == parts_max[-1]
384+
339385
def __eq__(self, other: object) -> bool:
340386
if not isinstance(other, VersionRangeConstraint):
341387
return False
@@ -398,6 +444,9 @@ def _compare_max(self, other: VersionRangeConstraint) -> int:
398444
return 0
399445

400446
def __str__(self) -> str:
447+
with suppress(ValueError):
448+
return self._single_wildcard_range_string()
449+
401450
text = ""
402451

403452
if self.min is not None:

src/poetry/core/version/pep440/segments.py

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dataclasses
44

55
from typing import Optional
6+
from typing import Sequence
67
from typing import Tuple
78
from typing import Union
89

@@ -68,6 +69,13 @@ def from_parts(cls, *parts: int) -> Release:
6869
extra=parts[3:],
6970
)
7071

72+
def to_parts(self) -> Sequence[int]:
73+
return tuple(
74+
part
75+
for part in [self.major, self.minor, self.patch, *self.extra]
76+
if part is not None
77+
)
78+
7179
def to_string(self) -> str:
7280
return self.text
7381

src/poetry/core/version/pep440/version.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from typing import TYPE_CHECKING
88
from typing import Any
9+
from typing import Sequence
910
from typing import TypeVar
1011

1112
from poetry.core.version.pep440.segments import RELEASE_PHASE_ID_ALPHA
@@ -139,10 +140,13 @@ def patch(self) -> int | None:
139140
return self.release.patch
140141

141142
@property
142-
def non_semver_parts(self) -> tuple[int, ...]:
143-
assert isinstance(self.release.extra, tuple)
143+
def non_semver_parts(self) -> Sequence[int]:
144144
return self.release.extra
145145

146+
@property
147+
def parts(self) -> Sequence[int]:
148+
return self.release.to_parts()
149+
146150
def to_string(self, short: bool = False) -> str:
147151
if short:
148152
import warnings

tests/packages/test_dependency.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,21 @@ def test_complete_name() -> None:
204204
["x"],
205205
"A[x] (>=1.6.5,!=1.8.0,<3.1.0)",
206206
),
207-
# test single version range exclusions
207+
# test single version range (wildcard)
208+
("A", "==2.*", None, "A (==2.*)"),
209+
("A", "==2.0.*", None, "A (==2.0.*)"),
210+
("A", "==0.0.*", None, "A (==0.0.*)"),
211+
("A", "==0.1.*", None, "A (==0.1.*)"),
212+
("A", "==0.*", None, "A (<1.0.0)"),
213+
("A", ">=1.0.dev0,<2", None, "A (==1.*)"),
214+
("A", ">=1.0.dev1,<2", None, "A (>=1.0.dev1,<2)"),
215+
("A", ">=1.1.dev0,<2", None, "A (>=1.1.dev0,<2)"),
216+
("A", ">=1.0.dev0,<2.0.dev0", None, "A (==1.*)"),
217+
("A", ">=1.0.dev0,<2.0.dev1", None, "A (>=1.0.dev0,<2.0.dev1)"),
218+
("A", ">=1,<2", None, "A (>=1,<2)"),
219+
("A", ">=1.0.dev0,<1.1", None, "A (==1.0.*)"),
220+
("A", ">=1.0.0.0.dev0,<1.1.0.0.0", None, "A (==1.0.*)"),
221+
# test single version range (wildcard) exclusions
208222
("A", ">=1.8,!=2.0.*", None, "A (>=1.8,!=2.0.*)"),
209223
("A", "!=0.0.*", None, "A (!=0.0.*)"),
210224
("A", "!=0.1.*", None, "A (!=0.1.*)"),
@@ -223,10 +237,7 @@ def test_complete_name() -> None:
223237
("A", ">=1.8,<2.0 || >=2.2.0", None, "A (>=1.8,<2.0 || >=2.2.0)"),
224238
("A", ">=1.8,<2.0 || >=2.1.5", None, "A (>=1.8,<2.0 || >=2.1.5)"),
225239
("A", ">=1.8.0.0,<2 || >=2.0.1.5", None, "A (>=1.8.0.0,<2 || >=2.0.1.5)"),
226-
# non-semver version test is ignored due to existing bug in wildcard
227-
# constraint parsing that ignores non-semver versions
228-
# TODO: re-enable for verification once fixed
229-
# ("A", ">=1.8.0.0,!=2.0.0.*", None, "A (>=1.8.0.0,!=2.0.0.*)"), # noqa: E800
240+
("A", ">=1.8.0.0,!=2.0.0.*", None, "A (>=1.8.0.0,!=2.0.0.*)"),
230241
],
231242
)
232243
def test_dependency_string_representation(

tests/version/pep440/test_segments.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,19 @@ def test_release_equal_zero_padding(precision1: int, precision2: int) -> None:
5151
((1, 2, 3, 4, 5, 6), Release(1, 2, 3, (4, 5, 6))),
5252
],
5353
)
54-
def test_release_from_parts(parts: tuple[int, ...], result: Release) -> None:
54+
def test_release_from_parts_to_parts(parts: tuple[int, ...], result: Release) -> None:
5555
assert Release.from_parts(*parts) == result
56+
assert result.to_parts() == parts
5657

5758

5859
@pytest.mark.parametrize("precision", list(range(1, 6)))
5960
def test_release_precision(precision: int) -> None:
6061
"""
6162
Semantically identical releases might have a different precision, e.g. 1 vs. 1.0
6263
"""
63-
assert Release.from_parts(1, *[0] * (precision - 1)).precision == precision
64+
release = Release.from_parts(1, *[0] * (precision - 1))
65+
assert release.precision == precision
66+
assert len(release.to_parts()) == precision
6467

6568

6669
@pytest.mark.parametrize("precision", list(range(1, 6)))

0 commit comments

Comments
 (0)