From 1228d7c90eead63991249ec49829796150da38fb Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Sat, 22 Mar 2025 18:16:25 +0000 Subject: [PATCH 1/8] Add conditional --- README.md | 15 ++++++++++ py_trees_parser/parser.py | 19 +++++++++++++ test/data/test_conditional.xml | 30 ++++++++++++++++++++ test/data/test_conditional_main.xml | 6 ++++ test/test_parser.py | 43 +++++++++++++++++++++-------- 5 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 test/data/test_conditional.xml create mode 100644 test/data/test_conditional_main.xml diff --git a/README.md b/README.md index 5836159..84518ba 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,21 @@ and finally in subtree2 ``` +### Conditionals + +Conditionals are a method for choosing which xml nodes should be included in the +final tree. If a condition evaluates to true then the element is included in the +final tree otherwise it is not. Conditionals can be used with any element of a +behavior tree and is represented by the `if` keyword and are written +like + +```xml + + + + +``` + ### Logging By default the parser uses `rclpy.logging` module to log messages. However, if this is not available diff --git a/py_trees_parser/parser.py b/py_trees_parser/parser.py index 8c2538f..6c88559 100644 --- a/py_trees_parser/parser.py +++ b/py_trees_parser/parser.py @@ -378,6 +378,9 @@ def _create_node( name = node_attribs["name"] del node_attribs["name"] + if "if" in node_attribs: + del node_attribs["if"] + self.logger.debug(f"Found {node_type}") # name is a special attribute that is handled separately @@ -453,6 +456,19 @@ def _sub_args(self, args, var): return None + def _condition(self, condition): + if condition is None: + return True + + try: + # Safely evaluate the condition + result = eval(condition, {"__builtins__": {}}, {}) + self.logger.debug(f"Condition result: {result}") + return bool(result) + except Exception as ex: + self.logger.error(f"Error evaluating condition '{condition}': {ex}") + raise ValueError(f"Error evaluating condition '{condition}'") from ex + def _build_tree( self, xml_node: Element, @@ -503,6 +519,9 @@ def _build_tree( children = list() for child_xml in xml_node: self._process_args(child_xml, args) + if_cond = child_xml.attrib.get("if") + if not self._condition(if_cond): + continue child = self._build_tree(child_xml, args) children.append(child) diff --git a/test/data/test_conditional.xml b/test/data/test_conditional.xml new file mode 100644 index 0000000..3a4e2cf --- /dev/null +++ b/test/data/test_conditional.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/data/test_conditional_main.xml b/test/data/test_conditional_main.xml new file mode 100644 index 0000000..e0dd0f7 --- /dev/null +++ b/test/data/test_conditional_main.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/test_parser.py b/test/test_parser.py index 3e4e9e8..e2d1c18 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -47,7 +47,7 @@ def setup_parser(ros_init): """Setup the parser and test file processing.""" def _setup(tree_file): - xml = os.path.join(SHARE_DIR, tree_file) + xml = os.path.join(SHARE_DIR, "test", "data", tree_file) parser = BTParser(xml, log_level=rclpy.logging.LoggingSeverity.DEBUG) try: root = parser.parse() @@ -63,12 +63,12 @@ def _setup(tree_file): @pytest.mark.parametrize( "tree_file", [ - "test/data/test1.xml", - "test/data/test6.xml", - "test/data/test_idioms.xml", - "test/data/test_function_parse.xml", - "test/data/test_subtree_main.xml", - "test/data/test_arg_substitution_main.xml", + "test1.xml", + "test6.xml", + "test_idioms.xml", + "test_function_parse.xml", + "test_subtree_main.xml", + "test_arg_substitution_main.xml", ], ) def test_tree_parser(setup_parser, tree_file): @@ -78,7 +78,7 @@ def test_tree_parser(setup_parser, tree_file): def test_subtree_and_args(setup_parser): """Test that subtree arguments are working as expected.""" - tree_file = "test/data/test_args.xml" + tree_file = "test_args.xml" root = setup_parser(tree_file) assert root.name == "Subtree Selector" @@ -95,7 +95,7 @@ def test_subtree_and_args(setup_parser): def test_subtree_cascaded_args(setup_parser): """Test that cascaded args through subtrees and nested subtrees is working as expected.""" - tree_file = "test/data/test_cascade_args.xml" + tree_file = "test_cascade_args.xml" root = setup_parser(tree_file).children[0] assert root.name == "Subtree Selector" @@ -112,7 +112,7 @@ def test_subtree_cascaded_args(setup_parser): def test_arg_substitution_within_strings(setup_parser): """Test that argument substitution within strings works as expected.""" - tree_file = "test/data/test_arg_substitution_main.xml" + tree_file = "test_arg_substitution_main.xml" root = setup_parser(tree_file) assert root.name == "Argument Substitution Test" @@ -134,10 +134,31 @@ def test_arg_substitution_within_strings(setup_parser): def test_bool(setup_parser): """Test that True, true, False, or false are evaluated as booleans.""" - tree_file = "test/data/test_bool.xml" + tree_file = "test_bool.xml" root = setup_parser(tree_file) assert root.name == "No Memory" assert root.memory is False assert root.children[0].name == "Memory" assert root.children[0].memory + + +def test_conditionals(setup_parser): + """Test that conditionals include nodes as expected.""" + tree_file = "test_conditional_main.xml" + root = setup_parser(tree_file) + + assert root.name == "Conditional Test" + + children = root.children + assert children[0].name == "Advanced Mode Feature" + assert children[1].name == "Non-Basic Mode Feature" + assert children[2].name == "High Level Feature" + assert children[3].name == "Debug Feature" + assert children[4].name == "Conditional Selector" + assert children[5].name == "Always Included" + + children = children[4].children + assert children[0].name == "Feature 1" + assert children[1].name == "Feature 2" + assert children[2].name == "Feature 3" From a46afb2e87d6100b38902a0a9630dcfe8959b537 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Sat, 22 Mar 2025 18:56:36 +0000 Subject: [PATCH 2/8] Update version --- package.xml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.xml b/package.xml index e0fa8d4..66534e0 100644 --- a/package.xml +++ b/package.xml @@ -2,7 +2,7 @@ py_trees_parser - 0.6.0 + 0.6.1 A py_trees xml parser Erich L Foster Apache-2.0 diff --git a/setup.py b/setup.py index de4cdb3..67d90da 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( name=package_name, - version="0.6.0", + version="0.6.1", packages=find_packages(exclude=["test"]), data_files=[ ("share/ament_index/resource_index/packages", [os.path.join("resource", package_name)]), From e2e9a7442bec5ea4b8611bb96fe8fc0896bde69e Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Sat, 22 Mar 2025 19:06:11 +0000 Subject: [PATCH 3/8] Update change log --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1af1906..6712740 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog for package py_trees_parser .. This is only a rough description of the main changes of the repository +0.6.1 (2025-03-21) +------------------ +* Add inline argument substitution +* Add conditionals + 0.6.0 (2025-01-24) ------------------ * Split py_trees_parser out into its own repo From 14c52769b397211758f33d5dd4ddf4d614c10308 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Sat, 22 Mar 2025 19:09:15 +0000 Subject: [PATCH 4/8] Add debug log to _condition --- py_trees_parser/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/py_trees_parser/parser.py b/py_trees_parser/parser.py index 6c88559..87bbc09 100644 --- a/py_trees_parser/parser.py +++ b/py_trees_parser/parser.py @@ -462,6 +462,7 @@ def _condition(self, condition): try: # Safely evaluate the condition + self.logger.debug(f"Found conditional: {condition}") result = eval(condition, {"__builtins__": {}}, {}) self.logger.debug(f"Condition result: {result}") return bool(result) From a89715ff1a50f43d6d30029630f2af315083fb35 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Mon, 24 Mar 2025 17:16:52 +0000 Subject: [PATCH 5/8] Fix linting --- test/test_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_parser.py b/test/test_parser.py index e2d1c18..2b99eeb 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -53,7 +53,7 @@ def _setup(tree_file): root = parser.parse() py_trees_ros.trees.BehaviourTree(root=root, unicode_tree_debug=True) except Exception as ex: - assert False, f"parse raised an exception {ex}" + assert False, f"parse raised an exception {ex}" # noqa return root From 015c133857d5d9f8937a10ee499d13f554573548 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Fri, 28 Mar 2025 14:34:56 +0000 Subject: [PATCH 6/8] Add comment for deleting if condition --- py_trees_parser/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/py_trees_parser/parser.py b/py_trees_parser/parser.py index 87bbc09..0ceaaca 100644 --- a/py_trees_parser/parser.py +++ b/py_trees_parser/parser.py @@ -378,6 +378,7 @@ def _create_node( name = node_attribs["name"] del node_attribs["name"] + # creating node, so we've handle the condition already if "if" in node_attribs: del node_attribs["if"] From cfd87ec10d45e46b5fc56b84c70a9f26c79173be Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Thu, 3 Apr 2025 09:54:11 +0000 Subject: [PATCH 7/8] Fix conditional description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 84518ba..ce72d49 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ and finally in subtree2 ### Conditionals -Conditionals are a method for choosing which xml nodes should be included in the +A conditionals allows one to choose which xml nodes should be included in the final tree. If a condition evaluates to true then the element is included in the final tree otherwise it is not. Conditionals can be used with any element of a behavior tree and is represented by the `if` keyword and are written From 0a50edb4e6f4345f03b420aed566d24693eb5cb4 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Thu, 3 Apr 2025 10:58:05 +0000 Subject: [PATCH 8/8] Add subtree to conditional test --- test/data/test_conditional.xml | 20 +++++++++++++++++--- test/data/test_conditional_subtree.xml | 7 +++++++ test/test_parser.py | 18 ++++++++++++------ 3 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 test/data/test_conditional_subtree.xml diff --git a/test/data/test_conditional.xml b/test/data/test_conditional.xml index 3a4e2cf..c52ae7f 100644 --- a/test/data/test_conditional.xml +++ b/test/data/test_conditional.xml @@ -1,4 +1,4 @@ - + @@ -13,18 +13,32 @@ - + - + + + + + + + + + + + + + + + diff --git a/test/data/test_conditional_subtree.xml b/test/data/test_conditional_subtree.xml new file mode 100644 index 0000000..b3f1c66 --- /dev/null +++ b/test/data/test_conditional_subtree.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/test_parser.py b/test/test_parser.py index 2b99eeb..45c2f41 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -156,9 +156,15 @@ def test_conditionals(setup_parser): assert children[2].name == "High Level Feature" assert children[3].name == "Debug Feature" assert children[4].name == "Conditional Selector" - assert children[5].name == "Always Included" - - children = children[4].children - assert children[0].name == "Feature 1" - assert children[1].name == "Feature 2" - assert children[2].name == "Feature 3" + assert children[5].name == "Included Subtree" + assert children[6].name == "Always Included" + + grand_children = children[4].children + assert grand_children[0].name == "Feature 1" + assert grand_children[1].name == "Feature 2" + assert grand_children[2].name == "Feature 3" + + grand_children = children[5].children + assert grand_children[0].name == "Subtree Feature 1" + assert grand_children[1].name == "Subtree Feature 2" + assert grand_children[2].name == "Subtree Feature 3"