Skip to content

Commit

Permalink
wip: pretty print single wildcard range
Browse files Browse the repository at this point in the history
  • Loading branch information
radoering committed Dec 22, 2022
1 parent f8bfdbc commit f07af0d
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 9 deletions.
49 changes: 49 additions & 0 deletions src/poetry/core/constraints/version/version_range.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from contextlib import suppress
from typing import TYPE_CHECKING

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

def _single_wildcard_range_string(self) -> str:
if not self.is_single_wildcard_range():
raise ValueError("Not a valid wildcard range")

assert self.max is not None
parts = list(self.max.parts)

# remove trailing zeros from max
while parts and parts[-1] == 0:
del parts[-1]

parts[-1] = parts[-1] - 1

return f"=={'.'.join(str(part) for part in parts)}.*"

def is_single_wildcard_range(self) -> bool:
# e.g.
# - "1.*" equals ">=1.0.dev0, <2" (equivalent to ">=1.0.dev0, <2.0.dev0")
# - "1.0.*" equals ">=1.0.dev0, <1.1"
# - "1.2.*" equals ">=1.2.dev0, <1.3"
if self.min is None or self.max is None:
return False
if not self.include_min or self.include_max:
return False
if not self.min.is_devrelease() or self.min.first_devrelease() != self.min:
return False
if (
self.max.is_postrelease()
or self.max.is_prerelease()
or self.max.is_local()
or (self.max.is_devrelease() and self.max.first_devrelease() != self.max)
):
return False
parts_min = list(self.min.parts)
parts_max = list(self.max.parts)

# remove trailing zeros from max
while parts_max and parts_max[-1] == 0:
del parts_max[-1]

if set(parts_min[len(parts_max) :]) not in [set(), {0}]:
return False
parts_min = parts_min[: len(parts_max)]
return parts_min[:-1] == parts_max[:-1] and parts_min[-1] + 1 == parts_max[-1]

def __eq__(self, other: object) -> bool:
if not isinstance(other, VersionRangeConstraint):
return False
Expand Down Expand Up @@ -398,6 +444,9 @@ def _compare_max(self, other: VersionRangeConstraint) -> int:
return 0

def __str__(self) -> str:
with suppress(ValueError):
return self._single_wildcard_range_string()

text = ""

if self.min is not None:
Expand Down
8 changes: 8 additions & 0 deletions src/poetry/core/version/pep440/segments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dataclasses

from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
from typing import cast
Expand Down Expand Up @@ -69,6 +70,13 @@ def from_parts(cls, *parts: int) -> Release:
extra=parts[3:],
)

def to_parts(self) -> Sequence[int]:
return tuple(
part
for part in [self.major, self.minor, self.patch, *self.extra]
if part is not None
)

def to_string(self) -> str:
return self.text

Expand Down
8 changes: 6 additions & 2 deletions src/poetry/core/version/pep440/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from typing import TYPE_CHECKING
from typing import Any
from typing import Sequence
from typing import TypeVar

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

@property
def non_semver_parts(self) -> tuple[int, ...]:
assert isinstance(self.release.extra, tuple)
def non_semver_parts(self) -> Sequence[int]:
return self.release.extra

@property
def parts(self) -> Sequence[int]:
return self.release.to_parts()

def to_string(self, short: bool = False) -> str:
if short:
import warnings
Expand Down
21 changes: 16 additions & 5 deletions tests/packages/test_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,21 @@ def test_complete_name() -> None:
["x"],
"A[x] (>=1.6.5,!=1.8.0,<3.1.0)",
),
# test single version range exclusions
# test single version range (wildcard)
("A", "==2.*", None, "A (==2.*)"),
("A", "==2.0.*", None, "A (==2.0.*)"),
("A", "==0.0.*", None, "A (==0.0.*)"),
("A", "==0.1.*", None, "A (==0.1.*)"),
("A", "==0.*", None, "A (<1.0.0)"),
("A", ">=1.0.dev0,<2", None, "A (==1.*)"),
("A", ">=1.0.dev1,<2", None, "A (>=1.0.dev1,<2)"),
("A", ">=1.1.dev0,<2", None, "A (>=1.1.dev0,<2)"),
("A", ">=1.0.dev0,<2.0.dev0", None, "A (==1.*)"),
("A", ">=1.0.dev0,<2.0.dev1", None, "A (>=1.0.dev0,<2.0.dev1)"),
("A", ">=1,<2", None, "A (>=1,<2)"),
("A", ">=1.0.dev0,<1.1", None, "A (==1.0.*)"),
("A", ">=1.0.0.0.dev0,<1.1.0.0.0", None, "A (==1.0.*)"),
# test single version range (wildcard) exclusions
("A", ">=1.8,!=2.0.*", None, "A (>=1.8,!=2.0.*)"),
("A", "!=0.0.*", None, "A (!=0.0.*)"),
("A", "!=0.1.*", None, "A (!=0.1.*)"),
Expand All @@ -221,10 +235,7 @@ def test_complete_name() -> None:
("A", ">=1.8,<2.0 || >=2.2.0", None, "A (>=1.8,<2.0 || >=2.2.0)"),
("A", ">=1.8,<2.0 || >=2.1.5", None, "A (>=1.8,<2.0 || >=2.1.5)"),
("A", ">=1.8.0.0,<2 || >=2.0.1.5", None, "A (>=1.8.0.0,<2 || >=2.0.1.5)"),
# non-semver version test is ignored due to existing bug in wildcard
# constraint parsing that ignores non-semver versions
# TODO: re-enable for verification once fixed
# ("A", ">=1.8.0.0,!=2.0.0.*", None, "A (>=1.8.0.0,!=2.0.0.*)"), # noqa: E800
("A", ">=1.8.0.0,!=2.0.0.*", None, "A (>=1.8.0.0,!=2.0.0.*)"),
],
)
def test_dependency_string_representation(
Expand Down
7 changes: 5 additions & 2 deletions tests/version/pep440/test_segments.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@ def test_release_post_init_minor_and_patch() -> None:
((1, 2, 3, 4, 5, 6), Release(1, 2, 3, (4, 5, 6))),
],
)
def test_release_from_parts(parts: tuple[int, ...], result: Release) -> None:
def test_release_from_parts_to_parts(parts: tuple[int, ...], result: Release) -> None:
assert Release.from_parts(*parts) == result
assert result.to_parts() == parts


@pytest.mark.parametrize("precision", list(range(1, 6)))
def test_release_precision(precision: int) -> None:
"""
Semantically identical releases might have a different precision, e.g. 1 vs. 1.0
"""
assert Release.from_parts(1, *[0] * (precision - 1)).precision == precision
release = Release.from_parts(1, *[0] * (precision - 1))
assert release.precision == precision
assert len(release.to_parts()) == precision


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

0 comments on commit f07af0d

Please sign in to comment.