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 the SubTree tag) 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:

  1. The parser starts at the MainTree with an empty remapping table and namespace /.

  2. It encounters the Writer node, which uses {some_key}. Since this key is new, it is mapped to /some_key in the remapping table.

  3. The SubTree node 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 MySubtree template.

  4. Inside MySubtree:

    • The Reader node uses {input_key}, which resolves (via remapping) to /some_key.

    • The Writer node uses {transfer_key}, which is new, so it is mapped to /Subtree1/transfer_key.

    • The second Reader node 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 SubTree or PortsMixin-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 any PortsMixin node from XML attributes.

Mirrors the logic used for PortsMixin leaves:

  • 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.