py_trees.parsers.behaviour_tree_xml module
XML parser for the BehaviorTree format.
Note
The parser is experimental and its API may change between releases.
This module provides a parser for the BehaviorTree XML format, used to construct behaviour trees with key remapping and subtree instantiation.
Overview
The parser recursively builds a behaviour tree from an XML file, using a remapping table to track key assignments
and substitutions. The remapping table is a dictionary that maps keys (referenced in curly braces in the XML,
e.g. {key}) to either their absolute paths (e.g. /some/key) or to other keys
(e.g. {other_key}), which are then further resolved to absolute paths.
In the end, all keys in the tree map to absolute paths which can be used to address the value in a map, blackboard, or similar structure.
Remapping table example:
{
"absolute_value": "/absolute/path",
"curly_reference": "{absolute_value}",
}
Keys are resolved recursively until an absolute path is found. This allows flexible wiring of data flow between nodes and subtrees.
Subtree templates and instantiation
Subtrees are defined as <BehaviorTree ID="..."> elements in the XML. These act as templates, which can be
instantiated elsewhere in the tree using a <SubTree> tag. When a subtree is instantiated, the parser:
Makes a local remapping table by applying any remappings specified in the
<SubTree>tag.Recursively parses the referenced subtree template, using the updated remapping table and a new namespace.
Example subtree template:
<BehaviorTree ID="MySubtree">
<Sequence>
<Reader name="MyReader" input="{input_key}" />
<Writer name="MyInternalWriter" output="{transfer_key}" />
<Reader name="MyInternalReader" input="{transfer_key}" />
</Sequence>
</BehaviorTree>
Example main tree instantiating a subtree:
<BehaviorTree ID="MainTree">
<Writer output="{some_key}" name="WriterMain" />
<SubTree ID="MySubtree" name="Subtree1" input_key="{some_key}"/>
</BehaviorTree>
This example will map {some_key} to /some_key in the remapping table (the root namespace is /),
and when the MySubtree is instantiated, it will create a new namespace /Subtree1 where:
{input_key}resolves to/some_key{transfer_key}(which is not remapped in theSubTreetag) resolves to/Subtree1/transfer_key— so a new key is created for the subtree.
A good documentation of remapping and subtrees can be found on the BT.CPP documentation.
Parsing walkthrough
Given the above example, parsing proceeds as follows:
The parser starts at the
MainTreewith an empty remapping table and namespace/.It encounters the
Writernode, which uses{some_key}. Since this key is new, it is mapped to/some_keyin the remapping table.The
SubTreenode is encountered. The parser:Copies the current remapping table.
Adds
input_key -> {some_key}to the remapping table for the subtree.Sets the namespace to
/Subtree1.Recursively parses the
MySubtreetemplate.
Inside
MySubtree:The
Readernode uses{input_key}, which resolves (via remapping) to/some_key.The
Writernode uses{transfer_key}, which is new, so it is mapped to/Subtree1/transfer_key.The second
Readernode uses{transfer_key}, which now resolves to/Subtree1/transfer_key.
Key concepts
Remapping table: Tracks how keys in curly braces are resolved to absolute paths or to other keys.
Namespace: Each subtree instantiation gets its own namespace, ensuring keys are scoped and do not collide.
Subtree instantiation: Subtrees are templates; instantiating them is like copy-pasting their structure, but with remapped keys and a new namespace.
For more details, see the code and the accompanying tests in test_ports_xml_parser.py.
- py_trees.parsers.behaviour_tree_xml.add_new_key_to_remapping_table(value: str, remapping_table: dict[str, str], subtree_namespace: str) None
Add the value to the remapping table, if it is a key itself.
If we encounter a remapping in a
SubTreeorPortsMixin-derived tag, e.g. remapped_key={other_key} and the value (i.e. {other_key}) is a key itself, and this key is not yet in the remapping table, then it means that in the current subtree namespace, we have encountered this key for the first time.Example:
` <SubTree ID="subtree1" in="{other_key}"/> `The first time we parse a tag which has a key in the assigned value, and the key (i.e. {other_key}) is not yet in the remapping table, it needs to be added, within the scope of the subtree namespace.
- Args:
value (str): The value of the remapping, e.g. {other_key} in the statement remapped_key={other_key}. remapping_table (dict[str, str]): The remapping table. subtree_namespace (str): The current subtree namespace.
- py_trees.parsers.behaviour_tree_xml.build_bt_index(root: Element) dict[str, Element]
Return
{ID: element}for every<BehaviorTree>child of root.
- py_trees.parsers.behaviour_tree_xml.build_port_remappings(elem: ~xml.etree.ElementTree.Element, class_: type[~py_trees.ports.PortsMixin], remapping_table: dict[str, str], subtree_namespace: str, logger: ~py_trees.ports_utils.PortsLogger = <py_trees.ports_utils._NoOpLogger object>) dict[str, str]
Build
{port_name -> absolute_key}for anyPortsMixinnode from XML attributes.Mirrors the logic used for
PortsMixinleaves:Attributes must correspond to declared input/output ports (otherwise
NotImplementedError).Each attribute value may be a
"{key}"reference or a direct value; both are resolved to absolute keys via the remapping table (adding entries as needed).If the natural in-namespace key (
/{ns}/{port}) differs from the resolved absolute key, a remapping is recorded.
- py_trees.parsers.behaviour_tree_xml.build_subtree_remapping(elem: ~xml.etree.ElementTree.Element, remapping_table: dict[str, str], parent_namespace: str, logger: ~py_trees.ports_utils.PortsLogger = <py_trees.ports_utils._NoOpLogger object>) dict[str, str]
Process the <SubTree> XML element.
The remapping table is updated to include the remappings from the <subtreeplus> or <subtree> element. The subtree is then instantiated with the new remapping table.
- Args:
elem: The <SubTree> (or <SubTreePlus>) XML element. remapping_table: Parent remapping table (logical name -> absolute key). parent_namespace: Absolute namespace of the parent tree. logger: Optional logger.
- Returns:
dict[str, str]: The local remapping for this subtree’s ports.
- Raises:
ValueError: If a referenced key is missing or a value can’t be resolved. RuntimeError: If cyclic remapping is detected during resolution.
- py_trees.parsers.behaviour_tree_xml.build_tree_from_xml(elem: ~xml.etree.ElementTree.Element, remapping_table: dict[str, str], init_lookup: dict, bt_index: dict, logger: ~py_trees.ports_utils.PortsLogger = <py_trees.ports_utils._NoOpLogger object>, subtree_namespace: str = '/', parent_names_str: str = '') Behaviour
Recursively build the tree from XML element.
- Args:
elem: XML element remapping_table (dict[str, str]): Remapping table. init_lookup (dict): Mapping from class names (str) to callables (constructors or partials)
that return
PortsMixin-derived instances.bt_index (dict[str, BehaviorTree]): dictionary {ID: BehaviorTree element} for subtree lookup. subtree_namespace (str): current blackboard namespace. logger: Optional logger-like object. parent_names_str (str): Dot-separated string of parent names for logging context and generating node names.
- Returns:
py_trees.behaviour.Behaviour
- Raises:
NotImplementedError: For unknown composite tags or unsupported ports. ValueError: For missing trees or unsupported tags. AssertionError: If a <BehaviorTree> does not have exactly one child.
- py_trees.parsers.behaviour_tree_xml.get_absolute_reference(value: str, subtree_namespace: str) str
Get the absolute path of a value (e.g. a key) by prepending the namespace.
- Args:
value (str): The value to make absolute. subtree_namespace (str): The current subtree namespace.
- Returns:
str: The absolute reference to the value.
- py_trees.parsers.behaviour_tree_xml.get_class_from_init_lookup(class_name: str, init_lookup: dict) type[PortsMixin]
Get the class from the init_lookup dictionary, ensuring it is a subclass of
PortsMixin.- Args:
class_name (str): The name of the class to look up. init_lookup (dict): A dictionary mapping class names to class constructors or partial callables,
e.g.
{"Producer": Producer, "Consumer": partial(Consumer, name=name)}.- Returns:
The class (subclass of
PortsMixin).- Raises:
KeyError: If the class name is not found in the init_lookup. TypeError: If the entry is not a class or partial callable,
or if the class is not a subclass of
PortsMixin.
- py_trees.parsers.behaviour_tree_xml.get_key_name(value: str) str
Extract the key name from a
{key}reference string.
- py_trees.parsers.behaviour_tree_xml.instantiate_ports_node(elem: ~xml.etree.ElementTree.Element, init_lookup: dict, remapping_table: dict[str, str], subtree_namespace: str, logger: ~py_trees.ports_utils.PortsLogger = <py_trees.ports_utils._NoOpLogger object>, constructor_kwargs: dict | None = None, parent_names_str: str = '') PortsMixin
Instantiate any PortsMixin-based node (leaf or composite).
looks up the class via init_lookup (validated with get_class_from_init_lookup)
builds port remappings from elem.attrib
constructs the instance (using name attribute or class_name)
calls setup_ports(…)
- Args:
elem: The XML element to parse. init_lookup (dict): Mapping from class names (str) to callables (constructors or partials)
that return
PortsMixin-derived instances.remapping_table (dict): Mapping from keys (str) to absolute keys (str). subtree_namespace (str): The namespace for this subtree. logger: Optional logger-like object. constructor_kwargs (dict | None): Additional keyword arguments to pass to the constructor. parent_names_str (str): Dot-separated string of parent names for logging context and generating node names.
- Returns:
PortsMixin: the fully initialised node.
- py_trees.parsers.behaviour_tree_xml.is_key(value: str) Match | None
Return a regex match if value is a
{key}reference.
- py_trees.parsers.behaviour_tree_xml.parse_behaviour_tree_xml(xml_file: str, main_tree_id: str | None = None, init_lookup: dict | None = None, logger: PortsLogger | None = None, search_paths: list[str] | None = None) Behaviour
Parse the XML file and build the behavior tree.
- Supports simple top-level imports via:
<Import src=”other.xml”/> <Include file=”other.xml”/>
The import pre-pass inlines all <BehaviorTree> elements from the referenced XMLs into the current document. If any imported BehaviorTree ID already exists, a ValueError is raised.
- Args:
xml_file (str): Path to the main XML file. main_tree_id (str | None): ID of the tree to execute; if None, read from ‘main_tree_to_execute’. init_lookup (dict): Mapping from tag -> constructor/partial for PortsMixin nodes (required). logger (PortsLogger | None): Optional logger (NoOp if None). search_paths (list[str] | None): Optional extra directories to resolve imports.
- Returns:
The root py_trees.behaviour.Behaviour for the requested tree.
- Raises:
ValueError: If init_lookup is missing or the main BehaviorTree ID is not found. FileNotFoundError / RuntimeError: From the import pre-pass if relevant.
- py_trees.parsers.behaviour_tree_xml.resolve_direct_value_remapping(key: str, remapping_table: dict[str, str]) str
Obtain a direct value from the remapping table.
- Args:
key: The direct value. remapping_table: A mapping of prefixed keys to their resolved values.
- Returns:
The resolved value from the remapping table.
- Raises:
ValueError: If the prefixed key is not found in the remapping table or is of invalid format.
- py_trees.parsers.behaviour_tree_xml.resolve_key_remapping(key: str, remapping_table: dict[str, str]) str
Recursively resolve a key through the remapping table until it is not a curly-brace key.
Cyclic remappings are explicitly checked and will raise a RuntimeError if detected.
- Args:
key: Input key. Expected to be either a curly-brace key or an absolute key. remapping_table: Maps logical keys to absolute keys.
- Returns:
str: The resolved absolute key.
- Raises:
ValueError: If the key can’t be properly resolved. RuntimeError: If a cyclic remapping is detected.