From 946e88b4ca3f99aa8c7229c94760f1039cbc440f Mon Sep 17 00:00:00 2001 From: Mohammad Hijjawi Date: Mon, 28 Sep 2026 19:13:44 +0100 Subject: [PATCH] fix(nodes): keep every key of an AND group in parentheses When a node input expression contains a parenthesised group, the group is evaluated first and its result is spliced back into the expression. The keys were joined with "|", which turns an AND group into an OR: "a & (b & c)" became "a&b|c" and resolved to ["a", "b"], and "(a & b) & c" resolved to just ["a"], so required keys were silently dropped from the node's input. Join the group's keys with "&" so they stay required together. Groups that resolve to a single key (all built-in graph inputs) are unaffected. The same fix is applied to the standalone parse_expression helper. --- scrapegraphai/nodes/base_node.py | 2 +- scrapegraphai/utils/parse_state_keys.py | 2 +- tests/nodes/base_node_test.py | 24 ++++++++++++++++++++++++ tests/utils/parse_state_keys_test.py | 9 +++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 tests/nodes/base_node_test.py diff --git a/scrapegraphai/nodes/base_node.py b/scrapegraphai/nodes/base_node.py index 179865eb5..41f4acc0d 100644 --- a/scrapegraphai/nodes/base_node.py +++ b/scrapegraphai/nodes/base_node.py @@ -214,7 +214,7 @@ def evaluate_expression(expression: str) -> List[str]: sub_result = evaluate_simple_expression(sub_exp) expression = ( - expression[:start] + "|".join(sub_result) + expression[end + 1 :] + expression[:start] + "&".join(sub_result) + expression[end + 1 :] ) return evaluate_simple_expression(expression) diff --git a/scrapegraphai/utils/parse_state_keys.py b/scrapegraphai/utils/parse_state_keys.py index 040f13102..bcaeebafa 100644 --- a/scrapegraphai/utils/parse_state_keys.py +++ b/scrapegraphai/utils/parse_state_keys.py @@ -86,7 +86,7 @@ def evaluate_expression(expression): sub_exp = expression[start + 1 : end] sub_result = evaluate_simple_expression(sub_exp) expression = ( - expression[:start] + "|".join(sub_result) + expression[end + 1 :] + expression[:start] + "&".join(sub_result) + expression[end + 1 :] ) return evaluate_simple_expression(expression) diff --git a/tests/nodes/base_node_test.py b/tests/nodes/base_node_test.py new file mode 100644 index 000000000..920b0d24b --- /dev/null +++ b/tests/nodes/base_node_test.py @@ -0,0 +1,24 @@ +import pytest + +from scrapegraphai.nodes.base_node import BaseNode + + +class DummyNode(BaseNode): + def execute(self, state): + return state + + +@pytest.mark.parametrize( + "expression, expected", + [ + ("a & (b & c)", ["a", "b", "c"]), + ("(a & b) & c", ["a", "b", "c"]), + ("(a & b) | c", ["a", "b"]), + ("a & (d | b)", ["a", "b"]), + ], +) +def test_get_input_keys_with_parentheses(expression, expected): + node = DummyNode("Dummy", "node", expression, ["out"]) + state = {"a": 1, "b": 2, "c": 3} + + assert node.get_input_keys(state) == expected diff --git a/tests/utils/parse_state_keys_test.py b/tests/utils/parse_state_keys_test.py index a4617482b..656d6cac7 100644 --- a/tests/utils/parse_state_keys_test.py +++ b/tests/utils/parse_state_keys_test.py @@ -19,3 +19,12 @@ def test_parse_expression(): assert result != [] except ValueError as e: assert "Error" in str(e) + + +def test_parse_expression_and_inside_parentheses(): + """Every key of an AND group in parentheses is required and returned.""" + state = {"a": None, "b": None, "c": None} + + assert parse_expression("a & (b & c)", state) == ["a", "b", "c"] + assert parse_expression("(a & b) & c", state) == ["a", "b", "c"] + assert parse_expression("(a & b) | c", state) == ["a", "b"]