Skip to content

Commit 80f8a97

Browse files
add support for string comparisons with in/not in (#722)
This allows markers like `"tegra" in platform_release`. Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com>
1 parent 4f766a6 commit 80f8a97

File tree

5 files changed

+97
-15
lines changed

5 files changed

+97
-15
lines changed

src/poetry/core/constraints/generic/parser.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
1616

1717

1818
BASIC_CONSTRAINT = re.compile(r"^(!?==?)?\s*([^\s]+?)\s*$")
19+
STR_CMP_CONSTRAINT = re.compile(
20+
r"""(?ix)^ # case insensitive and verbose mode
21+
(?P<quote>['"]) # Single or double quotes
22+
(?P<value>.+?) # The value itself inside quotes
23+
\1 # Closing single of double quote
24+
\s* # Space
25+
(?P<op>(not\sin|in)) # Literal match of 'in' or 'not in'
26+
$"""
27+
)
1928

2029

2130
@functools.lru_cache(maxsize=None)
@@ -26,9 +35,7 @@ def parse_constraint(constraints: str) -> BaseConstraint:
2635
or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip())
2736
or_groups = []
2837
for constraints in or_constraints:
29-
and_constraints = re.split(
30-
r"(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)", constraints
31-
)
38+
and_constraints = re.split(r"\s*,\s*", constraints)
3239
constraint_objects = []
3340

3441
if len(and_constraints) > 1:
@@ -53,9 +60,15 @@ def parse_constraint(constraints: str) -> BaseConstraint:
5360

5461

5562
def parse_single_constraint(constraint: str) -> Constraint:
63+
# string comparator
64+
if m := STR_CMP_CONSTRAINT.match(constraint):
65+
op = m.group("op")
66+
value = m.group("value").strip()
67+
return Constraint(value, op)
68+
5669
# Basic comparator
57-
m = BASIC_CONSTRAINT.match(constraint)
58-
if m:
70+
71+
if m := BASIC_CONSTRAINT.match(constraint):
5972
op = m.group(1)
6073
if op is None:
6174
op = "=="

src/poetry/core/version/markers.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from poetry.core.constraints.generic import Constraint
2020
from poetry.core.constraints.generic import MultiConstraint
2121
from poetry.core.constraints.generic import UnionConstraint
22+
from poetry.core.constraints.generic.parser import STR_CMP_CONSTRAINT
2223
from poetry.core.constraints.version import VersionConstraint
2324
from poetry.core.constraints.version import VersionUnion
2425
from poetry.core.constraints.version.exceptions import ParseConstraintError
@@ -336,7 +337,11 @@ def __hash__(self) -> int:
336337

337338

338339
class SingleMarker(SingleMarkerLike[Union[BaseConstraint, VersionConstraint]]):
339-
_CONSTRAINT_RE = re.compile(r"(?i)^(~=|!=|>=?|<=?|==?=?|in|not in)?\s*(.+)$")
340+
_CONSTRAINT_RE_PATTERN_1 = re.compile(
341+
r"(?i)^(?P<op>~=|!=|>=?|<=?|==?=?|not in|in)?\s*(?P<value>.+)$"
342+
)
343+
_CONSTRAINT_RE_PATTERN_2 = STR_CMP_CONSTRAINT
344+
340345
VALUE_SEPARATOR_RE = re.compile("[ ,|]+")
341346
_VERSION_LIKE_MARKER_NAME: ClassVar[set[str]] = {
342347
"python_version",
@@ -345,7 +350,10 @@ class SingleMarker(SingleMarkerLike[Union[BaseConstraint, VersionConstraint]]):
345350
}
346351

347352
def __init__(
348-
self, name: str, constraint: str | BaseConstraint | VersionConstraint
353+
self,
354+
name: str,
355+
constraint: str | BaseConstraint | VersionConstraint,
356+
swapped_name_value: bool = False,
349357
) -> None:
350358
from poetry.core.constraints.generic import (
351359
parse_constraint as parse_generic_constraint,
@@ -355,20 +363,29 @@ def __init__(
355363
parsed_constraint: BaseConstraint | VersionConstraint
356364
parser: Callable[[str], BaseConstraint | VersionConstraint]
357365
original_constraint_string = constraint_string = str(constraint)
366+
self._swapped_name_value: bool = swapped_name_value
367+
368+
if swapped_name_value:
369+
pattern = self._CONSTRAINT_RE_PATTERN_2
370+
else:
371+
pattern = self._CONSTRAINT_RE_PATTERN_1
358372

359-
# Extract operator and value
360-
m = self._CONSTRAINT_RE.match(constraint_string)
373+
m = pattern.match(constraint_string)
361374
if m is None:
362375
raise InvalidMarker(f"Invalid marker for '{name}': {constraint_string}")
363376

364-
self._operator = m.group(1)
377+
self._operator = m.group("op")
365378
if self._operator is None:
366379
self._operator = "=="
367380

368-
self._value = m.group(2)
381+
self._value = m.group("value")
369382
parser = parse_generic_constraint
370383

371-
if name in self._VERSION_LIKE_MARKER_NAME:
384+
if swapped_name_value and name not in PYTHON_VERSION_MARKERS:
385+
# Something like `"tegra" in platform_release`
386+
# or `"arm" not in platform_version`.
387+
pass
388+
elif name in self._VERSION_LIKE_MARKER_NAME:
372389
parser = parse_marker_version_constraint
373390

374391
if self._operator in {"in", "not in"}:
@@ -472,7 +489,11 @@ def invert(self) -> BaseMarker:
472489
# We should never go there
473490
raise RuntimeError(f"Invalid marker operator '{self._operator}'")
474491

475-
return parse_marker(f"{self._name} {operator} '{self._value}'")
492+
if self._swapped_name_value:
493+
constraint = f'"{self._value}" {operator} {self._name}'
494+
else:
495+
constraint = f'{self._name} {operator} "{self._value}"'
496+
return parse_marker(constraint)
476497

477498
def __eq__(self, other: object) -> bool:
478499
if not isinstance(other, SingleMarker):
@@ -484,6 +505,8 @@ def __hash__(self) -> int:
484505
return hash(self._key)
485506

486507
def __str__(self) -> str:
508+
if self._swapped_name_value:
509+
return f'"{self._value}" {self._operator} {self._name}'
487510
return f'{self._name} {self._operator} "{self._value}"'
488511

489512

@@ -961,11 +984,21 @@ def _compact_markers(
961984

962985
elif token.data == f"{tree_prefix}item":
963986
name, op, value = token.children
964-
if value.type == f"{tree_prefix}MARKER_NAME":
987+
swapped_name_value = value.type == f"{tree_prefix}MARKER_NAME"
988+
stringed_value = name.type in {
989+
f"{tree_prefix}ESCAPED_STRING",
990+
f"{tree_prefix}SINGLE_QUOTED_STRING",
991+
}
992+
if swapped_name_value:
965993
name, value = value, name
966994

967995
value = value[1:-1]
968-
sub_marker = SingleMarker(str(name), f"{op}{value}")
996+
997+
sub_marker = SingleMarker(
998+
str(name),
999+
f'"{value}" {op}' if stringed_value else f"{op}{value}",
1000+
swapped_name_value=swapped_name_value,
1001+
)
9691002
groups[-1].append(sub_marker)
9701003

9711004
elif token.data == f"{tree_prefix}BOOL_OP" and token.children[0] == "or":

tests/constraints/generic/test_main.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
("==win32", Constraint("win32", "=")),
1919
("!=win32", Constraint("win32", "!=")),
2020
("!= win32", Constraint("win32", "!=")),
21+
("'tegra' not in", Constraint("tegra", "not in")),
22+
("'tegra' in", Constraint("tegra", "in")),
2123
],
2224
)
2325
def test_parse_constraint(input: str, constraint: AnyConstraint | Constraint) -> None:
@@ -39,6 +41,13 @@ def test_parse_constraint(input: str, constraint: AnyConstraint | Constraint) ->
3941
Constraint("linux2", "!="),
4042
),
4143
),
44+
(
45+
"'tegra' not in,'rpi-v8' not in",
46+
MultiConstraint(
47+
Constraint("tegra", "not in"),
48+
Constraint("rpi-v8", "not in"),
49+
),
50+
),
4251
],
4352
)
4453
def test_parse_constraint_multi(input: str, constraint: MultiConstraint) -> None:
@@ -53,6 +62,10 @@ def test_parse_constraint_multi(input: str, constraint: MultiConstraint) -> None
5362
"win32 || !=linux2",
5463
UnionConstraint(Constraint("win32"), Constraint("linux2", "!=")),
5564
),
65+
(
66+
"'tegra' in || 'rpi-v8' in",
67+
UnionConstraint(Constraint("tegra", "in"), Constraint("rpi-v8", "in")),
68+
),
5669
],
5770
)
5871
def test_parse_constraint_union(input: str, constraint: UnionConstraint) -> None:

tests/version/test_markers.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
'extra == "a" or extra != "b"',
6363
'extra != "a" or extra == "b"',
6464
'extra != "a" or extra != "b"',
65+
# String comparison markers
66+
'"tegra" in platform_release',
67+
'"tegra" not in platform_release',
68+
'"tegra" in platform_release or "rpi-v8" in platform_release',
69+
'"tegra" not in platform_release and "rpi-v8" not in platform_release',
6570
],
6671
)
6772
def test_parse_marker(marker: str) -> None:
@@ -110,6 +115,10 @@ def test_parse_marker(marker: str) -> None:
110115
"platform_machine",
111116
"!=aarch64, !=loongarch64",
112117
),
118+
('"tegra" not in platform_release', "platform_release", "'tegra' not in"),
119+
('"rpi-v8" in platform_release', "platform_release", "'rpi-v8' in"),
120+
('"arm" not in platform_version', "platform_version", "'arm' not in"),
121+
('"arm" in platform_version', "platform_version", "'arm' in"),
113122
],
114123
)
115124
def test_parse_single_marker(
@@ -1300,6 +1309,8 @@ def test_union_of_multi_with_a_containing_single() -> None:
13001309
'python_full_version ~= "3.6.3"',
13011310
'python_full_version < "3.6.3" or python_full_version >= "3.7.0"',
13021311
),
1312+
('"tegra" in platform_release', '"tegra" not in platform_release'),
1313+
('"tegra" not in platform_release', '"tegra" in platform_release'),
13031314
],
13041315
)
13051316
def test_invert(marker: str, inverse: str) -> None:

tests/version/test_requirements.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,18 @@ def assert_requirement(
109109
),
110110
},
111111
),
112+
(
113+
(
114+
'foo (>=1.2.3) ; "tegra" not in platform_release and python_version >= "3.10"'
115+
),
116+
{
117+
"name": "foo",
118+
"constraint": ">=1.2.3",
119+
"marker": (
120+
'"tegra" not in platform_release and python_version >= "3.10"'
121+
),
122+
},
123+
),
112124
],
113125
)
114126
def test_requirement(string: str, expected: dict[str, Any]) -> None:

0 commit comments

Comments
 (0)