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"