Skip to content

Commit d6cc772

Browse files
authored
Merge pull request #5801 from MetRonnie/parentheses
Handle parentheses on RHS of trigger expression
2 parents c6c1ef8 + c5714f4 commit d6cc772

File tree

3 files changed

+92
-33
lines changed

3 files changed

+92
-33
lines changed

changes.d/5801.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix traceback when using parentheses on right hand side of graph trigger.

cylc/flow/graph_parser.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
Dict,
2424
List,
2525
Tuple,
26-
Optional
26+
Optional,
27+
Union
2728
)
2829

2930
import cylc.flow.flags
@@ -85,10 +86,10 @@ class GraphParser:
8586
store dependencies for the whole workflow (call parse_graph multiple times
8687
and key results by graph section).
8788
88-
The general form of a dependency is "EXPRESSION => NODE", where:
89-
* On the right, NODE is a task or family name
89+
The general form of a dependency is "LHS => RHS", where:
9090
* On the left, an EXPRESSION of nodes involving parentheses, and
9191
logical operators '&' (AND), and '|' (OR).
92+
* On the right, an EXPRESSION of nodes NOT involving '|'
9293
* Node names may be parameterized (any number of parameters):
9394
NODE<i,j,k>
9495
NODE<i=0,j,k> # specific parameter value
@@ -517,32 +518,33 @@ def _proc_dep_pair(
517518
"Suicide markers must be"
518519
f" on the right of a trigger: {left}")
519520

521+
# Check that parentheses match.
522+
mismatch_msg = 'Mismatched parentheses in: "{}"'
523+
if left and left.count("(") != left.count(")"):
524+
raise GraphParseError(mismatch_msg.format(left))
525+
if right.count("(") != right.count(")"):
526+
raise GraphParseError(mismatch_msg.format(right))
527+
520528
# Ignore cycle point offsets on the right side.
521529
# (Note we can't ban this; all nodes get process as left and right.)
522530
if '[' in right:
523531
return
524532

525-
# Check that parentheses match.
526-
if left and left.count("(") != left.count(")"):
527-
raise GraphParseError(
528-
"Mismatched parentheses in: \"" + left + "\"")
529-
530533
# Split right side on AND.
531534
rights = right.split(self.__class__.OP_AND)
532535
if '' in rights or right and not all(rights):
533536
raise GraphParseError(
534537
f"Null task name in graph: {left} => {right}")
535538

539+
lefts: Union[List[str], List[Optional[str]]]
536540
if not left or (self.__class__.OP_OR in left or '(' in left):
537-
# Treat conditional or bracketed expressions as a single entity.
541+
# Treat conditional or parenthesised expressions as a single entity
538542
# Can get [None] or [""] here
539-
lefts: List[Optional[str]] = [left]
543+
lefts = [left]
540544
else:
541545
# Split non-conditional left-side expressions on AND.
542546
# Can get [""] here too
543-
# TODO figure out how to handle this wih mypy:
544-
# assign List[str] to List[Optional[str]]
545-
lefts = left.split(self.__class__.OP_AND) # type: ignore
547+
lefts = left.split(self.__class__.OP_AND)
546548
if '' in lefts or left and not all(lefts):
547549
raise GraphParseError(
548550
f"Null task name in graph: {left} => {right}")
@@ -847,9 +849,14 @@ def _compute_triggers(
847849
trigs += [f"{name}{offset}:{trigger}"]
848850

849851
for right in rights:
852+
right = right.strip('()') # parentheses don't matter
850853
m = self.__class__.REC_RHS_NODE.match(right)
851-
# This will match, bad nodes are detected earlier (type ignore):
852-
suicide_char, name, output, opt_char = m.groups() # type: ignore
854+
if not m:
855+
# Bad nodes should have been detected earlier; fail loudly
856+
raise ValueError( # pragma: no cover
857+
f"Unexpected graph expression: '{right}'"
858+
)
859+
suicide_char, name, output, opt_char = m.groups()
853860
suicide = (suicide_char == self.__class__.SUICIDE)
854861
optional = (opt_char == self.__class__.OPTIONAL)
855862
if output:

tests/unit/test_graph_parser.py

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"""Unit tests for the GraphParser."""
1717

1818
import logging
19+
from typing import Dict, List
1920
import pytest
2021
from itertools import product
2122
from pytest import param
@@ -86,45 +87,59 @@ def test_graph_syntax_errors_2(seq, graph, expected_err):
8687
@pytest.mark.parametrize(
8788
'graph, expected_err',
8889
[
89-
[
90+
(
9091
"a b => c",
9192
"Bad graph node format"
92-
],
93-
[
93+
),
94+
(
95+
"a => b c",
96+
"Bad graph node format"
97+
),
98+
(
9499
"!foo => bar",
95100
"Suicide markers must be on the right of a trigger:"
96-
],
97-
[
101+
),
102+
(
98103
"( foo & bar => baz",
99-
"Mismatched parentheses in:"
100-
],
101-
[
104+
'Mismatched parentheses in: "(foo&bar"'
105+
),
106+
(
107+
"a => b & c)",
108+
'Mismatched parentheses in: "b&c)"'
109+
),
110+
(
111+
"(a => b & c)",
112+
'Mismatched parentheses in: "(a"'
113+
),
114+
(
115+
"(a => b[+P1]",
116+
'Mismatched parentheses in: "(a"'
117+
),
118+
(
102119
"""(a | b & c) => d
103120
foo => bar
104121
(a | b & c) => !d""",
105122
"can't trigger both d and !d"
106-
],
107-
[
123+
),
124+
(
108125
"a => b | c",
109126
"Illegal OR on right side"
110-
],
111-
[
127+
),
128+
(
112129
"foo && bar => baz",
113130
"The graph AND operator is '&'"
114-
],
115-
[
131+
),
132+
(
116133
"foo || bar => baz",
117134
"The graph OR operator is '|'"
118-
],
135+
),
119136
]
120137
)
121138
def test_graph_syntax_errors(graph, expected_err):
122139
"""Test various graph syntax errors."""
123140
with pytest.raises(GraphParseError) as cm:
124141
GraphParser().parse_graph(graph)
125-
assert (
126-
expected_err in str(cm.value)
127-
)
142+
assert expected_err in str(cm.value)
128143

129144

130145
def test_parse_graph_simple():
@@ -845,3 +860,39 @@ def test_fail_family_triggers_on_tasks(ftrig):
845860
"family trigger on non-family namespace"
846861
)
847862
)
863+
864+
865+
@pytest.mark.parametrize(
866+
'graph, expected_triggers',
867+
[
868+
param(
869+
'a => b & c',
870+
{'a': [''], 'b': ['a:succeeded'], 'c': ['a:succeeded']},
871+
id="simple"
872+
),
873+
param(
874+
'a => (b & c)',
875+
{'a': [''], 'b': ['a:succeeded'], 'c': ['a:succeeded']},
876+
id="simple w/ parentheses"
877+
),
878+
param(
879+
'a => (b & (c & d))',
880+
{
881+
'a': [''],
882+
'b': ['a:succeeded'],
883+
'c': ['a:succeeded'],
884+
'd': ['a:succeeded'],
885+
},
886+
id="more parentheses"
887+
),
888+
]
889+
)
890+
def test_RHS_AND(graph: str, expected_triggers: Dict[str, List[str]]):
891+
"""Test '&' operator on right hand side of trigger expression."""
892+
gp = GraphParser()
893+
gp.parse_graph(graph)
894+
triggers = {
895+
task: list(trigs.keys())
896+
for task, trigs in gp.triggers.items()
897+
}
898+
assert triggers == expected_triggers

0 commit comments

Comments
 (0)