Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,21 @@ and finally in subtree2
</py_trees.composites.Sequence>
```

### 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
<py_trees.behaviours.Success name="Feature 1" if="True"/>
<py_trees.behaviours.Success name="Feature 2" if="False"/>
<py_trees.behaviours.Success name="Feature 3" if="${my_arg1} < 5"/>
<py_trees.behaviours.Success name="Feature 4" if="'${my_arg2}' == 'simulation'"/>
```

### Logging

By default the parser uses `rclpy.logging` module to log messages. However, if this is not available
Expand Down
2 changes: 1 addition & 1 deletion package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>py_trees_parser</name>
<version>0.6.0</version>
<version>0.6.1</version>
<description>A py_trees xml parser</description>
<maintainer email="e.l.f.foster@tudelft.nl">Erich L Foster</maintainer>
<license>Apache-2.0</license>
Expand Down
21 changes: 21 additions & 0 deletions py_trees_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]),
Expand Down
44 changes: 44 additions & 0 deletions test/data/test_conditional.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<py_trees.composites.Selector name="Conditional Test" memory="False">
<!-- Basic condition: equal comparison -->
<py_trees.behaviours.Success name="Advanced Mode Feature" if="'${mode}' == 'advanced'" />

<!-- Basic condition: not equal comparison -->
<py_trees.behaviours.Running name="Non-Basic Mode Feature" if="'${mode}' != 'basic'" />

<!-- Numeric comparison -->
<py_trees.behaviours.Periodic name="High Level Feature" n="3" if="${level} > 5" />

<!-- Boolean condition -->
<py_trees.behaviours.Success name="Debug Feature" if="${enable_debug}"/>
<py_trees.behaviours.Success name="Not Debug Feature" if="not ${enable_debug}"/>

<!-- Multiple children in a conditional -->
<py_trees.composites.Sequence name="Conditional Selector" if="${include_all}" memory="True">
<py_trees.behaviours.Success name="Feature 1" />
<py_trees.behaviours.Success name="Feature 2" />
<py_trees.behaviours.Success name="Feature 3" />
</py_trees.composites.Sequence>

<py_trees.composites.Sequence name="Not Conditional Selector" if="not ${include_all}" memory="True">
<py_trees.behaviours.Success name="Feature 4" />
<py_trees.behaviours.Success name="Feature 5" />
<py_trees.behaviours.Success name="Feature 6" />
</py_trees.composites.Sequence>

<subtree name="Included Subtree" if="${include_all}" include="$(ament_index_python.get_package_share_directory('py_trees_parser') + '/test/data/test_conditional_subtree.xml')" >
<arg name="name" value="Included Subtree" />
<arg name="feature_1" value="Subtree Feature 1"/>
<arg name="feature_2" value="Subtree Feature 2"/>
<arg name="feature_3" value="Subtree Feature 3"/>
</subtree>

<subtree name="Not Included Subtree" if="not ${include_all}" include="$(ament_index_python.get_package_share_directory('py_trees_parser') + '/test/data/test_conditional_subtree.xml')" >
<arg name="name" value="Included Subtree" />
<arg name="feature_1" value="Subtree Feature 4"/>
<arg name="feature_2" value="Subtree Feature 5"/>
<arg name="feature_3" value="Subtree Feature 6"/>
</subtree>

<!-- Always included node -->
<py_trees.behaviours.Success name="Always Included" />
</py_trees.composites.Selector>
6 changes: 6 additions & 0 deletions test/data/test_conditional_main.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<subtree name="test_conditional" include="$(os.path.join(ament_index_python.get_package_share_directory('py_trees_parser'), 'test', 'data', 'test_conditional.xml'))" >
<arg name="mode" value="advanced" />
<arg name="level" value="10" />
<arg name="enable_debug" value="True" />
<arg name="include_all" value="True" />
</subtree>
7 changes: 7 additions & 0 deletions test/data/test_conditional_subtree.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- Multiple children in a conditional -->
<py_trees.composites.Sequence name="${name}" memory="True">
<py_trees.behaviours.Success name="${feature_1}" />
<py_trees.behaviours.Success name="${feature_2}" />
<py_trees.behaviours.Success name="${feature_3}" />
</py_trees.composites.Sequence>

51 changes: 39 additions & 12 deletions test/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Loading