Skip to content

Commit 569a3b3

Browse files
committed
Support decorators and composites with ports
1 parent 115fced commit 569a3b3

2 files changed

Lines changed: 151 additions & 27 deletions

File tree

py_trees/parsers/behaviour_tree_xml.py

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,7 @@ def build_port_remappings(
574574

575575
def instantiate_ports_node(
576576
elem: ET.Element,
577+
cls: type[PortsMixin],
577578
node_registry: dict,
578579
remapping_table: dict[str, str],
579580
subtree_namespace: str,
@@ -591,6 +592,7 @@ def instantiate_ports_node(
591592
592593
Args:
593594
elem: The XML element to parse.
595+
cls (type[py_trees.behavious.Behaviour]): A behaviour class type.
594596
node_registry (dict): Mapping from class names (str) to callables (constructors or partials)
595597
that return ``PortsMixin``-derived instances.
596598
remapping_table (dict): Mapping from keys (str) to absolute keys (str).
@@ -602,15 +604,6 @@ def instantiate_ports_node(
602604
Returns:
603605
PortsMixin: the fully initialised node.
604606
"""
605-
portsmixin_name = elem.tag
606-
# Get the class definition from node_registry - needed to check the ports.
607-
try:
608-
cls = get_class_from_registry(portsmixin_name, node_registry)
609-
except KeyError as e:
610-
raise NotImplementedError(
611-
f"Class name '{portsmixin_name}' not found in node_registry. Supporting other types is still TODO."
612-
) from e
613-
614607
port_remappings = build_port_remappings(
615608
elem=elem,
616609
class_=cls,
@@ -621,22 +614,19 @@ def instantiate_ports_node(
621614

622615
instance_name = generate_node_name(
623616
explicit_name=elem.attrib.get("name", None),
624-
general_name=portsmixin_name,
617+
general_name=elem.tag,
625618
prefix=parent_names_str,
626619
)
627620
logger.debug(f"PortsMixin node '{instance_name}' in namespace {subtree_namespace} remappings: {port_remappings}")
628621

629-
if portsmixin_name not in node_registry:
630-
raise ValueError(f"PortsMixin class '{portsmixin_name}' not found in node_registry table")
631-
632622
constructor_kwargs = constructor_kwargs or {} # create constructor_kwargs
633623
for attrib_key, attrib_value in elem.attrib.items():
634624
if attrib_key == "name":
635625
continue
636626
if attrib_key not in cls.input_ports() and attrib_key not in cls.output_ports():
637627
if is_key(attrib_value):
638628
raise ValueError(
639-
f"'{portsmixin_name}'(name='{instance_name}'): Port remappings are "
629+
f"'{elem.tag}'(name='{instance_name}'): Port remappings are "
640630
f"not supported for non-port attributes ('{attrib_key}'='{attrib_value}'). "
641631
)
642632
# Consistency check: if constructor_kwargs already has this key, and it's conflicting,
@@ -654,7 +644,7 @@ def instantiate_ports_node(
654644
)
655645
constructor_kwargs[attrib_key] = attrib_value
656646

657-
ctor_callable = node_registry[portsmixin_name]
647+
ctor_callable = node_registry[elem.tag]
658648
# Try to convert the constructor arguments to the correct type.
659649
ignore_keys = {"child", "children", "behaviour_class_name"}
660650
constructor_kwargs, success = apply_type_hints(ctor_callable, constructor_kwargs, logger=logger, ignore=ignore_keys)
@@ -665,14 +655,14 @@ def instantiate_ports_node(
665655
)
666656
try:
667657
# Pass the behaviour_class_name (the tag/registry name) to the constructor
668-
node: PortsMixin = node_registry[portsmixin_name](
658+
node: PortsMixin = ctor_callable(
669659
name=instance_name,
670-
behaviour_class_name=portsmixin_name,
660+
behaviour_class_name=elem.tag,
671661
**constructor_kwargs,
672662
)
673663
except Exception as e: # Catch everything that may go wrong in the constructor
674664
raise XMLParserError(
675-
f"Failed to instantiate '{portsmixin_name}' with args {constructor_kwargs} "
665+
f"Failed to instantiate '{elem.tag}' with args {constructor_kwargs} "
676666
f"(check: does the instantiated class have kwargs in __init__?): {e}"
677667
) from e
678668

@@ -718,24 +708,73 @@ def build_tree_from_xml(
718708
logger.debug(f"Processing tag: '{elem.tag}' with attributes {elem.attrib}. Remapping table: {remapping_table}")
719709
if elem.tag in node_registry:
720710
# Prioritize nodes that are in the registry, which could even be user overrides for built-ins.
721-
# This is any leaf PortsMixin node (any PortsMixin + Behaviour combination).
722-
logger.debug(f"Creating PortsMixin leaf node for tag {elem.tag}.")
711+
# This is any PortsMixin node (any PortsMixin + Behaviour combination).
712+
try:
713+
cls = get_class_from_registry(elem.tag, node_registry)
714+
except KeyError as e:
715+
raise NotImplementedError(
716+
f"Class name '{tag}' not found in node_registry. Supporting other types is still TODO."
717+
) from e
718+
719+
if not (issubclass(cls, PortsMixin) and issubclass(cls, py_trees.behaviour.Behaviour)):
720+
raise TypeError(
721+
f"XML tag '{elem.tag}' did not instantiate a PortsMixin + "
722+
f"py_trees.behaviour.Behaviour; got {cls.__name__}"
723+
)
724+
725+
children = []
726+
constructor_kwargs: dict | None = None
727+
is_decorator = issubclass(cls, py_trees.decorators.Decorator)
728+
is_composite = issubclass(cls, py_trees.composites.Composite)
729+
if is_decorator and len(elem) != 1:
730+
raise XMLParserError(f"Decorator node '{tag}' must have exactly 1 child, found {len(elem)}.")
731+
if is_decorator or is_composite:
732+
for child in elem:
733+
# Use no general name fallback for the parent_names_str to pass to the children.
734+
# Otherwise, the generated node names will get too long and not very readable.
735+
# It also means that we lose the guarantee of unique names, but that can be avoided by
736+
# assigning an explicit name to the parent tags.
737+
concise_parent_names_str = generate_node_name(
738+
explicit_name=elem.attrib.get("name", None),
739+
general_name="",
740+
prefix=parent_names_str,
741+
no_uuid=True,
742+
)
743+
child_node = build_tree_from_xml(
744+
child,
745+
remapping_table,
746+
node_registry,
747+
bt_index,
748+
logger=logger,
749+
subtree_namespace=subtree_namespace,
750+
parent_names_str=concise_parent_names_str,
751+
)
752+
if not isinstance(child_node, py_trees.behaviour.Behaviour):
753+
raise TypeError(f"Child node of type {type(child_node).__name__} is not a valid py_trees Behavior.")
754+
children.append(child_node)
755+
756+
if is_decorator:
757+
constructor_kwargs = {"child": children[0]} # already checked its length
758+
elif is_composite:
759+
constructor_kwargs = {"children": children}
760+
761+
logger.debug(f"Creating PortsMixin node for tag {elem.tag}.")
723762
node = instantiate_ports_node(
724763
elem=elem,
764+
cls=cls,
725765
node_registry=node_registry,
726766
remapping_table=remapping_table,
727767
subtree_namespace=subtree_namespace,
728768
logger=logger,
769+
constructor_kwargs=constructor_kwargs,
729770
parent_names_str=parent_names_str,
730771
)
731-
if not (isinstance(node, PortsMixin) and isinstance(node, py_trees.behaviour.Behaviour)):
732-
raise TypeError(
733-
f"XML tag '{elem.tag}' did not instantiate a PortsMixin + "
734-
f"py_trees.behaviour.Behaviour; got {type(node).__name__}"
735-
)
736772
return node
773+
737774
elif tag in PARENT_NODES_TAGS:
738-
# Composite/Decorator nodes which can have children:
775+
# Composite/Decorator nodes which can have children.
776+
# TODO: Everything in this section should be eventually replaced with actual
777+
# built-in behaviours with ports that are part of the node registry.
739778
node_name = generate_node_name(
740779
explicit_name=elem.attrib.get("name", None),
741780
general_name=elem.tag,

tests/test_ports_xml_parser.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import py_trees
2121
from py_trees.parsers.behaviour_tree_xml import is_key, parse_behaviour_tree_xml
22-
from py_trees.ports import BehaviourWithPorts, PortInformation, get_ports_registry
22+
from py_trees.ports import BehaviourWithPorts, PortInformation, PortsMixin, get_ports_registry
2323
from py_trees.ports_utils import (
2424
find_node_by_class,
2525
find_node_by_name,
@@ -664,6 +664,91 @@ def test_decorator_node(self) -> None:
664664
finally:
665665
os.unlink(path)
666666

667+
def test_decorator_ports_node(self) -> None:
668+
"""Verify that a decorator with ports is instantiated correctly."""
669+
670+
class RepeatWithPorts(PortsMixin, py_trees.decorators.Repeat):
671+
"""A `py_trees.decorators.Repeat` decorator that also exposes ports."""
672+
673+
@classmethod
674+
def input_ports(cls) -> dict:
675+
return {}
676+
677+
@classmethod
678+
def output_ports(cls) -> dict:
679+
return {"count": PortInformation(data_type=int, required=False)}
680+
681+
xml = """<root main_tree_to_execute="MainTree">
682+
<BehaviorTree ID="MainTree">
683+
<Sequence>
684+
<RepeatWithPorts num_success="5">
685+
<Producer name="prod" output="{out}" />
686+
</RepeatWithPorts>
687+
</Sequence>
688+
</BehaviorTree>
689+
</root>"""
690+
691+
with tempfile.NamedTemporaryFile(delete=False, mode="w", suffix=".xml") as tf:
692+
tf.write(xml)
693+
path = tf.name
694+
695+
try:
696+
root = parse_behaviour_tree_xml(path, logger=StdoutLogger())
697+
repeat = find_node_by_class(root, RepeatWithPorts)
698+
self.assertIsNotNone(repeat)
699+
self.assertEqual(repeat.num_success, 5)
700+
self.assertIsInstance(repeat.num_success, int)
701+
self.assertIsInstance(repeat.decorated, Producer)
702+
703+
# num_success=-1 repeats indefinitely, so the decorator stays RUNNING.
704+
btree = py_trees.trees.BehaviourTree(root)
705+
btree.tick()
706+
self.assertEqual(repeat.status, py_trees.common.Status.RUNNING)
707+
finally:
708+
os.unlink(path)
709+
710+
def test_composite_ports_node(self) -> None:
711+
"""Verify that a decorator with ports is instantiated correctly."""
712+
713+
class SequenceWithPorts(PortsMixin, py_trees.composites.Sequence):
714+
"""A `py_trees.composites.Sequence` composite that also exposes ports."""
715+
716+
@classmethod
717+
def input_ports(cls) -> dict:
718+
return {}
719+
720+
@classmethod
721+
def output_ports(cls) -> dict:
722+
return {}
723+
724+
xml = """<root main_tree_to_execute="MainTree">
725+
<BehaviorTree ID="MainTree">
726+
<SequenceWithPorts memory="true">
727+
<Producer name="prod1" output="{out1}" />
728+
<Producer name="prod2" output="{out2}" />
729+
</SequenceWithPorts>
730+
</BehaviorTree>
731+
</root>"""
732+
733+
with tempfile.NamedTemporaryFile(delete=False, mode="w", suffix=".xml") as tf:
734+
tf.write(xml)
735+
path = tf.name
736+
737+
try:
738+
root = parse_behaviour_tree_xml(path, logger=StdoutLogger())
739+
sequence = find_node_by_class(root, SequenceWithPorts)
740+
self.assertIsNotNone(sequence)
741+
self.assertIsInstance(sequence.children, list)
742+
self.assertEqual(len(sequence.children), 2)
743+
self.assertIsInstance(sequence.children[0], Producer)
744+
self.assertIsInstance(sequence.children[1], Producer)
745+
746+
btree = py_trees.trees.BehaviourTree(root)
747+
btree.tick()
748+
self.assertEqual(sequence.status, py_trees.common.Status.SUCCESS)
749+
finally:
750+
os.unlink(path)
751+
667752
def test_direct_portsmixin_leaf_accepted(self) -> None:
668753
"""
669754
A leaf class that is ``PortsMixin + Behaviour`` (not via ``BehaviourWithPorts``)

0 commit comments

Comments
 (0)