Skip to content

Commit 163ab1a

Browse files
committed
[decorators] finally behaviour
1 parent 6e97964 commit 163ab1a

11 files changed

+344
-2
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Release Notes
33

44
Forthcoming
55
-----------
6-
* ...
6+
* [decorators] a finally-style decorator, `#427 <https://github.com/splintered-reality/py_trees/pull/427>`_
77

88
2.2.3 (2023-02-08)
99
------------------

docs/demos.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,22 @@ py-trees-demo-eternal-guard
147147
:linenos:
148148
:caption: py_trees/demos/eternal_guard.py
149149

150+
.. _py-trees-demo-finally-program:
151+
152+
py-trees-demo-finally
153+
---------------------
154+
155+
.. automodule:: py_trees.demos.decorator_finally
156+
:members:
157+
:special-members:
158+
:show-inheritance:
159+
:synopsis: demo the finally-like decorator
160+
161+
.. literalinclude:: ../py_trees/demos/decorator_finally.py
162+
:language: python
163+
:linenos:
164+
:caption: py_trees/demos/decorator_finally.py
165+
150166
.. _py-trees-demo-logging-program:
151167

152168
py-trees-demo-logging

docs/dot/demo-finally.dot

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
digraph pastafarianism {
2+
ordering=out;
3+
graph [fontname="times-roman"];
4+
node [fontname="times-roman"];
5+
edge [fontname="times-roman"];
6+
root [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ root", shape=box, style=filled];
7+
SetFlagFalse [fillcolor=gray, fontcolor=black, fontsize=9, label=SetFlagFalse, shape=ellipse, style=filled];
8+
root -> SetFlagFalse;
9+
Parallel [fillcolor=gold, fontcolor=black, fontsize=9, label="Parallel\nSuccessOnOne", shape=parallelogram, style=filled];
10+
root -> Parallel;
11+
Counter [fillcolor=gray, fontcolor=black, fontsize=9, label=Counter, shape=ellipse, style=filled];
12+
Parallel -> Counter;
13+
Finally [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label=Finally, shape=ellipse, style=filled];
14+
Parallel -> Finally;
15+
SetFlagTrue [fillcolor=gray, fontcolor=black, fontsize=9, label=SetFlagTrue, shape=ellipse, style=filled];
16+
Finally -> SetFlagTrue;
17+
}

docs/images/finally.png

49.6 KB
Loading

py_trees/behaviour.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ def iterate(self, direct_descendants: bool = False) -> typing.Iterator[Behaviour
344344
yield child
345345
yield self
346346

347-
# TODO: better type refinement of 'viso=itor'
347+
# TODO: better type refinement of 'visitor'
348348
def visit(self, visitor: typing.Any) -> None:
349349
"""
350350
Introspect on this behaviour with a visitor.

py_trees/behaviours.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ def update(self) -> common.Status:
280280
:data:`~py_trees.common.Status.RUNNING` while not expired, the given completion status otherwise
281281
"""
282282
self.counter += 1
283+
self.feedback_message = f"count: {self.counter}"
283284
if self.counter <= self.duration:
284285
return common.Status.RUNNING
285286
else:

py_trees/decorators.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
* :class:`py_trees.decorators.Condition`
3737
* :class:`py_trees.decorators.Count`
3838
* :class:`py_trees.decorators.EternalGuard`
39+
* :class:`py_trees.decorators.Finally`
3940
* :class:`py_trees.decorators.Inverter`
4041
* :class:`py_trees.decorators.OneShot`
4142
* :class:`py_trees.decorators.Repeat`
@@ -920,3 +921,90 @@ def update(self) -> common.Status:
920921
the behaviour's new status :class:`~py_trees.common.Status`
921922
"""
922923
return self.decorated.status
924+
925+
926+
class Finally(Decorator):
927+
"""
928+
The py_trees equivalent of python's 'finally' keyword.
929+
930+
Always return :data:`~py_trees.common.Status.RUNNING` and
931+
on :meth:`terminate`, call the child's :meth:`~py_trees.behaviour.Behaviour.update`
932+
method, once. The return status of the child is unused as both decorator
933+
and child will be in the process of terminating with status
934+
:data:`~py_trees.common.Status.INVALID`.
935+
936+
This decorator is usually used underneath a parallel with a sibling
937+
that represents the 'try' part of the behaviour.
938+
939+
.. code-block::
940+
941+
/_/ Parallel
942+
--> Work
943+
-^- Finally (Decorator)
944+
--> Finally (Implementation)
945+
946+
.. seealso:: :ref:`py-trees-demo-finally-program`
947+
948+
NB: If you need to persist the execution of the 'finally'-like block for more
949+
than a single tick, you'll need to build that explicitly into your tree. There
950+
are various ways of doing so (with and without the blackboard). One pattern
951+
that works:
952+
953+
.. code-block::
954+
955+
[o] Selector
956+
{-} Sequence
957+
--> Work
958+
--> Finally (Triggers on Success)
959+
{-} Sequence
960+
--> Finally (Triggers on Failure)
961+
--> Failure
962+
"""
963+
964+
def __init__(self, name: str, child: behaviour.Behaviour):
965+
"""
966+
Initialise with the standard decorator arguments.
967+
968+
Args:
969+
name: the decorator name
970+
child: the child to be decorated
971+
"""
972+
super(Finally, self).__init__(name=name, child=child)
973+
974+
def tick(self) -> typing.Iterator[behaviour.Behaviour]:
975+
"""
976+
Bypass the child when ticking.
977+
978+
Yields:
979+
a reference to itself
980+
"""
981+
self.logger.debug(f"{self.__class__.__name__}.tick()")
982+
self.status = self.update()
983+
yield self
984+
985+
def update(self):
986+
"""
987+
Always :data:`~py_trees.common.Status.RUNNING`.
988+
989+
Returns:
990+
the behaviour's new status :class:`~py_trees.common.Status`
991+
"""
992+
return common.Status.RUNNING
993+
994+
def terminate(self, new_status: common.Status) -> None:
995+
"""
996+
Finally, tick the child behaviour once.
997+
"""
998+
self.logger.debug(
999+
"{}.terminate({})".format(
1000+
self.__class__.__name__,
1001+
"{}->{}".format(self.status, new_status)
1002+
if self.status != new_status
1003+
else f"{new_status}",
1004+
)
1005+
)
1006+
if new_status == common.Status.INVALID:
1007+
self.decorated.tick_once()
1008+
# Do not need to stop the child here - this method
1009+
# is only called by Decorator.stop() which will handle
1010+
# that responsibility immediately after this method returns.

py_trees/demos/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from . import display_modes # usort:skip # noqa: F401
2222
from . import dot_graphs # usort:skip # noqa: F401
2323
from . import either_or # usort:skip # noqa: F401
24+
from . import decorator_finally # usort:skip # noqa: F401
2425
from . import lifecycle # usort:skip # noqa: F401
2526
from . import selector # usort:skip # noqa: F401
2627
from . import sequence # usort:skip # noqa: F401

py_trees/demos/decorator_finally.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#!/usr/bin/env python
2+
#
3+
# License: BSD
4+
# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
5+
#
6+
##############################################################################
7+
# Documentation
8+
##############################################################################
9+
10+
"""
11+
Trigger a 'finally'-like behaviour with the
12+
:class:`~py_trees.decorators.Finally` decorator.
13+
14+
.. argparse::
15+
:module: py_trees.demos.decorator_finally
16+
:func: command_line_argument_parser
17+
:prog: py-trees-demo-finally
18+
19+
.. graphviz:: dot/demo-finally.dot
20+
21+
.. image:: images/finally.png
22+
23+
"""
24+
25+
##############################################################################
26+
# Imports
27+
##############################################################################
28+
29+
import argparse
30+
import sys
31+
import typing
32+
33+
import py_trees
34+
import py_trees.console as console
35+
36+
##############################################################################
37+
# Classes
38+
##############################################################################
39+
40+
41+
def description(root: py_trees.behaviour.Behaviour) -> str:
42+
"""
43+
Print description and usage information about the program.
44+
45+
Returns:
46+
the program description string
47+
"""
48+
content = (
49+
"Trigger python-like 'finally' behaviour with the 'Finally' decorator.\n\n"
50+
)
51+
content += "A blackboard flag is set to false prior to commencing work. \n"
52+
content += "Once the work terminates, the decorator and it's child\n"
53+
content += "child will also terminate and toggle the flag to true.\n"
54+
content += "\n"
55+
content += "The demonstration is run twice - on the first occasion\n"
56+
content += "the work terminates with SUCCESS and on the second, it\n"
57+
content + "terminates with FAILURE\n"
58+
content += "\n"
59+
content += "EVENTS\n"
60+
content += "\n"
61+
content += " - 1 : flag is set to false, worker is running\n"
62+
content += " - 2 : worker completes (with SUCCESS||FAILURE)\n"
63+
content += " - 2 : finally is triggered, flag is set to true\n"
64+
content += "\n"
65+
if py_trees.console.has_colours:
66+
banner_line = console.green + "*" * 79 + "\n" + console.reset
67+
s = banner_line
68+
s += console.bold_white + "Finally".center(79) + "\n" + console.reset
69+
s += banner_line
70+
s += "\n"
71+
s += content
72+
s += "\n"
73+
s += banner_line
74+
else:
75+
s = content
76+
return s
77+
78+
79+
def epilog() -> typing.Optional[str]:
80+
"""
81+
Print a noodly epilog for --help.
82+
83+
Returns:
84+
the noodly message
85+
"""
86+
if py_trees.console.has_colours:
87+
return (
88+
console.cyan
89+
+ "And his noodly appendage reached forth to tickle the blessed...\n"
90+
+ console.reset
91+
)
92+
else:
93+
return None
94+
95+
96+
def command_line_argument_parser() -> argparse.ArgumentParser:
97+
"""
98+
Process command line arguments.
99+
100+
Returns:
101+
the argument parser
102+
"""
103+
parser = argparse.ArgumentParser(
104+
description=description(create_root(py_trees.common.Status.SUCCESS)),
105+
epilog=epilog(),
106+
formatter_class=argparse.RawDescriptionHelpFormatter,
107+
)
108+
group = parser.add_mutually_exclusive_group()
109+
group.add_argument(
110+
"-r", "--render", action="store_true", help="render dot tree to file"
111+
)
112+
return parser
113+
114+
115+
def create_root(
116+
expected_work_termination_result: py_trees.common.Status,
117+
) -> py_trees.behaviour.Behaviour:
118+
"""
119+
Create the root behaviour and it's subtree.
120+
121+
Returns:
122+
the root behaviour
123+
"""
124+
root = py_trees.composites.Sequence(name="root", memory=True)
125+
set_flag_to_false = py_trees.behaviours.SetBlackboardVariable(
126+
name="SetFlagFalse",
127+
variable_name="flag",
128+
variable_value=False,
129+
overwrite=True,
130+
)
131+
set_flag_to_true = py_trees.behaviours.SetBlackboardVariable(
132+
name="SetFlagTrue", variable_name="flag", variable_value=True, overwrite=True
133+
)
134+
parallel = py_trees.composites.Parallel(
135+
name="Parallel",
136+
policy=py_trees.common.ParallelPolicy.SuccessOnOne(),
137+
children=[],
138+
)
139+
worker = py_trees.behaviours.TickCounter(
140+
name="Counter", duration=1, completion_status=expected_work_termination_result
141+
)
142+
well_finally = py_trees.decorators.Finally(name="Finally", child=set_flag_to_true)
143+
parallel.add_children([worker, well_finally])
144+
root.add_children([set_flag_to_false, parallel])
145+
return root
146+
147+
148+
##############################################################################
149+
# Main
150+
##############################################################################
151+
152+
153+
def main() -> None:
154+
"""Entry point for the demo script."""
155+
args = command_line_argument_parser().parse_args()
156+
# py_trees.logging.level = py_trees.logging.Level.DEBUG
157+
print(description(create_root(py_trees.common.Status.SUCCESS)))
158+
159+
####################
160+
# Rendering
161+
####################
162+
if args.render:
163+
py_trees.display.render_dot_tree(create_root(py_trees.common.Status.SUCCESS))
164+
sys.exit()
165+
166+
for status in (py_trees.common.Status.SUCCESS, py_trees.common.Status.FAILURE):
167+
py_trees.blackboard.Blackboard.clear()
168+
console.banner(f"Experiment - Terminate with {status}")
169+
root = create_root(status)
170+
root.tick_once()
171+
print(py_trees.display.unicode_tree(root=root, show_status=True))
172+
print(py_trees.display.unicode_blackboard())
173+
root.tick_once()
174+
print(py_trees.display.unicode_tree(root=root, show_status=True))
175+
print(py_trees.display.unicode_blackboard())
176+
177+
print("\n")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ py-trees-demo-display-modes = "py_trees.demos.display_modes:main"
6666
py-trees-demo-dot-graphs = "py_trees.demos.dot_graphs:main"
6767
py-trees-demo-either-or = "py_trees.demos.either_or:main"
6868
py-trees-demo-eternal-guard = "py_trees.demos.eternal_guard:main"
69+
py-trees-demo-finally = "py_trees.demos.decorator_finally:main"
6970
py-trees-demo-logging = "py_trees.demos.logging:main"
7071
py-trees-demo-pick-up-where-you-left-off = "py_trees.demos.pick_up_where_you_left_off:main"
7172
py-trees-demo-selector = "py_trees.demos.selector:main"

0 commit comments

Comments
 (0)