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/src/poetry/core/constraints/generic/parser.py b/src/poetry/core/constraints/generic/parser.py index 9b5185385..ceb7d5e7c 100644 --- a/src/poetry/core/constraints/generic/parser.py +++ b/src/poetry/core/constraints/generic/parser.py @@ -16,6 +16,15 @@ BASIC_CONSTRAINT = re.compile(r"^(!?==?)?\s*([^\s]+?)\s*$") +STR_CMP_CONSTRAINT = re.compile( + r"""(?ix)^ # case insensitive and verbose mode + (?P['"]) # Single or double quotes + (?P.+?) # The value itself inside quotes + \1 # Closing single of double quote + \s* # Space + (?P(not\sin|in)) # Literal match of 'in' or 'not in' + $""" +) @functools.lru_cache(maxsize=None) @@ -26,9 +35,7 @@ def parse_constraint(constraints: str) -> BaseConstraint: or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip()) or_groups = [] for constraints in or_constraints: - and_constraints = re.split( - r"(?< ,]) *(? 1: @@ -53,9 +60,15 @@ def parse_constraint(constraints: str) -> BaseConstraint: def parse_single_constraint(constraint: str) -> Constraint: + # string comparator + if m := STR_CMP_CONSTRAINT.match(constraint): + op = m.group("op") + value = m.group("value").strip() + return Constraint(value, op) + # Basic comparator - m = BASIC_CONSTRAINT.match(constraint) - if m: + + if m := BASIC_CONSTRAINT.match(constraint): op = m.group(1) if op is None: op = "==" diff --git a/src/poetry/core/constraints/version/parser.py b/src/poetry/core/constraints/version/parser.py index 9980f4398..e4500974e 100644 --- a/src/poetry/core/constraints/version/parser.py +++ b/src/poetry/core/constraints/version/parser.py @@ -19,12 +19,16 @@ def parse_constraint(constraints: str) -> VersionConstraint: return _parse_constraint(constraints=constraints) -def parse_marker_version_constraint(constraints: str) -> VersionConstraint: - return _parse_constraint(constraints=constraints, is_marker_constraint=True) +def parse_marker_version_constraint( + constraints: str, *, pep440: bool = True +) -> VersionConstraint: + return _parse_constraint( + constraints=constraints, is_marker_constraint=True, pep440=pep440 + ) def _parse_constraint( - constraints: str, *, is_marker_constraint: bool = False + constraints: str, *, is_marker_constraint: bool = False, pep440: bool = True ) -> VersionConstraint: if constraints == "*": from poetry.core.constraints.version.version_range import VersionRange @@ -46,13 +50,17 @@ def _parse_constraint( for constraint in and_constraints: constraint_objects.append( parse_single_constraint( - constraint, is_marker_constraint=is_marker_constraint + constraint, + is_marker_constraint=is_marker_constraint, + pep440=pep440, ) ) else: constraint_objects.append( parse_single_constraint( - and_constraints[0], is_marker_constraint=is_marker_constraint + and_constraints[0], + is_marker_constraint=is_marker_constraint, + pep440=pep440, ) ) @@ -74,9 +82,10 @@ def _parse_constraint( def parse_single_constraint( - constraint: str, *, is_marker_constraint: bool = False + constraint: str, *, is_marker_constraint: bool = False, pep440: bool = True ) -> VersionConstraint: from poetry.core.constraints.version.patterns import BASIC_CONSTRAINT + from poetry.core.constraints.version.patterns import BASIC_RELEASE_CONSTRAINT from poetry.core.constraints.version.patterns import CARET_CONSTRAINT from poetry.core.constraints.version.patterns import TILDE_CONSTRAINT from poetry.core.constraints.version.patterns import TILDE_PEP440_CONSTRAINT @@ -185,6 +194,35 @@ def parse_single_constraint( return version + # These below should be reserved for comparing non python packages such as OS + # versions using `platform_release` + if not pep440 and (m := BASIC_RELEASE_CONSTRAINT.match(constraint)): + op = m.group("op") + release_string = m.group("release") + build = m.group("build") + + try: + version = Version( + release=Version.parse(release_string).release, + local=build, + ) + except InvalidVersion as e: + raise ParseConstraintError( + f"Could not parse version constraint: {constraint}" + ) from e + + if op == "<": + return VersionRange(max=version) + if op == "<=": + return VersionRange(max=version, include_max=True) + if op == ">": + return VersionRange(min=version) + if op == ">=": + return VersionRange(min=version, include_min=True) + if op == "!=": + return VersionUnion(VersionRange(max=version), VersionRange(min=version)) + return version + raise ParseConstraintError(f"Could not parse version constraint: {constraint}") diff --git a/src/poetry/core/constraints/version/patterns.py b/src/poetry/core/constraints/version/patterns.py index 6dfdf8c0d..d1187e55d 100644 --- a/src/poetry/core/constraints/version/patterns.py +++ b/src/poetry/core/constraints/version/patterns.py @@ -26,3 +26,17 @@ rf"^(?P<>|!=|>=?|<=?|==?)?\s*(?P{VERSION_PATTERN}|dev)(?P\.\*)?$", re.VERBOSE | re.IGNORECASE, ) + +RELEASE_PATTERN = r""" +(?P[0-9]+(?:\.[0-9]+)*) +(?:(\+|-)(?P + [0-9a-zA-Z-]+ + (?:\.[0-9a-zA-Z-]+)* +))? +""" + +# pattern for non Python versions such as OS versions in `platform_release` +BASIC_RELEASE_CONSTRAINT = re.compile( + rf"^(?P<>|!=|>=?|<=?|==?)?\s*(?P{RELEASE_PATTERN})$", + re.VERBOSE | re.IGNORECASE, +) diff --git a/src/poetry/core/version/markers.py b/src/poetry/core/version/markers.py index 1740d7c1e..2001cc384 100644 --- a/src/poetry/core/version/markers.py +++ b/src/poetry/core/version/markers.py @@ -19,6 +19,7 @@ from poetry.core.constraints.generic import Constraint from poetry.core.constraints.generic import MultiConstraint from poetry.core.constraints.generic import UnionConstraint +from poetry.core.constraints.generic.parser import STR_CMP_CONSTRAINT from poetry.core.constraints.version import VersionConstraint from poetry.core.constraints.version import VersionUnion from poetry.core.constraints.version.exceptions import ParseConstraintError @@ -232,15 +233,15 @@ def __init__(self, name: str, constraint: SingleMarkerConstraint) -> None: from poetry.core.constraints.generic import ( parse_constraint as parse_generic_constraint, ) - from poetry.core.constraints.version import ( - parse_constraint as parse_version_constraint, - ) + from poetry.core.constraints.version import parse_marker_version_constraint self._name = ALIASES.get(name, name) self._constraint = constraint self._parser: Callable[[str], BaseConstraint | VersionConstraint] if isinstance(constraint, VersionConstraint): - self._parser = parse_version_constraint + self._parser = functools.partial( + parse_marker_version_constraint, pep440=name != "platform_release" + ) else: self._parser = parse_generic_constraint @@ -336,7 +337,11 @@ def __hash__(self) -> int: class SingleMarker(SingleMarkerLike[Union[BaseConstraint, VersionConstraint]]): - _CONSTRAINT_RE = re.compile(r"(?i)^(~=|!=|>=?|<=?|==?=?|in|not in)?\s*(.+)$") + _CONSTRAINT_RE_PATTERN_1 = re.compile( + r"(?i)^(?P~=|!=|>=?|<=?|==?=?|not in|in)?\s*(?P.+)$" + ) + _CONSTRAINT_RE_PATTERN_2 = STR_CMP_CONSTRAINT + VALUE_SEPARATOR_RE = re.compile("[ ,|]+") _VERSION_LIKE_MARKER_NAME: ClassVar[set[str]] = { "python_version", @@ -345,7 +350,10 @@ class SingleMarker(SingleMarkerLike[Union[BaseConstraint, VersionConstraint]]): } def __init__( - self, name: str, constraint: str | BaseConstraint | VersionConstraint + self, + name: str, + constraint: str | BaseConstraint | VersionConstraint, + swapped_name_value: bool = False, ) -> None: from poetry.core.constraints.generic import ( parse_constraint as parse_generic_constraint, @@ -355,21 +363,32 @@ def __init__( parsed_constraint: BaseConstraint | VersionConstraint parser: Callable[[str], BaseConstraint | VersionConstraint] original_constraint_string = constraint_string = str(constraint) + self._swapped_name_value: bool = swapped_name_value - # Extract operator and value - m = self._CONSTRAINT_RE.match(constraint_string) + if swapped_name_value: + pattern = self._CONSTRAINT_RE_PATTERN_2 + else: + pattern = self._CONSTRAINT_RE_PATTERN_1 + + m = pattern.match(constraint_string) if m is None: raise InvalidMarker(f"Invalid marker for '{name}': {constraint_string}") - self._operator = m.group(1) + self._operator = m.group("op") if self._operator is None: self._operator = "==" - self._value = m.group(2) + self._value = m.group("value") parser = parse_generic_constraint - if name in self._VERSION_LIKE_MARKER_NAME: - parser = parse_marker_version_constraint + if swapped_name_value and name not in PYTHON_VERSION_MARKERS: + # Something like `"tegra" in platform_release` + # or `"arm" not in platform_version`. + pass + elif name in self._VERSION_LIKE_MARKER_NAME: + parser = functools.partial( + parse_marker_version_constraint, pep440=name != "platform_release" + ) if self._operator in {"in", "not in"}: versions = [] @@ -472,7 +491,11 @@ def invert(self) -> BaseMarker: # We should never go there raise RuntimeError(f"Invalid marker operator '{self._operator}'") - return parse_marker(f"{self._name} {operator} '{self._value}'") + if self._swapped_name_value: + constraint = f'"{self._value}" {operator} {self._name}' + else: + constraint = f'{self._name} {operator} "{self._value}"' + return parse_marker(constraint) def __eq__(self, other: object) -> bool: if not isinstance(other, SingleMarker): @@ -484,6 +507,8 @@ def __hash__(self) -> int: return hash(self._key) def __str__(self) -> str: + if self._swapped_name_value: + return f'"{self._value}" {self._operator} {self._name}' return f'{self._name} {self._operator} "{self._value}"' @@ -961,11 +986,21 @@ def _compact_markers( elif token.data == f"{tree_prefix}item": name, op, value = token.children - if value.type == f"{tree_prefix}MARKER_NAME": + swapped_name_value = value.type == f"{tree_prefix}MARKER_NAME" + stringed_value = name.type in { + f"{tree_prefix}ESCAPED_STRING", + f"{tree_prefix}SINGLE_QUOTED_STRING", + } + if swapped_name_value: name, value = value, name value = value[1:-1] - sub_marker = SingleMarker(str(name), f"{op}{value}") + + sub_marker = SingleMarker( + str(name), + f'"{value}" {op}' if stringed_value else f"{op}{value}", + swapped_name_value=swapped_name_value, + ) groups[-1].append(sub_marker) elif token.data == f"{tree_prefix}BOOL_OP" and token.children[0] == "or": 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] diff --git a/tests/constraints/generic/test_main.py b/tests/constraints/generic/test_main.py index 2707fd92f..567755233 100644 --- a/tests/constraints/generic/test_main.py +++ b/tests/constraints/generic/test_main.py @@ -18,6 +18,8 @@ ("==win32", Constraint("win32", "=")), ("!=win32", Constraint("win32", "!=")), ("!= win32", Constraint("win32", "!=")), + ("'tegra' not in", Constraint("tegra", "not in")), + ("'tegra' in", Constraint("tegra", "in")), ], ) def test_parse_constraint(input: str, constraint: AnyConstraint | Constraint) -> None: @@ -39,6 +41,13 @@ def test_parse_constraint(input: str, constraint: AnyConstraint | Constraint) -> Constraint("linux2", "!="), ), ), + ( + "'tegra' not in,'rpi-v8' not in", + MultiConstraint( + Constraint("tegra", "not in"), + Constraint("rpi-v8", "not in"), + ), + ), ], ) def test_parse_constraint_multi(input: str, constraint: MultiConstraint) -> None: @@ -53,6 +62,10 @@ def test_parse_constraint_multi(input: str, constraint: MultiConstraint) -> None "win32 || !=linux2", UnionConstraint(Constraint("win32"), Constraint("linux2", "!=")), ), + ( + "'tegra' in || 'rpi-v8' in", + UnionConstraint(Constraint("tegra", "in"), Constraint("rpi-v8", "in")), + ), ], ) def test_parse_constraint_union(input: str, constraint: UnionConstraint) -> None: diff --git a/tests/constraints/version/test_parse_constraint.py b/tests/constraints/version/test_parse_constraint.py index 17a6f2c3c..e71ba5c89 100644 --- a/tests/constraints/version/test_parse_constraint.py +++ b/tests/constraints/version/test_parse_constraint.py @@ -7,6 +7,8 @@ from poetry.core.constraints.version import VersionRange from poetry.core.constraints.version import VersionUnion from poetry.core.constraints.version import parse_constraint +from poetry.core.constraints.version import parse_marker_version_constraint +from poetry.core.constraints.version.exceptions import ParseConstraintError from poetry.core.version.pep440 import ReleaseTag @@ -595,3 +597,14 @@ def test_parse_constraint_with_white_space_padding( padding = " " * (4 if with_whitespace_padding else 0) constraint = padding.join(["", *constraint_parts, ""]) assert parse_constraint(constraint) == expected + + +def test_parse_marker_constraint_does_not_allow_invalid_version() -> None: + with pytest.raises(ParseConstraintError): + parse_marker_version_constraint("4.9.253-tegra") + + +def test_parse_marker_constraint_does_allow_invalid_version_if_requested() -> None: + assert parse_marker_version_constraint( + "4.9.253-tegra", pep440=False + ) == Version.from_parts(4, 9, 253, local="tegra") diff --git a/tests/version/test_markers.py b/tests/version/test_markers.py index 7e9164a9e..5fb2f5cf2 100644 --- a/tests/version/test_markers.py +++ b/tests/version/test_markers.py @@ -12,6 +12,7 @@ from poetry.core.version.markers import AnyMarker from poetry.core.version.markers import AtomicMarkerUnion from poetry.core.version.markers import EmptyMarker +from poetry.core.version.markers import InvalidMarker from poetry.core.version.markers import MarkerUnion from poetry.core.version.markers import MultiMarker from poetry.core.version.markers import SingleMarker @@ -62,12 +63,32 @@ 'extra == "a" or extra != "b"', 'extra != "a" or extra == "b"', 'extra != "a" or extra != "b"', + # String comparison markers + '"tegra" in platform_release', + '"tegra" not in platform_release', + '"tegra" in platform_release or "rpi-v8" in platform_release', + '"tegra" not in platform_release and "rpi-v8" not in platform_release', ], ) def test_parse_marker(marker: str) -> None: assert str(parse_marker(marker)) == marker +@pytest.mark.parametrize( + ("marker", "valid"), + [ + ('platform_release != "4.9.253-tegra"', True), + ('python_version != "4.9.253-tegra"', False), + ], +) +def test_parse_marker_non_python_versions(marker: str, valid: bool) -> None: + if valid: + assert str(parse_marker(marker)) == marker + else: + with pytest.raises(InvalidMarker): + parse_marker(marker) + + @pytest.mark.parametrize( ("marker", "expected_name", "expected_constraint"), [ @@ -110,6 +131,10 @@ def test_parse_marker(marker: str) -> None: "platform_machine", "!=aarch64, !=loongarch64", ), + ('"tegra" not in platform_release', "platform_release", "'tegra' not in"), + ('"rpi-v8" in platform_release', "platform_release", "'rpi-v8' in"), + ('"arm" not in platform_version', "platform_version", "'arm' not in"), + ('"arm" in platform_version', "platform_version", "'arm' in"), ], ) def test_parse_single_marker( @@ -954,6 +979,39 @@ def test_multi_marker_removes_duplicates() -> None: {"platform_machine": "x86_64"}, False, ), + ('"tegra" in platform_release', {"platform_release": "5.10.120-tegra"}, True), + ('"tegra" in platform_release', {"platform_release": "5.10.120"}, False), + ( + '"tegra" not in platform_release', + {"platform_release": "5.10.120-tegra"}, + False, + ), + ('"tegra" not in platform_release', {"platform_release": "5.10.120"}, True), + ( + "platform_machine == 'aarch64' and 'tegra' in platform_release", + {"platform_release": "5.10.120-tegra", "platform_machine": "aarch64"}, + True, + ), + ( + "platform_release != '4.9.253-tegra'", + {"platform_release": "4.9.254-tegra"}, + True, + ), + ( + "platform_release != '4.9.253-tegra'", + {"platform_release": "4.9.253"}, + True, + ), + ( + "platform_release >= '6.6.0+rpt-rpi-v8'", + {"platform_release": "6.6.20+rpt-rpi-v8"}, + True, + ), + ( + "platform_release < '5.10.123-tegra' and platform_release >= '4.9.254-tegra'", + {"platform_release": "4.9.254-tegra"}, + True, + ), # extras # single extra ("extra == 'security'", {"extra": "quux"}, False), @@ -1300,6 +1358,8 @@ def test_union_of_multi_with_a_containing_single() -> None: 'python_full_version ~= "3.6.3"', 'python_full_version < "3.6.3" or python_full_version >= "3.7.0"', ), + ('"tegra" in platform_release', '"tegra" not in platform_release'), + ('"tegra" not in platform_release', '"tegra" in platform_release'), ], ) def test_invert(marker: str, inverse: str) -> None: diff --git a/tests/version/test_requirements.py b/tests/version/test_requirements.py index 5c0e289e1..c1d6a34aa 100644 --- a/tests/version/test_requirements.py +++ b/tests/version/test_requirements.py @@ -109,6 +109,18 @@ def assert_requirement( ), }, ), + ( + ( + 'foo (>=1.2.3) ; "tegra" not in platform_release and python_version >= "3.10"' + ), + { + "name": "foo", + "constraint": ">=1.2.3", + "marker": ( + '"tegra" not in platform_release and python_version >= "3.10"' + ), + }, + ), ], ) def test_requirement(string: str, expected: dict[str, Any]) -> None: