From a4307dbf566bf109100779fb2a03e67271587017 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 28 Apr 2024 00:10:18 -0500 Subject: [PATCH] add support for operators `in`/`not in` to generic constraint (#722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a precondition to allow markers like `"tegra" in platform_release`. Attention: In contrast to other operators the value comes before the operator (and the marker name after the operator). Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com> --- .../core/constraints/generic/constraint.py | 105 ++++-- tests/constraints/generic/test_constraint.py | 304 ++++++++++++++++++ 2 files changed, 375 insertions(+), 34 deletions(-) diff --git a/src/poetry/core/constraints/generic/constraint.py b/src/poetry/core/constraints/generic/constraint.py index cbe8d42f0..0570e7ae5 100644 --- a/src/poetry/core/constraints/generic/constraint.py +++ b/src/poetry/core/constraints/generic/constraint.py @@ -2,7 +2,6 @@ import operator -from typing import Any from typing import Callable from typing import ClassVar @@ -11,20 +10,44 @@ from poetry.core.constraints.generic.empty_constraint import EmptyConstraint -OperatorType = Callable[[object, object], Any] +OperatorType = Callable[[object, object], bool] + + +def contains(a: object, b: object, /) -> bool: + return operator.contains(a, b) # type: ignore[arg-type] + + +def not_contains(a: object, b: object, /) -> bool: + return not contains(a, b) class Constraint(BaseConstraint): OP_EQ = operator.eq OP_NE = operator.ne + OP_IN = contains + OP_NC = not_contains _trans_op_str: ClassVar[dict[str, OperatorType]] = { "=": OP_EQ, "==": OP_EQ, "!=": OP_NE, + "in": OP_IN, + "not in": OP_NC, } - _trans_op_int: ClassVar[dict[OperatorType, str]] = {OP_EQ: "==", OP_NE: "!="} + _trans_op_int: ClassVar[dict[OperatorType, str]] = { + OP_EQ: "==", + OP_NE: "!=", + OP_IN: "in", + OP_NC: "not in", + } + + _trans_op_inv: ClassVar[dict[str, str]] = { + "!=": "==", + "==": "!=", + "not in": "in", + "in": "not in", + } def __init__(self, value: str, operator: str = "==") -> None: if operator == "=": @@ -49,14 +72,8 @@ def allows(self, other: BaseConstraint) -> bool: f' ("other" must be a constraint with operator "=="): {other}' ) - is_equal_op = self._operator == "==" - is_non_equal_op = self._operator == "!=" - - if is_equal_op: - return self._value == other.value - - if is_non_equal_op: - return self._value != other.value + if op := self._trans_op_str.get(self._operator): + return op(other.value, self._value) return False @@ -68,6 +85,15 @@ def allows_all(self, other: BaseConstraint) -> bool: if other.operator == "==": return self.allows(other) + if other.operator == "in" and self._operator == "in": + return self.value in other.value + + if other.operator == "not in": + if self._operator == "not in": + return other.value in self.value + if self._operator == "!=": + return self.value not in other.value + return self == other if isinstance(other, MultiConstraint): @@ -82,36 +108,36 @@ def allows_any(self, other: BaseConstraint) -> bool: from poetry.core.constraints.generic import MultiConstraint from poetry.core.constraints.generic import UnionConstraint - is_equal_op = self._operator == "==" - is_non_equal_op = self._operator == "!=" - - if is_equal_op: + if self._operator == "==": return other.allows(self) if isinstance(other, Constraint): - is_other_equal_op = other.operator == "==" - is_other_non_equal_op = other.operator == "!=" - - if is_other_equal_op: + if other.operator == "==": return self.allows(other) - if is_equal_op and is_other_non_equal_op: + if other.operator == "!=" and self._operator == "==": return self._value != other.value - return is_non_equal_op and is_other_non_equal_op + if other.operator == "not in" and self._operator == "in": + return other.value not in self.value + + if other.operator == "in" and self._operator == "not in": + return self.value not in other.value + + return True elif isinstance(other, MultiConstraint): - return is_non_equal_op + return self._operator == "!=" elif isinstance(other, UnionConstraint): - return is_non_equal_op and any( + return self._operator == "!=" and any( self.allows_any(c) for c in other.constraints ) return other.is_any() def invert(self) -> Constraint: - return Constraint(self._value, "!=" if self._operator == "==" else "==") + return Constraint(self._value, self._trans_op_inv[self.operator]) def difference(self, other: BaseConstraint) -> Constraint | EmptyConstraint: if other.allows(self): @@ -126,16 +152,16 @@ def intersect(self, other: BaseConstraint) -> BaseConstraint: if other == self: return self - if self.operator == "!=" and other.operator == "==" and self.allows(other): + if self.allows_all(other): return other - if other.operator == "!=" and self.operator == "==" and other.allows(self): + if other.allows_all(self): return self - if other.operator == "!=" and self.operator == "!=": - return MultiConstraint(self, other) + if not self.allows_any(other) or not other.allows_any(self): + return EmptyConstraint() - return EmptyConstraint() + return MultiConstraint(self, other) return other.intersect(self) @@ -146,16 +172,25 @@ def union(self, other: BaseConstraint) -> BaseConstraint: if other == self: return self - if self.operator == "!=" and other.operator == "==" and self.allows(other): + if self.allows_all(other): return self - if other.operator == "!=" and self.operator == "==" and other.allows(self): + if other.allows_all(self): return other - if other.operator == "==" and self.operator == "==": - return UnionConstraint(self, other) + ops = {self.operator, other.operator} + if ( + (ops in ({"!="}, {"not in"})) + or ( + ops in ({"in", "!="}, {"in", "not in"}) + and (self.operator == "in" and self.value in other.value) + or (other.operator == "in" and other.value in self.value) + ) + or self.invert() == other + ): + return AnyConstraint() - return AnyConstraint() + return UnionConstraint(self, other) # to preserve order (functionally not necessary) if isinstance(other, UnionConstraint): @@ -179,5 +214,7 @@ def __hash__(self) -> int: return hash((self._operator, self._value)) def __str__(self) -> str: + if self._operator in {"in", "not in"}: + return f"'{self._value}' {self._operator}" op = self._operator if self._operator != "==" else "" return f"{op}{self._value}" diff --git a/tests/constraints/generic/test_constraint.py b/tests/constraints/generic/test_constraint.py index 6a5a2173f..aa2b14881 100644 --- a/tests/constraints/generic/test_constraint.py +++ b/tests/constraints/generic/test_constraint.py @@ -22,12 +22,20 @@ (Constraint("win32"), Constraint("linux"), False), (Constraint("win32", "!="), Constraint("win32"), False), (Constraint("win32", "!="), Constraint("linux"), True), + (Constraint("tegra", "in"), Constraint("1.2-tegra"), True), + (Constraint("tegra", "in"), Constraint("1.2-teg"), False), + (Constraint("tegra", "not in"), Constraint("1.2-tegra"), False), + (Constraint("tegra", "not in"), Constraint("1.2-teg"), True), ], ) def test_allows( constraint1: Constraint, constraint2: Constraint, expected: bool ) -> None: assert constraint1.allows(constraint2) is expected + # allows_any() and allows_all() should be the same as allows() + # if the second constraint is a `==` constraint + assert constraint1.allows_any(constraint2) is expected + assert constraint1.allows_all(constraint2) is expected @pytest.mark.parametrize( @@ -117,6 +125,144 @@ def test_allows( True, False, ), + ( + Constraint("tegra", "not in"), + Constraint("tegra", "not in"), + True, + True, + ), + ( + Constraint("tegra", "in"), + Constraint("tegra", "not in"), + False, + False, + ), + ( + Constraint("tegra", "in"), + Constraint("tegra", "in"), + True, + True, + ), + ( + Constraint("tegra", "not in"), + Constraint("tegra", "in"), + False, + False, + ), + ( + Constraint("tegra", "in"), + Constraint("teg", "in"), + True, + False, + ), + ( + Constraint("teg", "in"), + Constraint("tegra", "in"), + True, + True, + ), + ( + Constraint("teg", "not in"), + Constraint("tegra", "not in"), + True, + False, + ), + ( + Constraint("tegra", "not in"), + Constraint("teg", "not in"), + True, + True, + ), + ( + Constraint("teg", "not in"), + Constraint("tegra", "in"), + False, + False, + ), + ( + Constraint("tegra", "in"), + Constraint("teg", "not in"), + False, + False, + ), + ( + Constraint("teg", "in"), + Constraint("tegra", "not in"), + True, + False, + ), + ( + Constraint("tegra", "not in"), + Constraint("teg", "in"), + True, + False, + ), + ( + Constraint("tegra", "in"), + Constraint("rpi", "in"), + True, + False, + ), + ( + Constraint("1.2.3-tegra", "!="), + Constraint("tegra", "in"), + True, + False, + ), + ( + Constraint("tegra", "in"), + Constraint("1.2.3-tegra", "!="), + True, + False, + ), + ( + Constraint("1.2.3-tegra", "!="), + Constraint("teg", "in"), + True, + False, + ), + ( + Constraint("teg", "in"), + Constraint("1.2.3-tegra", "!="), + True, + False, + ), + ( + Constraint("1.2.3-tegra", "!="), + Constraint("tegra", "not in"), + True, + True, + ), + ( + Constraint("tegra", "not in"), + Constraint("1.2.3-tegra", "!="), + True, + False, + ), + ( + Constraint("1.2.3-tegra", "!="), + Constraint("teg", "not in"), + True, + True, + ), + ( + Constraint("teg", "not in"), + Constraint("1.2.3-tegra", "!="), + True, + False, + ), + ( + Constraint("1.2.3-tegra", "=="), + Constraint("tegra", "in"), + True, + False, + ), + ( + Constraint("1.2.3-tegra", "=="), + Constraint("tegra", "not in"), + False, + False, + ), ], ) def test_allows_any_and_allows_all( @@ -138,6 +284,7 @@ def test_allows_any_and_allows_all( MultiConstraint(Constraint("foo", "!="), Constraint("bar", "!=")), UnionConstraint(Constraint("foo"), Constraint("bar")), ), + (Constraint("tegra", "not in"), Constraint("tegra", "in")), ], ) def test_invert(constraint: BaseConstraint, inverted: BaseConstraint) -> None: @@ -315,6 +462,89 @@ def test_invert(constraint: BaseConstraint, inverted: BaseConstraint) -> None: ), ), ), + ( + Constraint("tegra", "not in"), + Constraint("tegra", "not in"), + Constraint("tegra", "not in"), + ), + ( + Constraint("tegra", "in"), + Constraint("tegra", "not in"), + EmptyConstraint(), + ), + ( + Constraint("tegra", "in"), + Constraint("tegra", "in"), + Constraint("tegra", "in"), + ), + ( + Constraint("teg", "in"), + Constraint("tegra", "in"), + Constraint("tegra", "in"), + ), + ( + Constraint("teg", "not in"), + Constraint("tegra", "in"), + EmptyConstraint(), + ), + ( + Constraint("teg", "in"), + Constraint("tegra", "not in"), + ( + MultiConstraint(Constraint("teg", "in"), Constraint("tegra", "not in")), + MultiConstraint(Constraint("tegra", "not in"), Constraint("teg", "in")), + ), + ), + ( + Constraint("tegra", "in"), + Constraint("rpi", "in"), + ( + MultiConstraint(Constraint("tegra", "in"), Constraint("rpi", "in")), + MultiConstraint(Constraint("rpi", "in"), Constraint("tegra", "in")), + ), + ), + ( + Constraint("tegra", "in"), + Constraint("1.2.3-tegra", "=="), + Constraint("1.2.3-tegra", "=="), + ), + ( + Constraint("tegra", "in"), + Constraint("1.2.3-tegra", "!="), + ( + MultiConstraint( + Constraint("tegra", "in"), Constraint("1.2.3-tegra", "!=") + ), + MultiConstraint( + Constraint("1.2.3-tegra", "!="), + Constraint("tegra", "in"), + ), + ), + ), + ( + Constraint("tegra", "not in"), + Constraint("1.2.3-tegra", "=="), + EmptyConstraint(), + ), + ( + Constraint("tegra", "not in"), + Constraint("1.2.3-tegra", "!="), + Constraint("tegra", "not in"), + ), + ( + Constraint("tegra", "not in"), + Constraint("rpi", "not in"), + ( + MultiConstraint( + Constraint("tegra", "not in"), + Constraint("rpi", "not in"), + ), + MultiConstraint( + Constraint("rpi", "not in"), + Constraint("tegra", "not in"), + ), + ), + ), ], ) def test_intersect( @@ -517,6 +747,79 @@ def test_intersect( MultiConstraint(Constraint("win32", "!="), Constraint("darwin", "!=")), MultiConstraint(Constraint("win32", "!=")), ), + ( + Constraint("tegra", "not in"), + Constraint("tegra", "not in"), + Constraint("tegra", "not in"), + ), + ( + Constraint("tegra", "in"), + Constraint("tegra", "not in"), + AnyConstraint(), + ), + ( + Constraint("tegra", "in"), + Constraint("tegra", "in"), + Constraint("tegra", "in"), + ), + ( + Constraint("teg", "in"), + Constraint("tegra", "in"), + Constraint("teg", "in"), + ), + ( + Constraint("teg", "in"), + Constraint("tegra", "not in"), + AnyConstraint(), + ), + ( + Constraint("teg", "not in"), + Constraint("tegra", "in"), + ( + UnionConstraint(Constraint("teg", "not in"), Constraint("tegra", "in")), + UnionConstraint(Constraint("tegra", "in"), Constraint("teg", "not in")), + ), + ), + ( + Constraint("tegra", "in"), + Constraint("rpi", "in"), + ( + UnionConstraint(Constraint("tegra", "in"), Constraint("rpi", "in")), + UnionConstraint(Constraint("rpi", "in"), Constraint("tegra", "in")), + ), + ), + ( + Constraint("tegra", "not in"), + Constraint("rpi", "not in"), + AnyConstraint(), + ), + ( + Constraint("tegra", "in"), + Constraint("1.2.3-tegra", "!="), + AnyConstraint(), + ), + ( + Constraint("tegra", "not in"), + Constraint("1.2.3-tegra", "!="), + Constraint("1.2.3-tegra", "!="), + ), + ( + Constraint("tegra", "in"), + Constraint("1.2.3-tegra", "=="), + Constraint("tegra", "in"), + ), + ( + Constraint("tegra", "not in"), + Constraint("1.2.3-tegra", "=="), + ( + UnionConstraint( + Constraint("tegra", "not in"), Constraint("1.2.3-tegra", "==") + ), + UnionConstraint( + Constraint("1.2.3-tegra", "=="), Constraint("tegra", "not in") + ), + ), + ), ], ) def test_union( @@ -526,6 +829,7 @@ def test_union( ) -> None: if not isinstance(expected, tuple): expected = (expected, expected) + assert constraint1.union(constraint2) == expected[0] assert constraint2.union(constraint1) == expected[1]