Skip to content

Commit a5a6c0b

Browse files
committed
[decorators] finally-style decorators and idioms
1 parent 0d5b39f commit a5a6c0b

24 files changed

+821
-6
lines changed

.devcontainer/devcontainer.json

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"bierner.markdown-preview-github-styles",
2424
"bungcip.better-toml",
2525
"eamodio.gitlens",
26+
"joaompinto.vscode-graphviz",
2627
"ms-python.python",
2728
"omnilib.ufmt",
2829
"redhat.vscode-yaml",

.devcontainer/py310/.devcontainer/devcontainer.json

+7-3
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@
1919
"vscode": {
2020
"extensions": [
2121
"bierner.github-markdown-preview",
22+
"bierner.markdown-preview-github-styles",
2223
"bungcip.better-toml",
23-
"streetsidesoftware.code-spell-checker",
24-
"lextudio.restructuredtext",
24+
"eamodio.gitlens",
25+
"joaompinto.vscode-graphviz",
2526
"ms-python.python",
26-
"omnilib.ufmt"
27+
"omnilib.ufmt",
28+
"redhat.vscode-yaml",
29+
"streetsidesoftware.code-spell-checker",
30+
"tht13.rst-vscode"
2731
]
2832
}
2933
},

.vscode/settings.json

+5
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@
1212
"behaviours",
1313
"bierner",
1414
"bungcip",
15+
"epilog",
16+
"graphviz",
17+
"literalinclude",
18+
"noodly",
1519
"omnilib",
1620
"py_trees",
1721
"pydot",
1822
"pypi",
23+
"seealso",
1924
"ufmt",
2025
"usort"
2126
]

CHANGELOG.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Release Notes
33

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

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

docs/demos.rst

+32
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,38 @@ py-trees-demo-eternal-guard
147147
:linenos:
148148
:caption: py_trees/demos/eternal_guard.py
149149

150+
.. _py-trees-demo-eventually-program:
151+
152+
py-trees-demo-eventually
153+
------------------------
154+
155+
.. automodule:: py_trees.demos.eventually
156+
:members:
157+
:special-members:
158+
:show-inheritance:
159+
:synopsis: demo the eventually idiom
160+
161+
.. literalinclude:: ../py_trees/demos/eventually.py
162+
:language: python
163+
:linenos:
164+
:caption: py_trees/demos/eventually.py
165+
166+
.. _py-trees-demo-eventually-swiss-program:
167+
168+
py-trees-demo-eventually-swiss
169+
------------------------------
170+
171+
.. automodule:: py_trees.demos.eventually_swiss
172+
:members:
173+
:special-members:
174+
:show-inheritance:
175+
:synopsis: demo the general purpose eventually idiom
176+
177+
.. literalinclude:: ../py_trees/demos/eventually_swiss.py
178+
:language: python
179+
:linenos:
180+
:caption: py_trees/demos/eventually_swiss.py
181+
150182
.. _py-trees-demo-logging-program:
151183

152184
py-trees-demo-logging

docs/dot/demo-eventually-swiss.dot

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
digraph pastafarianism {
2+
ordering=out;
3+
graph [fontname="times-roman"];
4+
node [fontname="times-roman"];
5+
edge [fontname="times-roman"];
6+
"Count with Result" [fillcolor=cyan, fontcolor=black, fontsize=9, label="Count with Result", shape=octagon, style=filled];
7+
"Work to Success" [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Work to Success", shape=box, style=filled];
8+
"Count with Result" -> "Work to Success";
9+
Counter [fillcolor=gray, fontcolor=black, fontsize=9, label=Counter, shape=ellipse, style=filled];
10+
"Work to Success" -> Counter;
11+
SetResultTrue [fillcolor=gray, fontcolor=black, fontsize=9, label=SetResultTrue, shape=ellipse, style=filled];
12+
"Work to Success" -> SetResultTrue;
13+
"On Failure" [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ On Failure", shape=box, style=filled];
14+
"Count with Result" -> "On Failure";
15+
SetResultFalse [fillcolor=gray, fontcolor=black, fontsize=9, label=SetResultFalse, shape=ellipse, style=filled];
16+
"On Failure" -> SetResultFalse;
17+
Failure [fillcolor=gray, fontcolor=black, fontsize=9, label=Failure, shape=ellipse, style=filled];
18+
"On Failure" -> Failure;
19+
}

docs/dot/demo-eventually.dot

+17
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+
"Count and Record" [fillcolor=gold, fontcolor=black, fontsize=9, label="Count and Record\nSuccessOnOne", shape=parallelogram, style=filled];
7+
Counting [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Counting", shape=box, style=filled];
8+
"Count and Record" -> Counting;
9+
SetCountingFlagTrue [fillcolor=gray, fontcolor=black, fontsize=9, label=SetCountingFlagTrue, shape=ellipse, style=filled];
10+
Counting -> SetCountingFlagTrue;
11+
Counter [fillcolor=gray, fontcolor=black, fontsize=9, label=Counter, shape=ellipse, style=filled];
12+
Counting -> Counter;
13+
Eventually [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label=Eventually, shape=ellipse, style=filled];
14+
"Count and Record" -> Eventually;
15+
SetCountingFlagFalse [fillcolor=gray, fontcolor=black, fontsize=9, label=SetCountingFlagFalse, shape=ellipse, style=filled];
16+
Eventually -> SetCountingFlagFalse;
17+
}

docs/dot/demo-finally-single-tick.dot

+17
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/dot/eventually-swiss.dot

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
digraph pastafarianism {
2+
ordering=out;
3+
graph [fontname="times-roman"];
4+
node [fontname="times-roman"];
5+
edge [fontname="times-roman"];
6+
"Eventually (Swiss)" [fillcolor=cyan, fontcolor=black, fontsize=9, label="Eventually (Swiss)", shape=octagon, style=filled];
7+
"Work to Success" [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Work to Success", shape=box, style=filled];
8+
"Eventually (Swiss)" -> "Work to Success";
9+
Worker [fillcolor=gray, fontcolor=black, fontsize=9, label=Worker, shape=ellipse, style=filled];
10+
"Work to Success" -> Worker;
11+
"On Success" [fillcolor=gray, fontcolor=black, fontsize=9, label="On Success", shape=ellipse, style=filled];
12+
"Work to Success" -> "On Success";
13+
"On Failure" [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ On Failure", shape=box, style=filled];
14+
"Eventually (Swiss)" -> "On Failure";
15+
"On Failure*" [fillcolor=gray, fontcolor=black, fontsize=9, label="On Failure*", shape=ellipse, style=filled];
16+
"On Failure" -> "On Failure*";
17+
Failure [fillcolor=gray, fontcolor=black, fontsize=9, label=Failure, shape=ellipse, style=filled];
18+
"On Failure" -> Failure;
19+
}

docs/dot/eventually.dot

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
digraph pastafarianism {
2+
ordering=out;
3+
graph [fontname="times-roman"];
4+
node [fontname="times-roman"];
5+
edge [fontname="times-roman"];
6+
Eventually [fillcolor=gold, fontcolor=black, fontsize=9, label="Eventually\nSuccessOnOne", shape=parallelogram, style=filled];
7+
Worker [fillcolor=gray, fontcolor=black, fontsize=9, label=Worker, shape=ellipse, style=filled];
8+
Eventually -> Worker;
9+
"Eventually*" [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label="Eventually*", shape=ellipse, style=filled];
10+
Eventually -> "Eventually*";
11+
"On Completion" [fillcolor=gray, fontcolor=black, fontsize=9, label="On Completion", shape=ellipse, style=filled];
12+
"Eventually*" -> "On Completion";
13+
}

docs/examples/eventually.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import py_trees
5+
6+
if __name__ == "__main__":
7+
worker = py_trees.behaviours.Success(name="Worker")
8+
on_completion = py_trees.behaviours.Success(name="On Completion")
9+
root = py_trees.idioms.eventually(
10+
name="Eventually",
11+
worker=worker,
12+
on_completion=on_completion,
13+
)
14+
py_trees.display.render_dot_tree(
15+
root, py_trees.common.string_to_visibility_level("all")
16+
)

docs/examples/eventually_swiss.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import py_trees
5+
6+
if __name__ == "__main__":
7+
worker = py_trees.behaviours.Success(name="Worker")
8+
on_failure = py_trees.behaviours.Success(name="On Failure")
9+
on_success = py_trees.behaviours.Success(name="On Success")
10+
root = py_trees.idioms.eventually_swiss(
11+
name="Eventually (Swiss)",
12+
workers=[worker],
13+
on_failure=on_failure,
14+
on_success=on_success,
15+
)
16+
py_trees.display.render_dot_tree(
17+
root, py_trees.common.string_to_visibility_level("all")
18+
)

docs/images/demo-eventually-swiss.png

86.9 KB
Loading

docs/images/demo-eventually.png

88.7 KB
Loading

py_trees/behaviour.py

+1-1
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

+1
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

+62
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
* :class:`py_trees.decorators.EternalGuard`
3939
* :class:`py_trees.decorators.Inverter`
4040
* :class:`py_trees.decorators.OneShot`
41+
* :class:`py_trees.decorators.OnTerminate`
4142
* :class:`py_trees.decorators.Repeat`
4243
* :class:`py_trees.decorators.Retry`
4344
* :class:`py_trees.decorators.StatusToBlackboard`
@@ -920,3 +921,64 @@ 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 OnTerminate(Decorator):
927+
"""
928+
Trigger the child for a single tick on :meth:`terminate`.
929+
930+
Always return :data:`~py_trees.common.Status.RUNNING` and on
931+
on :meth:`terminate`, call the child's
932+
:meth:`~py_trees.behaviour.Behaviour.update` method, once.
933+
934+
This is useful to cleanup, restore a context switch or to
935+
implement a finally-like behaviour.
936+
937+
.. seealso:: :meth:`py_trees.idioms.eventually`
938+
"""
939+
940+
def __init__(self, name: str, child: behaviour.Behaviour):
941+
"""
942+
Initialise with the standard decorator arguments.
943+
944+
Args:
945+
name: the decorator name
946+
child: the child to be decorated
947+
"""
948+
super(OnTerminate, self).__init__(name=name, child=child)
949+
950+
def tick(self) -> typing.Iterator[behaviour.Behaviour]:
951+
"""
952+
Bypass the child when ticking.
953+
954+
Yields:
955+
a reference to itself
956+
"""
957+
self.logger.debug(f"{self.__class__.__name__}.tick()")
958+
self.status = self.update()
959+
yield self
960+
961+
def update(self) -> common.Status:
962+
"""
963+
Return with :data:`~py_trees.common.Status.RUNNING`.
964+
965+
Returns:
966+
the behaviour's new status :class:`~py_trees.common.Status`
967+
"""
968+
return common.Status.RUNNING
969+
970+
def terminate(self, new_status: common.Status) -> None:
971+
"""Tick the child behaviour once."""
972+
self.logger.debug(
973+
"{}.terminate({})".format(
974+
self.__class__.__name__,
975+
"{}->{}".format(self.status, new_status)
976+
if self.status != new_status
977+
else f"{new_status}",
978+
)
979+
)
980+
if new_status == common.Status.INVALID:
981+
self.decorated.tick_once()
982+
# Do not need to stop the child here - this method
983+
# is only called by Decorator.stop() which will handle
984+
# that responsibility immediately after this method returns.

py_trees/demos/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
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 eventually # usort:skip # noqa: F401
25+
from . import eventually_swiss # usort:skip # noqa: F401
2426
from . import lifecycle # usort:skip # noqa: F401
2527
from . import selector # usort:skip # noqa: F401
2628
from . import sequence # usort:skip # noqa: F401

0 commit comments

Comments
 (0)