Skip to content

Commit 7b218cf

Browse files
committed
Use jinja expression parser to match conditions
1 parent 7555725 commit 7b218cf

File tree

2 files changed

+57
-50
lines changed

2 files changed

+57
-50
lines changed

conda_forge_tick/migrators/recipe_v1.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
from pathlib import Path
44
from typing import Any
55

6+
from jinja2 import Environment
7+
from jinja2.nodes import Compare, Node, Not
8+
from jinja2.parser import Parser
9+
610
from conda_forge_tick.migrators.core import MiniMigrator
711
from conda_forge_tick.recipe_parser._parser import _get_yaml_parser
812

@@ -12,38 +16,49 @@
1216
logger = logging.getLogger(__name__)
1317

1418

15-
def get_condition(node: Any) -> str | None:
19+
def get_condition(node: Any) -> Node | None:
1620
if isinstance(node, dict) and "if" in node:
17-
return node["if"].strip()
21+
return Parser(
22+
Environment(), node["if"].strip(), state="variable"
23+
).parse_expression()
1824
return None
1925

2026

21-
def is_same_condition(a: str, b: str) -> bool:
27+
def is_same_condition(a: Node, b: Node) -> bool:
2228
return a == b
2329

2430

25-
def is_single_expression(condition: str) -> bool:
26-
return not any(f" {x} " in condition for x in ("and", "or", "if"))
27-
31+
INVERSE_OPS = {
32+
"eq": "ne",
33+
"ne": "eq",
34+
"gt": "lteq",
35+
"gteq": "lt",
36+
"lt": "gteq",
37+
"lteq": "gt",
38+
"in": "notin",
39+
"notin": "in",
40+
}
2841

29-
def is_negated_condition(a: str, b: str) -> bool:
30-
# we only handle negating trivial expressions
31-
if not all(map(is_single_expression, (a, b))):
32-
return False
3342

43+
def is_negated_condition(a: Node, b: Node) -> bool:
3444
# X <-> not X
35-
a_not = a.startswith("not")
36-
b_not = b.startswith("not")
37-
if (
38-
a_not != b_not
39-
and a.removeprefix("not").lstrip() == b.removeprefix("not").lstrip()
40-
):
45+
if Not(a) == b or a == Not(b):
4146
return True
4247

48+
# unwrap (not X) <-> (not Y)
49+
if isinstance(a, Not) and isinstance(b, Not):
50+
a = a.node
51+
b = b.node
52+
4353
# A == B <-> A != B
44-
if "==" in b and a == b.replace("==", "!=", 1):
45-
return True
46-
if "!=" in b and a == b.replace("!=", "==", 1):
54+
if (
55+
isinstance(a, Compare)
56+
and isinstance(b, Compare)
57+
and len(a.ops) == len(b.ops) == 1
58+
and a.expr == b.expr
59+
and a.ops[0].expr == b.ops[0].expr
60+
and a.ops[0].op == INVERSE_OPS[b.ops[0].op]
61+
):
4762
return True
4863

4964
return False

tests/test_recipe_v1.py

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
Version,
1010
)
1111
from conda_forge_tick.migrators.recipe_v1 import (
12+
get_condition,
1213
is_negated_condition,
13-
is_single_expression,
1414
)
1515

1616
YAML_PATH = Path(__file__).parent / "test_v1_yaml"
@@ -21,44 +21,31 @@
2121
)
2222

2323

24-
@pytest.mark.parametrize(
25-
"x",
26-
[
27-
"win",
28-
"not unix",
29-
'cuda_compiler_version == "None"',
30-
"build_platform != target_platform",
31-
],
32-
)
33-
def test_is_single_expression(x):
34-
assert is_single_expression(x)
35-
36-
37-
@pytest.mark.parametrize(
38-
"x",
39-
[
40-
'cuda_compiler_version != "None" and linux"',
41-
'unix and blas_impl != "mkl"',
42-
"linux or osx",
43-
"foo if bar else baz",
44-
],
45-
)
46-
def test_not_is_single_expression(x):
47-
assert not is_single_expression(x)
48-
49-
5024
@pytest.mark.parametrize(
5125
"a,b",
5226
[
5327
("unix", "not unix"),
5428
('cuda_compiler_version == "None"', 'not cuda_compiler_version == "None"'),
5529
('cuda_compiler_version == "None"', 'cuda_compiler_version != "None"'),
5630
('not cuda_compiler_version == "None"', 'not cuda_compiler_version != "None"'),
31+
(
32+
'cuda_compiler_version != "None" and linux',
33+
'not (cuda_compiler_version != "None" and linux)',
34+
),
35+
("linux or osx", "not (linux or osx)"),
36+
("a >= 14", "a < 14"),
37+
("a >= 14", "not (a >= 14)"),
38+
("a in [1, 2, 3]", "a not in [1, 2, 3]"),
39+
("a in [1, 2, 3]", "not a in [1, 2, 3]"),
40+
("a + b < 10", "a + b >= 10"),
41+
("a == b == c", "not (a == b == c)"),
5742
],
5843
)
5944
def test_is_negated_condition(a, b):
60-
assert is_negated_condition(a, b)
61-
assert is_negated_condition(b, a)
45+
a_cond = get_condition({"if": a})
46+
b_cond = get_condition({"if": b})
47+
assert is_negated_condition(a_cond, b_cond)
48+
assert is_negated_condition(b_cond, a_cond)
6249

6350

6451
@pytest.mark.parametrize(
@@ -69,11 +56,16 @@ def test_is_negated_condition(a, b):
6956
('cuda_compiler_version != "None"', 'not cuda_compiler_version == "None"'),
7057
("a or b", "not a or b"),
7158
("a and b", "not a and b"),
59+
("a == b == c", "a != b != c"),
60+
("a > 4", "a < 4"),
61+
("a == b == c", "not (a == b) == c"),
7262
],
7363
)
7464
def test_not_is_negated_condition(a, b):
75-
assert not is_negated_condition(a, b)
76-
assert not is_negated_condition(b, a)
65+
a_cond = get_condition({"if": a})
66+
b_cond = get_condition({"if": b})
67+
assert not is_negated_condition(a_cond, b_cond)
68+
assert not is_negated_condition(b_cond, a_cond)
7769

7870

7971
@flaky

0 commit comments

Comments
 (0)