Skip to content

Commit

Permalink
[decorators] finally behaviour
Browse files Browse the repository at this point in the history
  • Loading branch information
stonier committed Sep 5, 2023
1 parent 6e97964 commit 812686b
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 2 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Release Notes

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

2.2.3 (2023-02-08)
------------------
Expand Down
16 changes: 16 additions & 0 deletions docs/demos.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,22 @@ py-trees-demo-eternal-guard
:linenos:
:caption: py_trees/demos/eternal_guard.py

.. _py-trees-demo-finally-program:

py-trees-demo-finally
---------------------

.. automodule:: py_trees.demos.decorator_finally
:members:
:special-members:
:show-inheritance:
:synopsis: demo the finally-like decorator

.. literalinclude:: ../py_trees/demos/decorator_finally.py
:language: python
:linenos:
:caption: py_trees/demos/decorator_finally.py

.. _py-trees-demo-logging-program:

py-trees-demo-logging
Expand Down
17 changes: 17 additions & 0 deletions docs/dot/demo-finally.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
digraph pastafarianism {
ordering=out;
graph [fontname="times-roman"];
node [fontname="times-roman"];
edge [fontname="times-roman"];
root [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ root", shape=box, style=filled];
SetFlagFalse [fillcolor=gray, fontcolor=black, fontsize=9, label=SetFlagFalse, shape=ellipse, style=filled];
root -> SetFlagFalse;
Parallel [fillcolor=gold, fontcolor=black, fontsize=9, label="Parallel\nSuccessOnOne", shape=parallelogram, style=filled];
root -> Parallel;
Counter [fillcolor=gray, fontcolor=black, fontsize=9, label=Counter, shape=ellipse, style=filled];
Parallel -> Counter;
Finally [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label=Finally, shape=ellipse, style=filled];
Parallel -> Finally;
SetFlagTrue [fillcolor=gray, fontcolor=black, fontsize=9, label=SetFlagTrue, shape=ellipse, style=filled];
Finally -> SetFlagTrue;
}
Binary file added docs/images/finally.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion py_trees/behaviour.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def iterate(self, direct_descendants: bool = False) -> typing.Iterator[Behaviour
yield child
yield self

# TODO: better type refinement of 'viso=itor'
# TODO: better type refinement of 'visitor'
def visit(self, visitor: typing.Any) -> None:
"""
Introspect on this behaviour with a visitor.
Expand Down
1 change: 1 addition & 0 deletions py_trees/behaviours.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ def update(self) -> common.Status:
:data:`~py_trees.common.Status.RUNNING` while not expired, the given completion status otherwise
"""
self.counter += 1
self.feedback_message = f"count: {self.counter}"
if self.counter <= self.duration:
return common.Status.RUNNING
else:
Expand Down
87 changes: 87 additions & 0 deletions py_trees/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
* :class:`py_trees.decorators.Condition`
* :class:`py_trees.decorators.Count`
* :class:`py_trees.decorators.EternalGuard`
* :class:`py_trees.decorators.Finally`
* :class:`py_trees.decorators.Inverter`
* :class:`py_trees.decorators.OneShot`
* :class:`py_trees.decorators.Repeat`
Expand Down Expand Up @@ -920,3 +921,89 @@ def update(self) -> common.Status:
the behaviour's new status :class:`~py_trees.common.Status`
"""
return self.decorated.status


class Finally(Decorator):
"""
Implement a 'finally'-like pattern with behaviours.
This implements a pattern
Always return :data:`~py_trees.common.Status.RUNNING` and
on :meth:`terminate`, call the child's :meth:`~py_trees.behaviour.Behaviour.update`
method, once. The return status of the child is unused as both decorator
and child will be in the process of terminating with status
:data:`~py_trees.common.Status.INVALID`.
This decorator is usually used underneath a parallel with a sibling
that represents the 'try' part of the behaviour.
.. code-block::
/_/ Parallel
--> Work
-^- Finally (Decorator)
--> Finally (Implementation)
.. seealso:: :ref:`py-trees-demo-finally-program`
NB: If you need to persist the execution of the 'finally'-like block for more
than a single tick, you'll need to build that explicitly into your tree. There
are various ways of doing so (with and without the blackboard). One pattern
that works:
.. code-block::
[o] Selector
{-} Sequence
--> Work
--> Finally (Triggers on Success)
{-} Sequence
--> Finally (Triggers on Failure)
--> Failure
"""

def __init__(self, name: str, child: behaviour.Behaviour):
"""
Initialise with the standard decorator arguments.
Args:
name: the decorator name
child: the child to be decorated
"""
super(Finally, self).__init__(name=name, child=child)

def tick(self) -> typing.Iterator[behaviour.Behaviour]:
"""
Bypass the child when ticking.
Yields:
a reference to itself
"""
self.logger.debug(f"{self.__class__.__name__}.tick()")
self.status = self.update()
yield self

def update(self):
"""
Return with :data:`~py_trees.common.Status.RUNNING`.
Returns:
the behaviour's new status :class:`~py_trees.common.Status`
"""
return common.Status.RUNNING

def terminate(self, new_status: common.Status) -> None:
"""Tick the child behaviour once."""
self.logger.debug(
"{}.terminate({})".format(
self.__class__.__name__,
"{}->{}".format(self.status, new_status)
if self.status != new_status
else f"{new_status}",
)
)
if new_status == common.Status.INVALID:
self.decorated.tick_once()
# Do not need to stop the child here - this method
# is only called by Decorator.stop() which will handle
# that responsibility immediately after this method returns.
1 change: 1 addition & 0 deletions py_trees/demos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from . import display_modes # usort:skip # noqa: F401
from . import dot_graphs # usort:skip # noqa: F401
from . import either_or # usort:skip # noqa: F401
from . import decorator_finally # usort:skip # noqa: F401
from . import lifecycle # usort:skip # noqa: F401
from . import selector # usort:skip # noqa: F401
from . import sequence # usort:skip # noqa: F401
Expand Down
176 changes: 176 additions & 0 deletions py_trees/demos/decorator_finally.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env python
#
# License: BSD
# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE
#
##############################################################################
# Documentation
##############################################################################

"""
Trigger 'finally'-like behaviour with :class:`~py_trees.decorators.Finally`.
.. argparse::
:module: py_trees.demos.decorator_finally
:func: command_line_argument_parser
:prog: py-trees-demo-finally
.. graphviz:: dot/demo-finally.dot
.. image:: images/finally.png
"""

##############################################################################
# Imports
##############################################################################

import argparse
import sys
import typing

import py_trees
import py_trees.console as console

##############################################################################
# Classes
##############################################################################


def description(root: py_trees.behaviour.Behaviour) -> str:
"""
Print description and usage information about the program.
Returns:
the program description string
"""
content = (
"Trigger python-like 'finally' behaviour with the 'Finally' decorator.\n\n"
)
content += "A blackboard flag is set to false prior to commencing work. \n"
content += "Once the work terminates, the decorator and it's child\n"
content += "child will also terminate and toggle the flag to true.\n"
content += "\n"
content += "The demonstration is run twice - on the first occasion\n"
content += "the work terminates with SUCCESS and on the second, it\n"
content + "terminates with FAILURE\n"
content += "\n"
content += "EVENTS\n"
content += "\n"
content += " - 1 : flag is set to false, worker is running\n"
content += " - 2 : worker completes (with SUCCESS||FAILURE)\n"
content += " - 2 : finally is triggered, flag is set to true\n"
content += "\n"
if py_trees.console.has_colours:
banner_line = console.green + "*" * 79 + "\n" + console.reset
s = banner_line
s += console.bold_white + "Finally".center(79) + "\n" + console.reset
s += banner_line
s += "\n"
s += content
s += "\n"
s += banner_line
else:
s = content
return s


def epilog() -> typing.Optional[str]:
"""
Print a noodly epilog for --help.
Returns:
the noodly message
"""
if py_trees.console.has_colours:
return (
console.cyan
+ "And his noodly appendage reached forth to tickle the blessed...\n"
+ console.reset
)
else:
return None


def command_line_argument_parser() -> argparse.ArgumentParser:
"""
Process command line arguments.
Returns:
the argument parser
"""
parser = argparse.ArgumentParser(
description=description(create_root(py_trees.common.Status.SUCCESS)),
epilog=epilog(),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-r", "--render", action="store_true", help="render dot tree to file"
)
return parser


def create_root(
expected_work_termination_result: py_trees.common.Status,
) -> py_trees.behaviour.Behaviour:
"""
Create the root behaviour and it's subtree.
Returns:
the root behaviour
"""
root = py_trees.composites.Sequence(name="root", memory=True)
set_flag_to_false = py_trees.behaviours.SetBlackboardVariable(
name="SetFlagFalse",
variable_name="flag",
variable_value=False,
overwrite=True,
)
set_flag_to_true = py_trees.behaviours.SetBlackboardVariable(
name="SetFlagTrue", variable_name="flag", variable_value=True, overwrite=True
)
parallel = py_trees.composites.Parallel(
name="Parallel",
policy=py_trees.common.ParallelPolicy.SuccessOnOne(),
children=[],
)
worker = py_trees.behaviours.TickCounter(
name="Counter", duration=1, completion_status=expected_work_termination_result
)
well_finally = py_trees.decorators.Finally(name="Finally", child=set_flag_to_true)
parallel.add_children([worker, well_finally])
root.add_children([set_flag_to_false, parallel])
return root


##############################################################################
# Main
##############################################################################


def main() -> None:
"""Entry point for the demo script."""
args = command_line_argument_parser().parse_args()
# py_trees.logging.level = py_trees.logging.Level.DEBUG
print(description(create_root(py_trees.common.Status.SUCCESS)))

####################
# Rendering
####################
if args.render:
py_trees.display.render_dot_tree(create_root(py_trees.common.Status.SUCCESS))
sys.exit()

for status in (py_trees.common.Status.SUCCESS, py_trees.common.Status.FAILURE):
py_trees.blackboard.Blackboard.clear()
console.banner(f"Experiment - Terminate with {status}")
root = create_root(status)
root.tick_once()
print(py_trees.display.unicode_tree(root=root, show_status=True))
print(py_trees.display.unicode_blackboard())
root.tick_once()
print(py_trees.display.unicode_tree(root=root, show_status=True))
print(py_trees.display.unicode_blackboard())

print("\n")
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ py-trees-demo-display-modes = "py_trees.demos.display_modes:main"
py-trees-demo-dot-graphs = "py_trees.demos.dot_graphs:main"
py-trees-demo-either-or = "py_trees.demos.either_or:main"
py-trees-demo-eternal-guard = "py_trees.demos.eternal_guard:main"
py-trees-demo-finally = "py_trees.demos.decorator_finally:main"
py-trees-demo-logging = "py_trees.demos.logging:main"
py-trees-demo-pick-up-where-you-left-off = "py_trees.demos.pick_up_where_you_left_off:main"
py-trees-demo-selector = "py_trees.demos.selector:main"
Expand Down
Loading

0 comments on commit 812686b

Please sign in to comment.