diff --git a/src/poetry/core/constraints/generic/constraint.py b/src/poetry/core/constraints/generic/constraint.py index 3180efb1a..2ee2411ec 100644 --- a/src/poetry/core/constraints/generic/constraint.py +++ b/src/poetry/core/constraints/generic/constraint.py @@ -1,6 +1,7 @@ from __future__ import annotations import operator +import warnings from poetry.core.constraints.generic.any_constraint import AnyConstraint from poetry.core.constraints.generic.base_constraint import BaseConstraint @@ -15,17 +16,27 @@ class Constraint(BaseConstraint): _trans_op_int = {OP_EQ: "==", OP_NE: "!="} - def __init__(self, version: str, operator: str = "==") -> None: + def __init__(self, value: str, operator: str = "==") -> None: if operator == "=": operator = "==" - self._version = version + self._value = value self._operator = operator self._op = self._trans_op_str[operator] + @property + def value(self) -> str: + return self._value + @property def version(self) -> str: - return self._version + warnings.warn( + "The property 'version' is deprecated and will be removed. " + "Please use the property 'value' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.value @property def operator(self) -> str: @@ -41,7 +52,7 @@ def allows(self, other: BaseConstraint) -> bool: is_other_non_equal_op = other.operator == "!=" if is_equal_op and is_other_equal_op: - return self._version == other.version + return self._value == other.value if ( is_equal_op @@ -51,7 +62,7 @@ def allows(self, other: BaseConstraint) -> bool: or is_non_equal_op and is_other_non_equal_op ): - return self._version != other.version + return self._value != other.value return False @@ -67,7 +78,7 @@ def allows_any(self, other: BaseConstraint) -> bool: is_other_non_equal_op = other.operator == "!=" if is_non_equal_op and is_other_non_equal_op: - return self._version != other.version + return self._value != other.value return other.allows(self) @@ -127,11 +138,11 @@ def __eq__(self, other: object) -> bool: if not isinstance(other, Constraint): return NotImplemented - return (self.version, self.operator) == (other.version, other.operator) + return (self.value, self.operator) == (other.value, other.operator) def __hash__(self) -> int: - return hash((self._operator, self._version)) + return hash((self._operator, self._value)) def __str__(self) -> str: op = self._operator if self._operator != "==" else "" - return f"{op}{self._version}" + return f"{op}{self._value}" diff --git a/src/poetry/core/constraints/generic/empty_constraint.py b/src/poetry/core/constraints/generic/empty_constraint.py index 83d0d148d..b20ad5c33 100644 --- a/src/poetry/core/constraints/generic/empty_constraint.py +++ b/src/poetry/core/constraints/generic/empty_constraint.py @@ -24,6 +24,9 @@ def allows_any(self, other: BaseConstraint) -> bool: def intersect(self, other: BaseConstraint) -> BaseConstraint: return self + def union(self, other: BaseConstraint) -> BaseConstraint: + return other + def difference(self, other: BaseConstraint) -> BaseConstraint: return self diff --git a/src/poetry/core/constraints/generic/multi_constraint.py b/src/poetry/core/constraints/generic/multi_constraint.py index 0a1f05f8e..b639c5763 100644 --- a/src/poetry/core/constraints/generic/multi_constraint.py +++ b/src/poetry/core/constraints/generic/multi_constraint.py @@ -1,5 +1,7 @@ from __future__ import annotations +from poetry.core.constraints.generic import AnyConstraint +from poetry.core.constraints.generic import EmptyConstraint from poetry.core.constraints.generic.base_constraint import BaseConstraint from poetry.core.constraints.generic.constraint import Constraint @@ -62,13 +64,34 @@ def allows_any(self, other: BaseConstraint) -> bool: def intersect(self, other: BaseConstraint) -> BaseConstraint: if not isinstance(other, Constraint): - raise ValueError("Unimplemented constraint intersection") + return other.intersect(self) - constraints = self._constraints - if other not in constraints: - constraints += (other,) - else: - constraints = (other,) + if other in self._constraints: + return self + + if other.value in (c.value for c in self._constraints): + # same value but different operator, e.g. '== "linux"' and '!= "linux"' + return EmptyConstraint() + + if other.operator == "==": + return other + + return MultiConstraint(*self._constraints, other) + + def union(self, other: BaseConstraint) -> BaseConstraint: + if not isinstance(other, Constraint): + return other.union(self) + + if other in self._constraints: + return other + + if other.value not in (c.value for c in self._constraints): + if other.operator == "!=": + return AnyConstraint() + + return self + + constraints = [c for c in self._constraints if c.value != other.value] if len(constraints) == 1: return constraints[0] diff --git a/src/poetry/core/constraints/generic/union_constraint.py b/src/poetry/core/constraints/generic/union_constraint.py index 8db1bd87c..a65f2a3ba 100644 --- a/src/poetry/core/constraints/generic/union_constraint.py +++ b/src/poetry/core/constraints/generic/union_constraint.py @@ -1,5 +1,6 @@ from __future__ import annotations +from poetry.core.constraints.generic import AnyConstraint from poetry.core.constraints.generic.base_constraint import BaseConstraint from poetry.core.constraints.generic.constraint import Constraint from poetry.core.constraints.generic.empty_constraint import EmptyConstraint @@ -71,21 +72,13 @@ def intersect(self, other: BaseConstraint) -> BaseConstraint: return other if isinstance(other, Constraint): - if self.allows(other): - return other + # (A or B) and C => (A and C) or (B and C) + # just a special case of UnionConstraint + other = UnionConstraint(other) - return EmptyConstraint() - - # Two remaining cases: an intersection with another union, or an intersection - # with a multi. - # - # In the first case: - # (A or B) and (C or D) => (A and C) or (A and D) or (B and C) or (B and D) - # - # In the second case: - # (A or B) and (C and D) => (A and C and D) or (B and C and D) new_constraints = [] if isinstance(other, UnionConstraint): + # (A or B) and (C or D) => (A and C) or (A and D) or (B and C) or (B and D) for our_constraint in self._constraints: for their_constraint in other.constraints: intersection = our_constraint.intersect(their_constraint) @@ -98,6 +91,7 @@ def intersect(self, other: BaseConstraint) -> BaseConstraint: else: assert isinstance(other, MultiConstraint) + # (A or B) and (C and D) => (A and C and D) or (B and C and D) for our_constraint in self._constraints: intersection = our_constraint @@ -115,15 +109,45 @@ def intersect(self, other: BaseConstraint) -> BaseConstraint: return UnionConstraint(*new_constraints) - def union(self, other: BaseConstraint) -> UnionConstraint: - if not isinstance(other, Constraint): - raise ValueError("Unimplemented constraint union") + def union(self, other: BaseConstraint) -> BaseConstraint: + if other.is_any(): + return other + + if other.is_empty(): + return self + + if isinstance(other, Constraint): + # (A or B) or C => A or B or C + # just a special case of UnionConstraint + other = UnionConstraint(other) + + new_constraints: list[BaseConstraint] = [] + if isinstance(other, UnionConstraint): + # (A or B) or (C or D) => A or B or C or D + for our_constraint in self._constraints: + for their_constraint in other.constraints: + union = our_constraint.union(their_constraint) + if union.is_any(): + return AnyConstraint() + if isinstance(union, Constraint): + if union not in new_constraints: + new_constraints.append(union) + else: + if our_constraint not in new_constraints: + new_constraints.append(our_constraint) + if their_constraint not in new_constraints: + new_constraints.append(their_constraint) + + else: + assert isinstance(other, MultiConstraint) + # (A or B) or (C and D) => nothing to do - constraints = self._constraints - if other not in self._constraints: - constraints += (other,) + new_constraints = [*self._constraints, other] - return UnionConstraint(*constraints) + if len(new_constraints) == 1: + return new_constraints[0] + + return UnionConstraint(*new_constraints) def __eq__(self, other: object) -> bool: if not isinstance(other, UnionConstraint): diff --git a/tests/constraints/generic/test_constraint.py b/tests/constraints/generic/test_constraint.py index 73a2a73c1..be9bff05f 100644 --- a/tests/constraints/generic/test_constraint.py +++ b/tests/constraints/generic/test_constraint.py @@ -55,6 +55,51 @@ def test_allows_all() -> None: @pytest.mark.parametrize( ("constraint1", "constraint2", "expected"), [ + ( + EmptyConstraint(), + Constraint("win32"), + EmptyConstraint(), + ), + ( + EmptyConstraint(), + MultiConstraint(Constraint("darwin", "!="), Constraint("linux", "!=")), + EmptyConstraint(), + ), + ( + EmptyConstraint(), + UnionConstraint(Constraint("win32"), Constraint("linux")), + EmptyConstraint(), + ), + ( + AnyConstraint(), + Constraint("win32"), + Constraint("win32"), + ), + ( + AnyConstraint(), + MultiConstraint(Constraint("darwin", "!="), Constraint("linux", "!=")), + MultiConstraint(Constraint("darwin", "!="), Constraint("linux", "!=")), + ), + ( + AnyConstraint(), + UnionConstraint(Constraint("win32"), Constraint("linux")), + UnionConstraint(Constraint("win32"), Constraint("linux")), + ), + ( + EmptyConstraint(), + AnyConstraint(), + EmptyConstraint(), + ), + ( + EmptyConstraint(), + EmptyConstraint(), + EmptyConstraint(), + ), + ( + AnyConstraint(), + AnyConstraint(), + AnyConstraint(), + ), ( Constraint("win32"), Constraint("win32"), @@ -65,6 +110,16 @@ def test_allows_all() -> None: Constraint("linux"), EmptyConstraint(), ), + ( + Constraint("win32"), + MultiConstraint(Constraint("darwin", "!="), Constraint("linux", "!=")), + Constraint("win32"), + ), + ( + Constraint("win32"), + MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), + EmptyConstraint(), + ), ( Constraint("win32"), UnionConstraint(Constraint("win32"), Constraint("linux")), @@ -90,16 +145,64 @@ def test_allows_all() -> None: Constraint("linux", "!="), MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), ), + ( + Constraint("win32", "!="), + MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), + MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), + ), + ( + Constraint("darwin", "!="), + MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), + MultiConstraint( + Constraint("win32", "!="), + Constraint("linux", "!="), + Constraint("darwin", "!="), + ), + ), + ( + Constraint("win32", "!="), + UnionConstraint(Constraint("win32"), Constraint("linux")), + Constraint("linux"), + ), + ( + Constraint("win32", "!="), + UnionConstraint( + Constraint("win32"), Constraint("linux"), Constraint("darwin") + ), + UnionConstraint(Constraint("linux"), Constraint("darwin")), + ), + ( + Constraint("win32", "!="), + UnionConstraint(Constraint("linux"), Constraint("linux2")), + UnionConstraint(Constraint("linux"), Constraint("linux2")), + ), ( UnionConstraint(Constraint("win32"), Constraint("linux")), UnionConstraint(Constraint("win32"), Constraint("darwin")), Constraint("win32"), ), + ( + UnionConstraint( + Constraint("win32"), Constraint("linux"), Constraint("darwin") + ), + UnionConstraint( + Constraint("win32"), Constraint("cygwin"), Constraint("darwin") + ), + UnionConstraint( + Constraint("win32"), + Constraint("darwin"), + ), + ), ( UnionConstraint(Constraint("win32"), Constraint("linux")), MultiConstraint(Constraint("win32", "!="), Constraint("darwin", "!=")), Constraint("linux"), ), + ( + UnionConstraint(Constraint("win32"), Constraint("linux")), + MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), + EmptyConstraint(), + ), ], ) def test_intersect( @@ -107,13 +210,58 @@ def test_intersect( constraint2: BaseConstraint, expected: BaseConstraint, ) -> None: - intersection = constraint1.intersect(constraint2) - assert intersection == expected + assert constraint1.intersect(constraint2) == expected + assert constraint2.intersect(constraint1) == expected @pytest.mark.parametrize( ("constraint1", "constraint2", "expected"), [ + ( + EmptyConstraint(), + Constraint("win32"), + Constraint("win32"), + ), + ( + EmptyConstraint(), + MultiConstraint(Constraint("darwin", "!="), Constraint("linux", "!=")), + MultiConstraint(Constraint("darwin", "!="), Constraint("linux", "!=")), + ), + ( + EmptyConstraint(), + UnionConstraint(Constraint("win32"), Constraint("linux")), + UnionConstraint(Constraint("win32"), Constraint("linux")), + ), + ( + AnyConstraint(), + Constraint("win32"), + AnyConstraint(), + ), + ( + AnyConstraint(), + MultiConstraint(Constraint("darwin", "!="), Constraint("linux", "!=")), + AnyConstraint(), + ), + ( + AnyConstraint(), + UnionConstraint(Constraint("win32"), Constraint("linux")), + AnyConstraint(), + ), + ( + EmptyConstraint(), + AnyConstraint(), + AnyConstraint(), + ), + ( + EmptyConstraint(), + EmptyConstraint(), + EmptyConstraint(), + ), + ( + AnyConstraint(), + AnyConstraint(), + AnyConstraint(), + ), ( Constraint("win32"), Constraint("win32"), @@ -124,6 +272,25 @@ def test_intersect( Constraint("linux"), UnionConstraint(Constraint("win32"), Constraint("linux")), ), + ( + Constraint("win32"), + MultiConstraint(Constraint("darwin", "!="), Constraint("linux", "!=")), + MultiConstraint(Constraint("darwin", "!="), Constraint("linux", "!=")), + ), + ( + Constraint("win32"), + MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), + Constraint("linux", "!="), + ), + ( + Constraint("win32"), + MultiConstraint( + Constraint("win32", "!="), + Constraint("linux", "!="), + Constraint("darwin", "!="), + ), + MultiConstraint(Constraint("linux", "!="), Constraint("darwin", "!=")), + ), ( Constraint("win32"), UnionConstraint(Constraint("win32"), Constraint("linux")), @@ -151,6 +318,65 @@ def test_intersect( Constraint("linux", "!="), AnyConstraint(), ), + ( + Constraint("win32", "!="), + MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), + Constraint("win32", "!="), + ), + ( + Constraint("darwin", "!="), + MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), + AnyConstraint(), + ), + ( + Constraint("win32", "!="), + UnionConstraint(Constraint("win32"), Constraint("linux")), + AnyConstraint(), + ), + ( + Constraint("win32", "!="), + UnionConstraint(Constraint("linux"), Constraint("linux2")), + Constraint("win32", "!="), + ), + ( + UnionConstraint(Constraint("win32"), Constraint("linux")), + UnionConstraint(Constraint("win32"), Constraint("darwin")), + UnionConstraint( + Constraint("win32"), Constraint("linux"), Constraint("darwin") + ), + ), + ( + UnionConstraint( + Constraint("win32"), Constraint("linux"), Constraint("darwin") + ), + UnionConstraint( + Constraint("win32"), Constraint("cygwin"), Constraint("darwin") + ), + UnionConstraint( + Constraint("win32"), + Constraint("linux"), + Constraint("darwin"), + Constraint("cygwin"), + ), + ), + ( + UnionConstraint(Constraint("win32"), Constraint("linux")), + MultiConstraint(Constraint("win32", "!="), Constraint("darwin", "!=")), + UnionConstraint( + Constraint("win32"), + Constraint("linux"), + MultiConstraint(Constraint("win32", "!="), Constraint("darwin", "!=")), + ), + ), + ( + UnionConstraint(Constraint("win32"), Constraint("linux")), + MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), + UnionConstraint( + Constraint("win32"), + Constraint("linux"), + MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")), + ), + ), ], ) def test_union( @@ -158,8 +384,8 @@ def test_union( constraint2: BaseConstraint, expected: BaseConstraint, ) -> None: - union = constraint1.union(constraint2) - assert union == expected + assert constraint1.union(constraint2) == expected + assert constraint2.union(constraint1) == expected def test_difference() -> None: diff --git a/tests/constraints/generic/test_multi_constraint.py b/tests/constraints/generic/test_multi_constraint.py index 583305d86..cbb6b679c 100644 --- a/tests/constraints/generic/test_multi_constraint.py +++ b/tests/constraints/generic/test_multi_constraint.py @@ -34,10 +34,3 @@ def test_allows_all() -> None: assert not c.allows_all( MultiConstraint(Constraint("win32", "!="), Constraint("darwin", "!=")) ) - - -def test_intersect() -> None: - c = MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")) - - intersection = c.intersect(Constraint("win32", "!=")) - assert intersection == Constraint("win32", "!=")