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 8927c9bc4..b639c5763 100644 --- a/src/poetry/core/constraints/generic/multi_constraint.py +++ b/src/poetry/core/constraints/generic/multi_constraint.py @@ -1,5 +1,6 @@ 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 @@ -77,6 +78,26 @@ def intersect(self, other: BaseConstraint) -> BaseConstraint: 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] + + return MultiConstraint(*constraints) + def __eq__(self, other: object) -> bool: if not isinstance(other, MultiConstraint): return False diff --git a/src/poetry/core/constraints/generic/union_constraint.py b/src/poetry/core/constraints/generic/union_constraint.py index 439861edd..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 @@ -108,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 7dfc2a3a8..be9bff05f 100644 --- a/tests/constraints/generic/test_constraint.py +++ b/tests/constraints/generic/test_constraint.py @@ -217,6 +217,51 @@ def test_intersect( @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"), @@ -227,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")), @@ -254,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(