Skip to content

Commit

Permalink
Add constraint_regions (python-poetry#381)
Browse files Browse the repository at this point in the history
* Add constraint_regions

* unit test constraint_regions

* _ranges_for() and flatten are the same
  • Loading branch information
dimbleby authored and bostonrwalker committed Aug 29, 2022
1 parent 45523bb commit 8df7d87
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 17 deletions.
4 changes: 4 additions & 0 deletions src/poetry/core/semver/empty_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

if TYPE_CHECKING:
from poetry.core.semver.version import Version
from poetry.core.semver.version_range_constraint import VersionRangeConstraint


class EmptyConstraint(VersionConstraint):
Expand Down Expand Up @@ -37,5 +38,8 @@ def union(self, other: VersionConstraint) -> VersionConstraint:
def difference(self, other: VersionConstraint) -> EmptyConstraint:
return self

def flatten(self) -> list[VersionRangeConstraint]:
return []

def __str__(self) -> str:
return "<empty>"
58 changes: 58 additions & 0 deletions src/poetry/core/semver/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from poetry.core.semver.version_range import VersionRange


if TYPE_CHECKING:
from poetry.core.semver.version_constraint import VersionConstraint


def constraint_regions(constraints: list[VersionConstraint]) -> list[VersionRange]:
"""
Transform a list of VersionConstraints into a list of VersionRanges that mark out
the distinct regions of version-space.
eg input >=3.6 and >=2.7,<3.0.0 || >=3.4.0
output <2.7, >=2.7,<3.0.0, >=3.0.0,<3.4.0, >=3.4.0,<3.6, >=3.6.
"""
flattened = []
for constraint in constraints:
flattened += constraint.flatten()

mins = {
(constraint.min, not constraint.include_min)
for constraint in flattened
if constraint.min is not None
}
maxs = {
(constraint.max, constraint.include_max)
for constraint in flattened
if constraint.max is not None
}

edges = sorted(mins | maxs)
if not edges:
return [VersionRange(None, None)]

start = edges[0]
regions = [
VersionRange(None, start[0], include_max=start[1]),
]

for low, high in zip(edges, edges[1:]):
version_range = VersionRange(
low[0],
high[0],
include_min=not low[1],
include_max=high[1],
)
regions.append(version_range)

end = edges[-1]
regions.append(
VersionRange(end[0], None, include_min=not end[1]),
)

return regions
3 changes: 3 additions & 0 deletions src/poetry/core/semver/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ def difference(self, other: VersionConstraint) -> Version | EmptyConstraint:

return self

def flatten(self) -> list[VersionRangeConstraint]:
return [self]

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

Expand Down
5 changes: 5 additions & 0 deletions src/poetry/core/semver/version_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

if TYPE_CHECKING:
from poetry.core.semver.version import Version
from poetry.core.semver.version_range_constraint import VersionRangeConstraint


class VersionConstraint:
Expand Down Expand Up @@ -44,3 +45,7 @@ def union(self, other: VersionConstraint) -> VersionConstraint:
@abstractmethod
def difference(self, other: VersionConstraint) -> VersionConstraint:
raise NotImplementedError()

@abstractmethod
def flatten(self) -> list[VersionRangeConstraint]:
raise NotImplementedError()
3 changes: 3 additions & 0 deletions src/poetry/core/semver/version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,9 @@ def difference(self, other: VersionConstraint) -> VersionConstraint:

raise ValueError(f"Unknown VersionConstraint type {other}.")

def flatten(self) -> list[VersionRangeConstraint]:
return [self]

def __eq__(self, other: object) -> bool:
if not isinstance(other, VersionRangeConstraint):
return False
Expand Down
23 changes: 6 additions & 17 deletions src/poetry/core/semver/version_union.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def allows(self, version: Version) -> bool:

def allows_all(self, other: VersionConstraint) -> bool:
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
their_ranges = iter(other.flatten())

our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
Expand All @@ -106,7 +106,7 @@ def allows_all(self, other: VersionConstraint) -> bool:

def allows_any(self, other: VersionConstraint) -> bool:
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
their_ranges = iter(other.flatten())

our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
Expand All @@ -124,7 +124,7 @@ def allows_any(self, other: VersionConstraint) -> bool:

def intersect(self, other: VersionConstraint) -> VersionConstraint:
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
their_ranges = iter(other.flatten())
new_ranges = []

our_current_range = next(our_ranges, None)
Expand All @@ -148,7 +148,7 @@ def union(self, other: VersionConstraint) -> VersionConstraint:

def difference(self, other: VersionConstraint) -> VersionConstraint:
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
their_ranges = iter(other.flatten())
new_ranges: list[VersionConstraint] = []

state = {
Expand Down Expand Up @@ -230,19 +230,8 @@ def our_next_range(include_current: bool = True) -> bool:

return VersionUnion.of(*new_ranges)

def _ranges_for(
self, constraint: VersionConstraint
) -> list[VersionRangeConstraint]:
if constraint.is_empty():
return []

if isinstance(constraint, VersionUnion):
return constraint.ranges

if isinstance(constraint, VersionRangeConstraint):
return [constraint]

raise ValueError(f"Unknown VersionConstraint type {constraint}")
def flatten(self) -> list[VersionRangeConstraint]:
return self.ranges

def _exclude_single_wildcard_range_string(self) -> str:
"""
Expand Down
83 changes: 83 additions & 0 deletions tests/semver/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from poetry.core.semver.empty_constraint import EmptyConstraint
from poetry.core.semver.util import constraint_regions
from poetry.core.semver.version import Version
from poetry.core.semver.version_range import VersionRange


if TYPE_CHECKING:
from poetry.core.semver.version_constraint import VersionConstraint


PY27 = Version.parse("2.7")
PY30 = Version.parse("3")
PY36 = Version.parse("3.6.0")
PY37 = Version.parse("3.7")
PY38 = Version.parse("3.8.0")
PY40 = Version.parse("4.0.0")


@pytest.mark.parametrize(
"versions, expected",
[
([VersionRange(None, None)], [VersionRange(None, None)]),
([EmptyConstraint()], [VersionRange(None, None)]),
(
[VersionRange(PY27, None, include_min=True)],
[
VersionRange(None, PY27, include_max=False),
VersionRange(PY27, None, include_min=True),
],
),
(
[VersionRange(None, PY40, include_max=False)],
[
VersionRange(None, PY40, include_max=False),
VersionRange(PY40, None, include_min=True),
],
),
(
[VersionRange(PY27, PY27, include_min=True, include_max=True)],
[
VersionRange(None, PY27, include_max=False),
VersionRange(PY27, PY27, include_min=True, include_max=True),
VersionRange(PY27, None, include_min=False),
],
),
(
[VersionRange(PY27, PY30, include_min=True, include_max=False)],
[
VersionRange(None, PY27, include_max=False),
VersionRange(PY27, PY30, include_min=True, include_max=False),
VersionRange(PY30, None, include_min=True),
],
),
(
[
VersionRange(PY27, PY30, include_min=True, include_max=False).union(
VersionRange(PY37, PY40, include_min=False, include_max=True)
),
VersionRange(PY36, PY38, include_min=True, include_max=False),
],
[
VersionRange(None, PY27, include_max=False),
VersionRange(PY27, PY30, include_min=True, include_max=False),
VersionRange(PY30, PY36, include_min=True, include_max=False),
VersionRange(PY36, PY37, include_min=True, include_max=True),
VersionRange(PY37, PY38, include_min=False, include_max=False),
VersionRange(PY38, PY40, include_min=True, include_max=True),
VersionRange(PY40, None, include_min=False),
],
),
],
)
def test_constraint_regions(
versions: list[VersionConstraint], expected: list[VersionRange]
) -> None:
regions = constraint_regions(versions)
assert regions == expected

0 comments on commit 8df7d87

Please sign in to comment.