py-trees 2.3.0__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {py_trees-2.3.0 → py_trees-2.4.0}/PKG-INFO +20 -9
  2. {py_trees-2.3.0 → py_trees-2.4.0}/README.md +16 -6
  3. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/behaviour.py +4 -4
  4. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/behaviours.py +85 -10
  5. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/blackboard.py +5 -5
  6. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/common.py +6 -6
  7. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/composites.py +30 -17
  8. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/decorators.py +81 -8
  9. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/action.py +1 -1
  10. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/blackboard.py +1 -1
  11. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/blackboard_namespaces.py +1 -1
  12. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/context_switching.py +1 -1
  13. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/dot_graphs.py +1 -1
  14. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/either_or.py +1 -1
  15. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/eternal_guard.py +1 -1
  16. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/lifecycle.py +1 -1
  17. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/logging.py +1 -1
  18. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/pick_up_where_you_left_off.py +1 -1
  19. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/selector.py +1 -1
  20. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/sequence.py +1 -1
  21. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/stewardship.py +1 -1
  22. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/tests.py +1 -1
  23. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/trees.py +5 -5
  24. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/version.py +1 -1
  25. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/visitors.py +1 -1
  26. {py_trees-2.3.0 → py_trees-2.4.0}/pyproject.toml +1 -2
  27. {py_trees-2.3.0 → py_trees-2.4.0}/LICENSE +0 -0
  28. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/__init__.py +0 -0
  29. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/console.py +0 -0
  30. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/README.md +0 -0
  31. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/__init__.py +0 -0
  32. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/blackboard_remappings.py +0 -0
  33. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/demos/display_modes.py +0 -0
  34. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/display.py +0 -0
  35. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/idioms.py +0 -0
  36. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/logging.py +0 -0
  37. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/meta.py +0 -0
  38. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/programs/__init__.py +0 -0
  39. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/programs/render.py +0 -0
  40. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/py.typed +0 -0
  41. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/syntax_highlighting.py +0 -0
  42. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/timers.py +0 -0
  43. {py_trees-2.3.0 → py_trees-2.4.0}/py_trees/utilities.py +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: py_trees
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: pythonic implementation of behaviour trees
5
5
  License: BSD
6
+ License-File: LICENSE
6
7
  Keywords: py_trees,py-trees,behaviour-trees
7
8
  Author: Daniel Stonier
8
9
  Maintainer: Daniel Stonier
@@ -19,9 +20,9 @@ Classifier: Programming Language :: Python :: 3.10
19
20
  Classifier: Programming Language :: Python :: 3.11
20
21
  Classifier: Programming Language :: Python :: 3.12
21
22
  Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
22
24
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
25
  Classifier: Topic :: Software Development :: Libraries
24
- Requires-Dist: py (>=1.11.0,<2.0.0)
25
26
  Requires-Dist: pydot (>=1.4)
26
27
  Project-URL: Documentation, https://py-trees.readthedocs.io/en/devel/
27
28
  Project-URL: Homepage, https://github.com/splintered-reality/py_trees
@@ -48,11 +49,13 @@ Brief feature list:
48
49
 
49
50
  ## What's New?
50
51
 
52
+ * [2025-11-13] New `ForEach` decorator.
53
+ * [2025-11-13] New `CompareBlackboardVariables` behaviour, with comparison expressions that allow callables.
51
54
  * [2025-01-11] Support for Python 3.12 was added, and Python 3.8 was dropped.
52
55
 
53
56
  ## Documentation
54
57
 
55
- [![devel][docs-devel-image]][docs-devel] [![2.3.x][docs-2.3.x-image]][docs-2.3.x] [![2.2.x][docs-2.2.x-image]][docs-2.2.x] [![2.1.x][docs-2.1.x-image]][docs-2.1.x]
58
+ [![devel][docs-devel-image]][docs-devel] [![2.4.x][docs-2.4.x-image]][docs-2.4.x] [![2.3.x][docs-2.3.x-image]][docs-2.3.x] [![2.2.x][docs-2.2.x-image]][docs-2.2.x] [![2.1.x][docs-2.1.x-image]][docs-2.1.x]
56
59
 
57
60
  ## Getting Started
58
61
 
@@ -115,6 +118,7 @@ Robotics:
115
118
 
116
119
  ## Releases
117
120
 
121
+ * `2.4.x` - New ForEach decorator, new CompareBlackboardVariables behaviour, and callables in comparison expressions.
118
122
  * `2.3.x` - Support for Python 3.12 was added, and Python 3.8 was dropped.
119
123
  * `2.2.x` - Selectors, Sequences with and without memory. Improved testing and style/type checking.
120
124
  * `2.1.x` - Chooser deprecated. API housekeeping.
@@ -124,12 +128,12 @@ Robotics:
124
128
  * `1.0.x` - Behaviours, Decorators, Composites, Blackboards, Tree Management and Viz tools.
125
129
  * `0.y.x` - First open source pre-releases.
126
130
 
127
- | | Devel | 2.3.x | 2.2.x | 2.1.x | 2.0.x | 1.2.x |
131
+ | | Devel | 2.4.x | 2.3.x | 2.2.x | 2.1.x | 2.0.x |
128
132
  |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
129
- | Sources | [![devel][sources-devel-image]][sources-devel] | [![2.3.x][sources-2.3.x-image]][sources-2.3.x] | [![2.2.x][sources-2.2.x-image]][sources-2.2.x] | [![2.1.x][sources-2.1.x-image]][sources-2.1.x] | [![2.0.x][sources-2.0.x-image]][sources-2.0.x] | [![1.2.x][sources-1.2.x-image]][sources-1.2.x] |
130
- | Compatibility | [![Python 3.12][python312-image]][python312-docs]<br/>[![Python 3.10][python310-image]][python310-docs] | [![Python 3.12][python312-image]][python312-docs]<br/>[![Python 3.10][python310-image]][python310-docs] | [![Python 3.10][python310-image]][python310-docs]<br/>[![Python 3.8][python38-image]][python38-docs] | [![Python 3.6][python36-image]][python36-docs] | [![Python 3.6][python36-image]][python36-docs] | [![Python 3.6][python36-image]][python36-docs] | [![Python 3.6][python36-image]][python36-docs] |
131
- | CI | [![devel-Status][devel-build-status-image]][devel-build-status] | [![2.3.x-Status][2.3.x-build-status-image]][2.3.x-build-status] | [![2.2.x-Status][2.2.x-build-status-image]][2.2.x-build-status] | - | - | - | - | - |
132
- | Documentation | [![devel-Docs][rtd-devel-image]][docs-devel] | [![2.3.x-Docs][rtd-2.3.x-image]][docs-2.3.x] | [![2.2.x-Docs][rtd-2.2.x-image]][docs-2.2.x] | [![2.1.x-Docs][rtd-2.1.x-image]][docs-2.1.x] | [![2.0.x-Docs][rtd-2.0.x-image]][docs-2.0.x] | [![1.2.x-Docs][rtd-1.2.x-image]][docs-1.2.x] |
133
+ | Sources | [![devel][sources-devel-image]][sources-devel] | [![2.4.x][sources-2.4.x-image]][sources-2.4.x] | [![2.3.x][sources-2.3.x-image]][sources-2.3.x] | [![2.2.x][sources-2.2.x-image]][sources-2.2.x] | [![2.1.x][sources-2.1.x-image]][sources-2.1.x] | [![2.0.x][sources-2.0.x-image]][sources-2.0.x] |
134
+ | Compatibility | [![Python 3.12][python312-image]][python312-docs]<br/>[![Python 3.10][python310-image]][python310-docs] | [![Python 3.12][python312-image]][python312-docs]<br/>[![Python 3.10][python310-image]][python310-docs] | [![Python 3.12][python312-image]][python312-docs]<br/>[![Python 3.10][python310-image]][python310-docs] | [![Python 3.10][python310-image]][python310-docs]<br/>[![Python 3.8][python38-image]][python38-docs] | [![Python 3.6][python36-image]][python36-docs] | [![Python 3.6][python36-image]][python36-docs] | [![Python 3.6][python36-image]][python36-docs] |
135
+ | CI | [![devel-Status][devel-build-status-image]][devel-build-status] | [![2.4.x-Status][2.4.x-build-status-image]][2.4.x-build-status] | [![2.3.x-Status][2.3.x-build-status-image]][2.3.x-build-status] | [![2.2.x-Status][2.2.x-build-status-image]][2.2.x-build-status] | - | - | - | - |
136
+ | Documentation | [![devel-Docs][rtd-devel-image]][docs-devel] | [![2.4.x-Docs][rtd-2.4.x-image]][docs-2.4.x] | [![2.3.x-Docs][rtd-2.3.x-image]][docs-2.3.x] | [![2.2.x-Docs][rtd-2.2.x-image]][docs-2.2.x] | [![2.1.x-Docs][rtd-2.1.x-image]][docs-2.1.x] | [![2.0.x-Docs][rtd-2.0.x-image]][docs-2.0.x] |
133
137
 
134
138
 
135
139
  [license-image]: https://img.shields.io/badge/License-BSD%203--Clause-orange.svg?style=plastic
@@ -146,12 +150,15 @@ Robotics:
146
150
 
147
151
  [devel-build-status-image]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml/badge.svg
148
152
  [devel-build-status]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml
153
+ [2.4.x-build-status-image]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml/badge.svg??branch=release/2.4.x
154
+ [2.4.x-build-status]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml
149
155
  [2.3.x-build-status-image]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml/badge.svg??branch=release/2.3.x
150
156
  [2.3.x-build-status]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml
151
157
  [2.2.x-build-status-image]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml/badge.svg??branch=release/2.2.x
152
158
  [2.2.x-build-status]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml
153
159
 
154
160
  [docs-devel]: http://py-trees.readthedocs.io/
161
+ [docs-2.4.x]: http://py-trees.readthedocs.io/en/release-2.4.x/
155
162
  [docs-2.3.x]: http://py-trees.readthedocs.io/en/release-2.3.x/
156
163
  [docs-2.2.x]: http://py-trees.readthedocs.io/en/release-2.2.x/
157
164
  [docs-2.1.x]: http://py-trees.readthedocs.io/en/release-2.1.x/
@@ -163,6 +170,7 @@ Robotics:
163
170
  [docs-0.5.x]: http://docs.ros.org/kinetic/api/py_trees/html/
164
171
 
165
172
  [docs-devel-image]: http://img.shields.io/badge/docs-devel-brightgreen.svg?style=plastic
173
+ [docs-2.4.x-image]: http://img.shields.io/badge/docs-2.4.x-brightgreen.svg?style=plastic
166
174
  [docs-2.3.x-image]: http://img.shields.io/badge/docs-2.3.x-brightgreen.svg?style=plastic
167
175
  [docs-2.2.x-image]: http://img.shields.io/badge/docs-2.2.x-brightgreen.svg?style=plastic
168
176
  [docs-2.1.x-image]: http://img.shields.io/badge/docs-2.1.x-brightgreen.svg?style=plastic
@@ -174,6 +182,7 @@ Robotics:
174
182
  [docs-0.5.x-image]: http://img.shields.io/badge/docs-0.5.x-brightgreen.svg?style=plastic
175
183
 
176
184
  [rtd-devel-image]: https://readthedocs.org/projects/py-trees/badge/?version=devel&style=plastic
185
+ [rtd-2.4.x-image]: https://readthedocs.org/projects/py-trees/badge/?version=release-2.4.x&style=plastic
177
186
  [rtd-2.3.x-image]: https://readthedocs.org/projects/py-trees/badge/?version=release-2.3.x&style=plastic
178
187
  [rtd-2.2.x-image]: https://readthedocs.org/projects/py-trees/badge/?version=release-2.2.x&style=plastic
179
188
  [rtd-2.1.x-image]: https://readthedocs.org/projects/py-trees/badge/?version=release-2.1.x&style=plastic
@@ -186,6 +195,7 @@ Robotics:
186
195
  [not-available-docs-image]: http://img.shields.io/badge/docs-n/a-yellow.svg?style=plastic
187
196
 
188
197
  [sources-devel]: https://github.com/splintered-reality/py_trees/tree/devel
198
+ [sources-2.4.x]: https://github.com/splintered-reality/py_trees/tree/release/2.4.x
189
199
  [sources-2.3.x]: https://github.com/splintered-reality/py_trees/tree/release/2.3.x
190
200
  [sources-2.2.x]: https://github.com/splintered-reality/py_trees/tree/release/2.2.x
191
201
  [sources-2.1.x]: https://github.com/splintered-reality/py_trees/tree/release/2.1.x
@@ -197,6 +207,7 @@ Robotics:
197
207
  [sources-0.5.x]: https://github.com/splintered-reality/py_trees/tree/release/0.5.x
198
208
 
199
209
  [sources-devel-image]: http://img.shields.io/badge/sources-devel-blue.svg?style=plastic
210
+ [sources-2.4.x-image]: http://img.shields.io/badge/sources-2.4.x-blue.svg?style=plastic
200
211
  [sources-2.3.x-image]: http://img.shields.io/badge/sources-2.3.x-blue.svg?style=plastic
201
212
  [sources-2.2.x-image]: http://img.shields.io/badge/sources-2.2.x-blue.svg?style=plastic
202
213
  [sources-2.1.x-image]: http://img.shields.io/badge/sources-2.1.x-blue.svg?style=plastic
@@ -18,11 +18,13 @@ Brief feature list:
18
18
 
19
19
  ## What's New?
20
20
 
21
+ * [2025-11-13] New `ForEach` decorator.
22
+ * [2025-11-13] New `CompareBlackboardVariables` behaviour, with comparison expressions that allow callables.
21
23
  * [2025-01-11] Support for Python 3.12 was added, and Python 3.8 was dropped.
22
24
 
23
25
  ## Documentation
24
26
 
25
- [![devel][docs-devel-image]][docs-devel] [![2.3.x][docs-2.3.x-image]][docs-2.3.x] [![2.2.x][docs-2.2.x-image]][docs-2.2.x] [![2.1.x][docs-2.1.x-image]][docs-2.1.x]
27
+ [![devel][docs-devel-image]][docs-devel] [![2.4.x][docs-2.4.x-image]][docs-2.4.x] [![2.3.x][docs-2.3.x-image]][docs-2.3.x] [![2.2.x][docs-2.2.x-image]][docs-2.2.x] [![2.1.x][docs-2.1.x-image]][docs-2.1.x]
26
28
 
27
29
  ## Getting Started
28
30
 
@@ -85,6 +87,7 @@ Robotics:
85
87
 
86
88
  ## Releases
87
89
 
90
+ * `2.4.x` - New ForEach decorator, new CompareBlackboardVariables behaviour, and callables in comparison expressions.
88
91
  * `2.3.x` - Support for Python 3.12 was added, and Python 3.8 was dropped.
89
92
  * `2.2.x` - Selectors, Sequences with and without memory. Improved testing and style/type checking.
90
93
  * `2.1.x` - Chooser deprecated. API housekeeping.
@@ -94,12 +97,12 @@ Robotics:
94
97
  * `1.0.x` - Behaviours, Decorators, Composites, Blackboards, Tree Management and Viz tools.
95
98
  * `0.y.x` - First open source pre-releases.
96
99
 
97
- | | Devel | 2.3.x | 2.2.x | 2.1.x | 2.0.x | 1.2.x |
100
+ | | Devel | 2.4.x | 2.3.x | 2.2.x | 2.1.x | 2.0.x |
98
101
  |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
99
- | Sources | [![devel][sources-devel-image]][sources-devel] | [![2.3.x][sources-2.3.x-image]][sources-2.3.x] | [![2.2.x][sources-2.2.x-image]][sources-2.2.x] | [![2.1.x][sources-2.1.x-image]][sources-2.1.x] | [![2.0.x][sources-2.0.x-image]][sources-2.0.x] | [![1.2.x][sources-1.2.x-image]][sources-1.2.x] |
100
- | Compatibility | [![Python 3.12][python312-image]][python312-docs]<br/>[![Python 3.10][python310-image]][python310-docs] | [![Python 3.12][python312-image]][python312-docs]<br/>[![Python 3.10][python310-image]][python310-docs] | [![Python 3.10][python310-image]][python310-docs]<br/>[![Python 3.8][python38-image]][python38-docs] | [![Python 3.6][python36-image]][python36-docs] | [![Python 3.6][python36-image]][python36-docs] | [![Python 3.6][python36-image]][python36-docs] | [![Python 3.6][python36-image]][python36-docs] |
101
- | CI | [![devel-Status][devel-build-status-image]][devel-build-status] | [![2.3.x-Status][2.3.x-build-status-image]][2.3.x-build-status] | [![2.2.x-Status][2.2.x-build-status-image]][2.2.x-build-status] | - | - | - | - | - |
102
- | Documentation | [![devel-Docs][rtd-devel-image]][docs-devel] | [![2.3.x-Docs][rtd-2.3.x-image]][docs-2.3.x] | [![2.2.x-Docs][rtd-2.2.x-image]][docs-2.2.x] | [![2.1.x-Docs][rtd-2.1.x-image]][docs-2.1.x] | [![2.0.x-Docs][rtd-2.0.x-image]][docs-2.0.x] | [![1.2.x-Docs][rtd-1.2.x-image]][docs-1.2.x] |
102
+ | Sources | [![devel][sources-devel-image]][sources-devel] | [![2.4.x][sources-2.4.x-image]][sources-2.4.x] | [![2.3.x][sources-2.3.x-image]][sources-2.3.x] | [![2.2.x][sources-2.2.x-image]][sources-2.2.x] | [![2.1.x][sources-2.1.x-image]][sources-2.1.x] | [![2.0.x][sources-2.0.x-image]][sources-2.0.x] |
103
+ | Compatibility | [![Python 3.12][python312-image]][python312-docs]<br/>[![Python 3.10][python310-image]][python310-docs] | [![Python 3.12][python312-image]][python312-docs]<br/>[![Python 3.10][python310-image]][python310-docs] | [![Python 3.12][python312-image]][python312-docs]<br/>[![Python 3.10][python310-image]][python310-docs] | [![Python 3.10][python310-image]][python310-docs]<br/>[![Python 3.8][python38-image]][python38-docs] | [![Python 3.6][python36-image]][python36-docs] | [![Python 3.6][python36-image]][python36-docs] | [![Python 3.6][python36-image]][python36-docs] |
104
+ | CI | [![devel-Status][devel-build-status-image]][devel-build-status] | [![2.4.x-Status][2.4.x-build-status-image]][2.4.x-build-status] | [![2.3.x-Status][2.3.x-build-status-image]][2.3.x-build-status] | [![2.2.x-Status][2.2.x-build-status-image]][2.2.x-build-status] | - | - | - | - |
105
+ | Documentation | [![devel-Docs][rtd-devel-image]][docs-devel] | [![2.4.x-Docs][rtd-2.4.x-image]][docs-2.4.x] | [![2.3.x-Docs][rtd-2.3.x-image]][docs-2.3.x] | [![2.2.x-Docs][rtd-2.2.x-image]][docs-2.2.x] | [![2.1.x-Docs][rtd-2.1.x-image]][docs-2.1.x] | [![2.0.x-Docs][rtd-2.0.x-image]][docs-2.0.x] |
103
106
 
104
107
 
105
108
  [license-image]: https://img.shields.io/badge/License-BSD%203--Clause-orange.svg?style=plastic
@@ -116,12 +119,15 @@ Robotics:
116
119
 
117
120
  [devel-build-status-image]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml/badge.svg
118
121
  [devel-build-status]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml
122
+ [2.4.x-build-status-image]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml/badge.svg??branch=release/2.4.x
123
+ [2.4.x-build-status]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml
119
124
  [2.3.x-build-status-image]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml/badge.svg??branch=release/2.3.x
120
125
  [2.3.x-build-status]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml
121
126
  [2.2.x-build-status-image]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml/badge.svg??branch=release/2.2.x
122
127
  [2.2.x-build-status]: https://github.com/splintered-reality/py_trees/actions/workflows/pre-merge.yaml
123
128
 
124
129
  [docs-devel]: http://py-trees.readthedocs.io/
130
+ [docs-2.4.x]: http://py-trees.readthedocs.io/en/release-2.4.x/
125
131
  [docs-2.3.x]: http://py-trees.readthedocs.io/en/release-2.3.x/
126
132
  [docs-2.2.x]: http://py-trees.readthedocs.io/en/release-2.2.x/
127
133
  [docs-2.1.x]: http://py-trees.readthedocs.io/en/release-2.1.x/
@@ -133,6 +139,7 @@ Robotics:
133
139
  [docs-0.5.x]: http://docs.ros.org/kinetic/api/py_trees/html/
134
140
 
135
141
  [docs-devel-image]: http://img.shields.io/badge/docs-devel-brightgreen.svg?style=plastic
142
+ [docs-2.4.x-image]: http://img.shields.io/badge/docs-2.4.x-brightgreen.svg?style=plastic
136
143
  [docs-2.3.x-image]: http://img.shields.io/badge/docs-2.3.x-brightgreen.svg?style=plastic
137
144
  [docs-2.2.x-image]: http://img.shields.io/badge/docs-2.2.x-brightgreen.svg?style=plastic
138
145
  [docs-2.1.x-image]: http://img.shields.io/badge/docs-2.1.x-brightgreen.svg?style=plastic
@@ -144,6 +151,7 @@ Robotics:
144
151
  [docs-0.5.x-image]: http://img.shields.io/badge/docs-0.5.x-brightgreen.svg?style=plastic
145
152
 
146
153
  [rtd-devel-image]: https://readthedocs.org/projects/py-trees/badge/?version=devel&style=plastic
154
+ [rtd-2.4.x-image]: https://readthedocs.org/projects/py-trees/badge/?version=release-2.4.x&style=plastic
147
155
  [rtd-2.3.x-image]: https://readthedocs.org/projects/py-trees/badge/?version=release-2.3.x&style=plastic
148
156
  [rtd-2.2.x-image]: https://readthedocs.org/projects/py-trees/badge/?version=release-2.2.x&style=plastic
149
157
  [rtd-2.1.x-image]: https://readthedocs.org/projects/py-trees/badge/?version=release-2.1.x&style=plastic
@@ -156,6 +164,7 @@ Robotics:
156
164
  [not-available-docs-image]: http://img.shields.io/badge/docs-n/a-yellow.svg?style=plastic
157
165
 
158
166
  [sources-devel]: https://github.com/splintered-reality/py_trees/tree/devel
167
+ [sources-2.4.x]: https://github.com/splintered-reality/py_trees/tree/release/2.4.x
159
168
  [sources-2.3.x]: https://github.com/splintered-reality/py_trees/tree/release/2.3.x
160
169
  [sources-2.2.x]: https://github.com/splintered-reality/py_trees/tree/release/2.2.x
161
170
  [sources-2.1.x]: https://github.com/splintered-reality/py_trees/tree/release/2.1.x
@@ -167,6 +176,7 @@ Robotics:
167
176
  [sources-0.5.x]: https://github.com/splintered-reality/py_trees/tree/release/0.5.x
168
177
 
169
178
  [sources-devel-image]: http://img.shields.io/badge/sources-devel-blue.svg?style=plastic
179
+ [sources-2.4.x-image]: http://img.shields.io/badge/sources-2.4.x-blue.svg?style=plastic
170
180
  [sources-2.3.x-image]: http://img.shields.io/badge/sources-2.3.x-blue.svg?style=plastic
171
181
  [sources-2.2.x-image]: http://img.shields.io/badge/sources-2.2.x-blue.svg?style=plastic
172
182
  [sources-2.1.x-image]: http://img.shields.io/badge/sources-2.1.x-blue.svg?style=plastic
@@ -146,7 +146,7 @@ class Behaviour(abc.ABC):
146
146
 
147
147
  Args:
148
148
  **kwargs: distribute arguments to this
149
- behaviour and in turn, all of it's children
149
+ behaviour and in turn, all of its children
150
150
 
151
151
  Raises:
152
152
  Exception: if this behaviour has a fault in construction or configuration
@@ -261,7 +261,7 @@ class Behaviour(abc.ABC):
261
261
  ############################################
262
262
 
263
263
  def setup_with_descendants(self) -> None:
264
- """Call setup on this child, it's children (it's children's children, )."""
264
+ """Call setup on this child, its children (its children's children, )."""
265
265
  for child in self.children:
266
266
  for node in child.iterate():
267
267
  node.setup()
@@ -321,7 +321,7 @@ class Behaviour(abc.ABC):
321
321
 
322
322
  def iterate(self, direct_descendants: bool = False) -> typing.Iterator[Behaviour]:
323
323
  """
324
- Iterate over this child and it's children.
324
+ Iterate over this child and its children.
325
325
 
326
326
  This utilises python generators for looping. To traverse the entire tree:
327
327
 
@@ -334,7 +334,7 @@ class Behaviour(abc.ABC):
334
334
  direct_descendants (:obj:`bool`): only yield children one step away from this behaviour.
335
335
 
336
336
  Yields:
337
- :class:`~py_trees.behaviour.Behaviour`: one of it's children
337
+ :class:`~py_trees.behaviour.Behaviour`: one of its children
338
338
  """
339
339
  for child in self.children:
340
340
  if not direct_descendants:
@@ -109,7 +109,7 @@ Crash test dummy used for anything dangerous.
109
109
 
110
110
  class Periodic(behaviour.Behaviour):
111
111
  """
112
- Simply periodically rotates it's status over all each status.
112
+ Simply periodically rotates its status over all each status.
113
113
 
114
114
  That is, :data:`~py_trees.common.Status.RUNNING` for N ticks,
115
115
  :data:`~py_trees.common.Status.SUCCESS` for N ticks,
@@ -210,7 +210,7 @@ class SuccessEveryN(behaviour.Behaviour):
210
210
  """
211
211
  Non-blocking, periodic success.
212
212
 
213
- This behaviour updates it's status with :data:`~py_trees.common.Status.SUCCESS`
213
+ This behaviour updates its status with :data:`~py_trees.common.Status.SUCCESS`
214
214
  once every N ticks, :data:`~py_trees.common.Status.FAILURE` otherwise.
215
215
 
216
216
  Args:
@@ -570,10 +570,10 @@ class CheckBlackboardVariableValue(behaviour.Behaviour):
570
570
  """
571
571
  self.logger.debug("%s.update()" % self.__class__.__name__)
572
572
  try:
573
- value = self.blackboard.get(self.key)
573
+ lhs_value = self.blackboard.get(self.key)
574
574
  if self.key_attributes:
575
575
  try:
576
- value = operator.attrgetter(self.key_attributes)(value)
576
+ lhs_value = operator.attrgetter(self.key_attributes)(lhs_value)
577
577
  except AttributeError:
578
578
  self.feedback_message = (
579
579
  "blackboard key-value pair exists, but the value does not "
@@ -588,20 +588,95 @@ class CheckBlackboardVariableValue(behaviour.Behaviour):
588
588
  )
589
589
  return common.Status.FAILURE
590
590
 
591
- success = self.check.operator(value, self.check.value)
591
+ rhs_value = self.check.value_generator()
592
+ success = self.check.operator(lhs_value, rhs_value)
592
593
 
593
594
  if success:
594
595
  self.feedback_message = "'%s' comparison succeeded [v: %s][e: %s]" % (
595
596
  self.check.variable,
596
- value,
597
- self.check.value,
597
+ lhs_value,
598
+ rhs_value,
598
599
  )
599
600
  return common.Status.SUCCESS
600
601
  else:
601
602
  self.feedback_message = "'%s' comparison failed [v: %s][e: %s]" % (
602
603
  self.check.variable,
603
- value,
604
- self.check.value,
604
+ lhs_value,
605
+ rhs_value,
606
+ )
607
+ return common.Status.FAILURE
608
+
609
+
610
+ class CompareBlackboardVariables(behaviour.Behaviour):
611
+ """
612
+ Non-blocking comparison between two blackboard variables.
613
+
614
+ Gets two blackboard variables and applies the given operator between them,
615
+ returning SUCCESS if the comparison holds.
616
+ This is non-blocking, so it will always tick with
617
+ :data:`~py_trees.common.Status.SUCCESS` or
618
+ :data:`~py_trees.common.Status.FAILURE`.
619
+
620
+ Args:
621
+ name: name of the behaviour
622
+ var1_key: blackboard key for the first variable
623
+ var2_key: blackboard key for the second variable
624
+ operator: a callable comparison operator
625
+
626
+ .. note::
627
+ If the variables do not yet exist on the blackboard, the behaviour will
628
+ return with status :data:`~py_trees.common.Status.FAILURE`.
629
+
630
+ .. tip::
631
+ The python `operator module`_ includes many useful comparison operations.
632
+ """
633
+
634
+ def __init__(
635
+ self,
636
+ name: str,
637
+ var1_key: str,
638
+ var2_key: str,
639
+ operator: typing.Callable[[common.ComparisonV, common.ComparisonV], bool],
640
+ ):
641
+ super().__init__(name=name)
642
+
643
+ self.var1_key = var1_key
644
+ self.var2_key = var2_key
645
+ self.operator = operator
646
+ self.blackboard = self.attach_blackboard_client()
647
+ self.blackboard.register_key(key=self.var1_key, access=common.Access.READ)
648
+ self.blackboard.register_key(key=self.var2_key, access=common.Access.READ)
649
+
650
+ def update(self) -> common.Status:
651
+ """
652
+ Check for the two variables and applies the operator between them.
653
+
654
+ Returns:
655
+ :class:`~py_trees.common.Status`: :data:`~py_trees.common.Status.FAILURE`
656
+ if not matched, :data:`~py_trees.common.Status.SUCCESS` otherwise.
657
+ """
658
+ self.logger.debug("%s.update()" % self.__class__.__name__)
659
+ try:
660
+ lhs_value = self.blackboard.get(self.var1_key)
661
+ rhs_value = self.blackboard.get(self.var2_key)
662
+ except KeyError as err:
663
+ self.logger.error(str(err))
664
+ return common.Status.FAILURE
665
+
666
+ if self.operator(lhs_value, rhs_value):
667
+ self.feedback_message = "'%s, %s' comparison succeeded [v1: %s][v2: %s]" % (
668
+ self.var1_key,
669
+ self.var2_key,
670
+ lhs_value,
671
+ rhs_value,
672
+ )
673
+ return common.Status.SUCCESS
674
+ else:
675
+ self.feedback_message = "'%s, %s' comparison failed [v1: %s][v2: %s]" % (
676
+ self.var1_key,
677
+ self.var2_key,
678
+ lhs_value,
679
+ rhs_value,
605
680
  )
606
681
  return common.Status.FAILURE
607
682
 
@@ -722,7 +797,7 @@ class CheckBlackboardVariableValues(behaviour.Behaviour):
722
797
  )
723
798
  )
724
799
  return common.Status.FAILURE
725
- results.append(check.operator(value, check.value))
800
+ results.append(check.operator(value, check.value_generator()))
726
801
  if self.blackboard_results is not None:
727
802
  for counter in range(1, len(results) + 1):
728
803
  self.blackboard_results.set(str(counter), results[counter - 1])
@@ -188,7 +188,7 @@ class Blackboard(object):
188
188
  Centralised key-value store for sharing data between behaviours.
189
189
 
190
190
  This class is a coat-hanger for the centralised data store, metadata
191
- for it's administration and static methods for interacting with it.
191
+ for its administration and static methods for interacting with it.
192
192
 
193
193
  This api is intended for authors of debugging and introspection
194
194
  tools on the blackboard. Users should make use of the :class:`Client`.
@@ -234,7 +234,7 @@ class Blackboard(object):
234
234
  variable_name: of the variable to get, can be nested, e.g. battery.percentage
235
235
 
236
236
  Raises:
237
- KeyError: if the variable or it's nested attributes do not yet exist on the blackboard
237
+ KeyError: if the variable or its nested attributes do not yet exist on the blackboard
238
238
 
239
239
  Return:
240
240
  The stored value for the given variable
@@ -528,7 +528,7 @@ class Blackboard(object):
528
528
  variable_name: blackboard variable name - can be nested, e.g. battery.percentage
529
529
 
530
530
  Returns:
531
- a tuple consisting of the key and it's attributes (in string form)
531
+ a tuple consisting of the key and its attributes (in string form)
532
532
  """
533
533
  name_components = variable_name.split(".")
534
534
  key = name_components[0]
@@ -986,7 +986,7 @@ class Client(object):
986
986
  opportunity followed by higher priority behaviours in the tree and so forth.
987
987
  Lower priority behaviours would use this to respect the pre-configured
988
988
  setting and at most, just validate that it is acceptable to the functionality
989
- of it's own behaviour.
989
+ of its own behaviour.
990
990
 
991
991
  Args:
992
992
  name: name of the variable to set
@@ -1092,7 +1092,7 @@ class Client(object):
1092
1092
 
1093
1093
  Raises:
1094
1094
  AttributeError: if the client does not have read access to the variable
1095
- KeyError: if the variable or it's nested attributes do not yet exist on the blackboard
1095
+ KeyError: if the variable or its nested attributes do not yet exist on the blackboard
1096
1096
  """
1097
1097
  # key attributes is an empty string if not a nested variable name
1098
1098
  name_components = name.split(".")
@@ -195,7 +195,7 @@ class ComparisonExpression(object):
195
195
 
196
196
  Args:
197
197
  variable: name of the variable to compare
198
- value: value to compare against
198
+ value: value or callable to compare against
199
199
  operator: a callable comparison operator
200
200
 
201
201
  .. tip::
@@ -206,27 +206,27 @@ class ComparisonExpression(object):
206
206
 
207
207
  check = ComparisonExpression(
208
208
  variable="foo",
209
- value= 5,
209
+ value=5,
210
210
  operator=operator.eq
211
211
  )
212
- success = check.operator(blackboard[check.variable], check.value)
212
+ success = check.operator(blackboard[check.variable], check.value_generator())
213
213
  """
214
214
 
215
215
  def __init__(
216
216
  self,
217
217
  variable: str,
218
- value: ComparisonV,
218
+ value: typing.Union[ComparisonV, typing.Callable[[], ComparisonV]],
219
219
  operator: typing.Callable[[ComparisonV, ComparisonV], bool],
220
220
  ):
221
221
  """Initialise variables.
222
222
 
223
223
  Args:
224
224
  variable: the variable name to use
225
- value: the value to use on the RHS of the expression
225
+ value: the value to use on the RHS of the expression (can also be a callable that returns the value)
226
226
  operator: a logical operator to enable comparisons
227
227
  """
228
228
  self.variable = variable
229
- self.value = value
229
+ self.value_generator = value if callable(value) else lambda: value
230
230
  self.operator = operator
231
231
 
232
232
 
@@ -120,7 +120,7 @@ class Composite(behaviour.Behaviour, abc.ABC):
120
120
  behaviours do the real work.
121
121
 
122
122
  Such flows are a consequence of how the composite
123
- interacts with it's children. The success of
123
+ interacts with its children. The success of
124
124
  behaviour trees depends on this logic being simple,
125
125
  well defined and limited to a few well established
126
126
  patterns - this is what ensures that visualising
@@ -391,7 +391,7 @@ class Selector(Composite):
391
391
  Customise the tick behaviour for a selector.
392
392
 
393
393
  This implements priority-interrupt style handling amongst the selector's children.
394
- The selector's status is always a reflection of it's children's status.
394
+ The selector's status is always a reflection of its children's status.
395
395
 
396
396
  Yields:
397
397
  :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children
@@ -427,7 +427,8 @@ class Selector(Composite):
427
427
  # clear out preceding status' - not actually necessary but helps
428
428
  # visualise the case of memory vs no memory
429
429
  for child in itertools.islice(self.children, None, index):
430
- child.stop(common.Status.INVALID)
430
+ if child.status != common.Status.INVALID:
431
+ child.stop(common.Status.INVALID)
431
432
  else:
432
433
  index = 0
433
434
 
@@ -442,7 +443,6 @@ class Selector(Composite):
442
443
  or node.status == common.Status.SUCCESS
443
444
  ):
444
445
  self.current_child = child
445
- self.status = node.status
446
446
  if previous is None or previous != self.current_child:
447
447
  # we interrupted, invalidate everything at a lower priority
448
448
  passed = False
@@ -451,10 +451,18 @@ class Selector(Composite):
451
451
  if child.status != common.Status.INVALID:
452
452
  child.stop(common.Status.INVALID)
453
453
  passed = True if child == self.current_child else passed
454
+
455
+ # terminate the selector if a terminal state was reached
456
+ if node.status == common.Status.SUCCESS:
457
+ self.stop(node.status)
458
+ else:
459
+ self.status = node.status
460
+
454
461
  yield self
455
462
  return
463
+
456
464
  # all children failed, set failure ourselves and current child to the last bugger who failed us
457
- self.status = common.Status.FAILURE
465
+ self.stop(common.Status.FAILURE)
458
466
  try:
459
467
  self.current_child = self.children[-1]
460
468
  except IndexError:
@@ -539,10 +547,9 @@ class Sequence(Composite):
539
547
  if child.status != common.Status.INVALID:
540
548
  child.stop(common.Status.INVALID)
541
549
  self.initialise() # user specific initialisation
542
- elif self.memory and common.Status.RUNNING:
543
- assert self.current_child is not None # should never be true, help mypy out
550
+ elif self.memory and self.current_child is not None:
544
551
  index = self.children.index(self.current_child)
545
- elif not self.memory and common.Status.RUNNING:
552
+ elif not self.memory:
546
553
  self.current_child = self.children[0] if self.children else None
547
554
  else:
548
555
  # previous conditional checks should cover all variations
@@ -560,13 +567,19 @@ class Sequence(Composite):
560
567
  for node in child.tick():
561
568
  yield node
562
569
  if node is child and node.status != common.Status.SUCCESS:
563
- self.status = node.status
564
570
  if not self.memory:
565
571
  # invalidate the remainder of the sequence
566
572
  # i.e. kill dangling runners
567
573
  for child in itertools.islice(self.children, index + 1, None):
568
574
  if child.status != common.Status.INVALID:
569
575
  child.stop(common.Status.INVALID)
576
+
577
+ # stop the sequence if a terminal (non-success) state was reached
578
+ if node.status != common.Status.RUNNING:
579
+ self.stop(node.status)
580
+ else:
581
+ self.status = node.status
582
+
570
583
  yield self
571
584
  return
572
585
  try:
@@ -660,13 +673,13 @@ class Parallel(Composite):
660
673
  super(Parallel, self).__init__(name, children)
661
674
  self.policy = policy
662
675
 
663
- def setup(self, **kwargs: int) -> None:
676
+ def setup(self, **kwargs: typing.Any) -> None:
664
677
  """
665
678
  Detect before ticking whether the policy configuration is invalid.
666
679
 
667
680
  Args:
668
681
  **kwargs (:obj:`dict`): distribute arguments to this
669
- behaviour and in turn, all of it's children
682
+ behaviour and in turn, all of its children
670
683
 
671
684
  Raises:
672
685
  RuntimeError: if the parallel's policy configuration is invalid
@@ -726,17 +739,17 @@ class Parallel(Composite):
726
739
  self.current_child = failed_child
727
740
  new_status = common.Status.FAILURE
728
741
  except StopIteration:
729
- if type(self.policy) is common.ParallelPolicy.SuccessOnAll:
730
- if all([c.status == common.Status.SUCCESS for c in self.children]):
742
+ if isinstance(self.policy, common.ParallelPolicy.SuccessOnAll):
743
+ if all(c.status == common.Status.SUCCESS for c in self.children):
731
744
  new_status = common.Status.SUCCESS
732
745
  self.current_child = self.children[-1]
733
- elif type(self.policy) is common.ParallelPolicy.SuccessOnOne:
746
+ elif isinstance(self.policy, common.ParallelPolicy.SuccessOnOne):
734
747
  for child in reversed(self.children):
735
748
  if child.status == common.Status.SUCCESS:
736
749
  new_status = common.Status.SUCCESS
737
750
  self.current_child = child
738
751
  break
739
- elif type(self.policy) is common.ParallelPolicy.SuccessOnSelected:
752
+ elif isinstance(self.policy, common.ParallelPolicy.SuccessOnSelected):
740
753
  if all(
741
754
  [c.status == common.Status.SUCCESS for c in self.policy.children]
742
755
  ):
@@ -770,7 +783,7 @@ class Parallel(Composite):
770
783
  # clean up dangling (running) children
771
784
  for child in self.children:
772
785
  if child.status == common.Status.RUNNING:
773
- # this unfortunately knocks out it's running status for introspection
786
+ # this unfortunately knocks out its running status for introspection
774
787
  # but logically is the correct thing to do, see #132.
775
788
  child.stop(common.Status.INVALID)
776
789
  Composite.stop(self, new_status)
@@ -786,7 +799,7 @@ class Parallel(Composite):
786
799
  Raises:
787
800
  RuntimeError: if policy configuration was invalid
788
801
  """
789
- if type(self.policy) is common.ParallelPolicy.SuccessOnSelected:
802
+ if isinstance(self.policy, common.ParallelPolicy.SuccessOnSelected):
790
803
  if not self.policy.children:
791
804
  error_message = (
792
805
  "policy SuccessOnSelected requires a non-empty "
@@ -36,6 +36,7 @@ Decorators with specific functionality:
36
36
  * :class:`py_trees.decorators.Condition`
37
37
  * :class:`py_trees.decorators.Count`
38
38
  * :class:`py_trees.decorators.EternalGuard`
39
+ * :class:`py_trees.decorators.ForEach`
39
40
  * :class:`py_trees.decorators.Inverter`
40
41
  * :class:`py_trees.decorators.OneShot`
41
42
  * :class:`py_trees.decorators.Repeat`
@@ -61,7 +62,7 @@ or :data:`~py_trees.common.Status.FAILURE`.
61
62
 
62
63
  A decorator, such as :func:`py_trees.decorators.RunningIsSuccess` on
63
64
  a blocking behaviour will immediately terminate the underlying child and
64
- re-intialise on it's next tick. This is often surprising (to the user) but
65
+ re-intialise on its next tick. This is often surprising (to the user) but
65
66
  is necessary to ensure the underlying child isn't left in a dangling state (i.e.
66
67
  :data:`~py_trees.common.Status.RUNNING`) as subsequent ticks move on to other
67
68
  parts of the tree.
@@ -80,6 +81,7 @@ import functools
80
81
  import inspect
81
82
  import time
82
83
  import typing
84
+ from collections.abc import Iterable
83
85
 
84
86
  from . import behaviour, blackboard, common
85
87
 
@@ -473,7 +475,7 @@ class Timeout(Decorator):
473
475
  :meth:`~py_trees.behaviour.Behaviour.stop` method is called with
474
476
  status :data:`~py_trees.common.Status.FAILURE` otherwise it will
475
477
  simply directly tick and return with the same status
476
- as that of it's encapsulated behaviour.
478
+ as that of its encapsulated behaviour.
477
479
  """
478
480
 
479
481
  def __init__(self, name: str, child: behaviour.Behaviour, duration: float = 5.0):
@@ -528,10 +530,10 @@ class Timeout(Decorator):
528
530
 
529
531
  class Count(Decorator):
530
532
  """
531
- Count the number of times it's child has been ticked.
533
+ Count the number of times its child has been ticked.
532
534
 
533
535
  This increments counters tracking the total number of times
534
- it's child has been ticked as well as the number of times it
536
+ its child has been ticked as well as the number of times it
535
537
  has landed in each respective state.
536
538
 
537
539
  It will always re-zero counters on
@@ -560,7 +562,7 @@ class Count(Decorator):
560
562
  self.running_count = 0
561
563
  self.interrupt_count = 0
562
564
 
563
- def setup(self, **kwargs: int) -> None:
565
+ def setup(self, **kwargs: typing.Any) -> None:
564
566
  """Reset the counters."""
565
567
  self.total_tick_count = 0
566
568
  self.failure_count = 0
@@ -619,7 +621,7 @@ class OneShot(Decorator):
619
621
 
620
622
  This decorator ensures that the underlying child is ticked through
621
623
  to completion just once and while doing so, will return
622
- with the same status as it's child. Thereafter it will return
624
+ with the same status as its child. Thereafter it will return
623
625
  with the final status of the underlying child.
624
626
 
625
627
  Completion status is determined by the policy given on construction.
@@ -664,7 +666,7 @@ class OneShot(Decorator):
664
666
  Tick the child or bounce back with the original status if already completed.
665
667
 
666
668
  Yields:
667
- a reference to itself or a behaviour in it's child subtree
669
+ a reference to itself or a behaviour in its child subtree
668
670
  """
669
671
  if self.final_status:
670
672
  # ignore the child
@@ -856,7 +858,7 @@ class Condition(Decorator):
856
858
  """
857
859
  A blocking conditional decorator.
858
860
 
859
- Encapsulates a behaviour and wait for it's status to flip to the
861
+ Encapsulates a behaviour and wait for its status to flip to the
860
862
  desired state. This behaviour will tick with
861
863
  :data:`~py_trees.common.Status.RUNNING` while waiting and
862
864
  :data:`~py_trees.common.Status.SUCCESS` when the flip occurs.
@@ -920,3 +922,74 @@ class PassThrough(Decorator):
920
922
  the behaviour's new status :class:`~py_trees.common.Status`
921
923
  """
922
924
  return self.decorated.status
925
+
926
+
927
+ class ForEach(Decorator):
928
+ """
929
+ Run the child behavior for each item in an iterable.
930
+
931
+ On initialization, the iterable is loaded from the blackboard and an
932
+ iterator is created. Every time the child succeeds, we advance to
933
+ the next item. We keep running until the iterable is exhausted.
934
+ """
935
+
936
+ def __init__(
937
+ self, name: str, child: behaviour.Behaviour, source_key: str, target_key: str
938
+ ):
939
+ """
940
+ Initialise the ForEach decorator.
941
+
942
+ Args:
943
+ name (:obj:`str`): name of the behaviour
944
+ child (:obj:`Behaviour`): the child behaviour to decorate
945
+ source_key (:obj:`str`): blackboard key to read the input iterable
946
+ target_key (:obj:`str`): blackboard key to set for each iteration
947
+ """
948
+ super().__init__(name=name, child=child)
949
+ self.source_key = source_key
950
+ self.target_key = target_key
951
+ self.blackboard = blackboard.Client(name=name)
952
+ self.blackboard.register_key(key=self.source_key, access=common.Access.READ)
953
+ self.blackboard.register_key(key=self.target_key, access=common.Access.WRITE)
954
+ self._iterator: typing.Iterator | None = None
955
+ self._current_item: typing.Any | None = None
956
+
957
+ def initialise(self) -> None:
958
+ """Reset iteration on first tick."""
959
+ iterable = self.blackboard.get(self.source_key) or []
960
+ if not isinstance(iterable, Iterable):
961
+ raise TypeError(
962
+ f"[{self.name}] source_key '{self.source_key}' is not an iterable"
963
+ )
964
+ self._iterator = iter(iterable)
965
+ self._advance()
966
+
967
+ def _advance(self) -> None:
968
+ """Advance to the next item in the iterator, or mark as finished."""
969
+ try:
970
+ if self._iterator is not None:
971
+ self._current_item = next(self._iterator)
972
+ self.blackboard.set(self.target_key, self._current_item)
973
+ except StopIteration:
974
+ self._current_item = None
975
+
976
+ def update(self) -> common.Status:
977
+ """Execute the child for the current item and manage iteration state."""
978
+ if self._current_item is None:
979
+ # no more items left
980
+ return common.Status.SUCCESS
981
+
982
+ child_status = self.decorated.status
983
+
984
+ if child_status == common.Status.SUCCESS:
985
+ # move to next item
986
+ self._advance()
987
+ if self._current_item is not None:
988
+ return common.Status.RUNNING
989
+
990
+ return child_status
991
+
992
+ def terminate(self, new_status: common.Status) -> None:
993
+ """Reset on termination."""
994
+ self._iterator = None
995
+ self._current_item = None
@@ -144,7 +144,7 @@ class Action(py_trees.behaviour.Behaviour):
144
144
  super(Action, self).__init__(name)
145
145
  self.logger.debug("%s.__init__()" % (self.__class__.__name__))
146
146
 
147
- def setup(self, **kwargs: int) -> None:
147
+ def setup(self, **kwargs: typing.Any) -> None:
148
148
  """Kickstart the separate process this behaviour will work with.
149
149
 
150
150
  Ordinarily this process will be already running. In this case,
@@ -218,7 +218,7 @@ class ParamsAndState(py_trees.behaviour.Behaviour):
218
218
 
219
219
  def create_root() -> py_trees.behaviour.Behaviour:
220
220
  """
221
- Create the root behaviour and it's subtree.
221
+ Create the root behaviour and its subtree.
222
222
 
223
223
  Returns:
224
224
  the root behaviour
@@ -165,7 +165,7 @@ def main() -> None:
165
165
  "-------------------------------------------------------------------------------"
166
166
  )
167
167
  foo.awesome = True
168
- # Only accessable via set since it's not in the namespace
168
+ # Only accessible via set since it's not in the namespace
169
169
  foo.set("/brilliant", False)
170
170
  # This will fail since it looks for the namespaced /foo/brilliant key
171
171
  # foo.brilliant = False
@@ -163,7 +163,7 @@ class ContextSwitch(py_trees.behaviour.Behaviour):
163
163
 
164
164
  def create_root() -> py_trees.behaviour.Behaviour:
165
165
  """
166
- Create the root behaviour and it's subtree.
166
+ Create the root behaviour and its subtree.
167
167
 
168
168
  Returns:
169
169
  the root behaviour
@@ -140,7 +140,7 @@ def command_line_argument_parser() -> argparse.ArgumentParser:
140
140
 
141
141
  def create_tree(level: str) -> py_trees.behaviour.Behaviour:
142
142
  """
143
- Create the root behaviour and it's subtree.
143
+ Create the root behaviour and its subtree.
144
144
 
145
145
  Returns:
146
146
  the root behaviour
@@ -147,7 +147,7 @@ def post_tick_handler(
147
147
 
148
148
  def create_root() -> py_trees.behaviour.Behaviour:
149
149
  """
150
- Create the root behaviour and it's subtree.
150
+ Create the root behaviour and its subtree.
151
151
 
152
152
  Returns:
153
153
  the root behaviour
@@ -142,7 +142,7 @@ def post_tick_handler(
142
142
 
143
143
  def create_root() -> py_trees.behaviour.Behaviour:
144
144
  """
145
- Create the root behaviour and it's subtree.
145
+ Create the root behaviour and its subtree.
146
146
 
147
147
  Returns:
148
148
  the root behaviour
@@ -107,7 +107,7 @@ class Counter(py_trees.behaviour.Behaviour):
107
107
  super(Counter, self).__init__(name)
108
108
  self.logger.debug("%s.__init__()" % (self.__class__.__name__))
109
109
 
110
- def setup(self, **kwargs: int) -> None:
110
+ def setup(self, **kwargs: typing.Any) -> None:
111
111
  """No delayed initialisation required for this example."""
112
112
  self.logger.debug("%s.setup()" % (self.__class__.__name__))
113
113
 
@@ -167,7 +167,7 @@ def logger(
167
167
 
168
168
  def create_tree() -> py_trees.behaviour.Behaviour:
169
169
  """
170
- Create the root behaviour and it's subtree.
170
+ Create the root behaviour and its subtree.
171
171
 
172
172
  Returns:
173
173
  the root behaviour
@@ -146,7 +146,7 @@ def post_tick_handler(
146
146
 
147
147
  def create_root() -> py_trees.behaviour.Behaviour:
148
148
  """
149
- Create the root behaviour and it's subtree.
149
+ Create the root behaviour and its subtree.
150
150
 
151
151
  Returns:
152
152
  the root behaviour
@@ -104,7 +104,7 @@ def command_line_argument_parser() -> argparse.ArgumentParser:
104
104
 
105
105
  def create_root() -> py_trees.behaviour.Behaviour:
106
106
  """
107
- Create the root behaviour and it's subtree.
107
+ Create the root behaviour and its subtree.
108
108
 
109
109
  Returns:
110
110
  the root behaviour
@@ -101,7 +101,7 @@ def command_line_argument_parser() -> argparse.ArgumentParser:
101
101
 
102
102
  def create_root() -> py_trees.behaviour.Behaviour:
103
103
  """
104
- Create the root behaviour and it's subtree.
104
+ Create the root behaviour and its subtree.
105
105
 
106
106
  Returns:
107
107
  the root behaviour
@@ -203,7 +203,7 @@ class Finisher(py_trees.behaviour.Behaviour):
203
203
 
204
204
  def create_tree() -> py_trees.behaviour.Behaviour:
205
205
  """
206
- Create the root behaviour and it's subtree.
206
+ Create the root behaviour and its subtree.
207
207
 
208
208
  Returns:
209
209
  the root behaviour
@@ -86,7 +86,7 @@ def tick_tree(
86
86
  to_tick: with from_tick, used to determine the # ticks required
87
87
  visitors: a list of visitors to run on each tree tick
88
88
  print_snapshot: print ascii/unicode snapshots after each tick
89
- print_blackboard: display the blackboard and it's update after each tick
89
+ print_blackboard: display the blackboard and its update after each tick
90
90
  """
91
91
  if visitors is None:
92
92
  visitors = []
@@ -51,7 +51,7 @@ def setup(
51
51
  root: behaviour.Behaviour,
52
52
  timeout: typing.Union[float, common.Duration] = common.Duration.INFINITE,
53
53
  visitor: typing.Optional[visitors.VisitorBase] = None,
54
- **kwargs: int,
54
+ **kwargs: typing.Any,
55
55
  ) -> None:
56
56
  """
57
57
  Crawl across a (sub)tree of behaviours calling :meth:`~py_trees.behaviour.Behaviour.setup` on each behaviour.
@@ -65,7 +65,7 @@ def setup(
65
65
  Args:
66
66
  root: unmanaged (sub)tree root behaviour
67
67
  timeout: time (s) to wait (use common.Duration.INFINITE to block indefinitely)
68
- visitor: runnable entities on each node after it's setup
68
+ visitor: runnable entities on each node after its setup
69
69
  **kwargs: dictionary of args to distribute to all behaviours in the (sub)tree
70
70
 
71
71
  Raises:
@@ -355,7 +355,7 @@ class BehaviourTree(object):
355
355
  self,
356
356
  timeout: typing.Union[float, common.Duration] = common.Duration.INFINITE,
357
357
  visitor: typing.Optional[visitors.VisitorBase] = None,
358
- **kwargs: int,
358
+ **kwargs: typing.Any,
359
359
  ) -> None:
360
360
  """
361
361
  Crawl across the tree calling :meth:`~py_trees.behaviour.Behaviour.setup` on each behaviour.
@@ -368,8 +368,8 @@ class BehaviourTree(object):
368
368
 
369
369
  Args:
370
370
  timeout: time (s) to wait (use common.Duration.INFINITE to block indefinitely)
371
- visitor: runnable entities on each node after it's setup
372
- **kwargs: distribute args to this behaviour and in turn, to it's children
371
+ visitor: runnable entities on each node after its setup
372
+ **kwargs: distribute args to this behaviour and in turn, to its children
373
373
 
374
374
  Raises:
375
375
  Exception: be ready to catch if any of the behaviours raise an exception
@@ -14,4 +14,4 @@
14
14
 
15
15
  # When changing, Also update setup.py and package.xml
16
16
  # TODO: use pkg_resources to fetch the version from setup.py
17
- __version__ = "2.3.0"
17
+ __version__ = "2.4.0"
@@ -174,7 +174,7 @@ class SnapshotVisitor(VisitorBase):
174
174
 
175
175
  class DisplaySnapshotVisitor(SnapshotVisitor):
176
176
  """
177
- Visit the tree, capturing the visited path, it's changes since the last tick.
177
+ Visit the tree, capturing the visited path and its changes since the last tick.
178
178
 
179
179
  Additionally print the snapshot to console.
180
180
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "py_trees"
3
- version = "2.3.0"
3
+ version = "2.4.0"
4
4
  description = "pythonic implementation of behaviour trees"
5
5
  authors = ["Daniel Stonier", "Naveed Usmani", "Michal Staniaszek"]
6
6
  maintainers = ["Daniel Stonier <d.stonier@gmail.com>", "Sebastian Castro <sebas.a.castro@gmail.com>"]
@@ -25,7 +25,6 @@ keywords=["py_trees", "py-trees", "behaviour-trees"]
25
25
  [tool.poetry.dependencies]
26
26
  python = ">=3.9,<4.0"
27
27
  pydot = ">=1.4"
28
- py = "^1.11.0"
29
28
 
30
29
  [tool.poetry.group.dev.dependencies]
31
30
  tox = ">=3.26"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes