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 diff --git a/README.md b/README.md index 5836159..ce72d49 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,21 @@ and finally in subtree2 ``` +### Conditionals + +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 +like + +```xml + + + + +``` + ### Logging By default the parser uses `rclpy.logging` module to log messages. However, if this is not available 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/py_trees_parser/parser.py b/py_trees_parser/parser.py index 8c2538f..0ceaaca 100644 --- a/py_trees_parser/parser.py +++ b/py_trees_parser/parser.py @@ -378,6 +378,10 @@ 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"] + self.logger.debug(f"Found {node_type}") # name is a special attribute that is handled separately @@ -453,6 +457,20 @@ def _sub_args(self, args, var): return None + def _condition(self, condition): + if condition is None: + return True + + 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) + 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 +521,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/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)]), diff --git a/test/data/test_conditional.xml b/test/data/test_conditional.xml new file mode 100644 index 0000000..c52ae7f --- /dev/null +++ b/test/data/test_conditional.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/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 3e4e9e8..45c2f41 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -47,13 +47,13 @@ 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() 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 @@ -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,37 @@ 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 == "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"