Skip to content

Commit

Permalink
add support for string comparisons with in/not in (#722)
Browse files Browse the repository at this point in the history
This allows markers like `"tegra" in platform_release`.

Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com>
  • Loading branch information
npapapietro and radoering committed Jul 30, 2024
1 parent 4f766a6 commit 80f8a97
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 15 deletions.
23 changes: 18 additions & 5 deletions src/poetry/core/constraints/generic/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<quote>['"]) # Single or double quotes
(?P<value>.+?) # The value itself inside quotes
\1 # Closing single of double quote
\s* # Space
(?P<op>(not\sin|in)) # Literal match of 'in' or 'not in'
$"""
)


@functools.lru_cache(maxsize=None)
Expand All @@ -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"(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)", constraints
)
and_constraints = re.split(r"\s*,\s*", constraints)
constraint_objects = []

if len(and_constraints) > 1:
Expand All @@ -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 = "=="
Expand Down
53 changes: 43 additions & 10 deletions src/poetry/core/version/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<op>~=|!=|>=?|<=?|==?=?|not in|in)?\s*(?P<value>.+)$"
)
_CONSTRAINT_RE_PATTERN_2 = STR_CMP_CONSTRAINT

VALUE_SEPARATOR_RE = re.compile("[ ,|]+")
_VERSION_LIKE_MARKER_NAME: ClassVar[set[str]] = {
"python_version",
Expand All @@ -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,
Expand All @@ -355,20 +363,29 @@ 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

if swapped_name_value:
pattern = self._CONSTRAINT_RE_PATTERN_2
else:
pattern = self._CONSTRAINT_RE_PATTERN_1

# Extract operator and value
m = self._CONSTRAINT_RE.match(constraint_string)
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:
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 = parse_marker_version_constraint

if self._operator in {"in", "not in"}:
Expand Down Expand Up @@ -472,7 +489,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):
Expand All @@ -484,6 +505,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}"'


Expand Down Expand Up @@ -961,11 +984,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":
Expand Down
13 changes: 13 additions & 0 deletions tests/constraints/generic/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions tests/version/test_markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@
'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:
Expand Down Expand Up @@ -110,6 +115,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(
Expand Down Expand Up @@ -1300,6 +1309,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:
Expand Down
12 changes: 12 additions & 0 deletions tests/version/test_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 80f8a97

Please sign in to comment.