Skip to content

Commit 3697776

Browse files
committed
add examples
1 parent 8f64d5c commit 3697776

5 files changed

Lines changed: 221 additions & 3 deletions

File tree

python/CONTRIBUTING.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ pytest benchmarks/ --benchmark-only
3030

3131
Results written in `benchmarks/.benchmarks/` (git-ignored). See [`benchmarks/README.md`](benchmarks/README.md).
3232

33+
## Run examples
34+
35+
```bash
36+
python examples/t01_build_your_first_tree.py
37+
python examples/t02_basic_ports.py
38+
python examples/t03_passing_data.py
39+
```
40+
41+
Each script prints what it's doing and exits 0 on success. `pytest tests/test_examples.py` runs all three in subprocesses and asserts clean exits — that's the rot-prevention gate.
42+
3343
## Pre-commit hooks
3444

3545
Install once:
@@ -51,14 +61,14 @@ This runs `ruff`, `mypy`, the no-C++-refs check, and the project's standard hook
5161
|---|---|
5262
| `src/pybt/` | Python package (the user-facing surface) |
5363
| `src/_pybt/` | C++ binding code (nanobind) |
54-
| `tests/` | pytest suite — smoke + lifecycle |
64+
| `tests/` | pytest suite — smoke + lifecycle + example runner |
65+
| `examples/` | Runnable tutorial scripts (t01..t03 so far) |
5566
| `benchmarks/` | pytest-benchmark microbenchmarks |
56-
| `docs/` | Sphinx site (stub in Phase 1) |
67+
| `docs/` | Sphinx site (stub for now) |
5768
| `pyproject.toml` | Build config (scikit-build-core, nanobind, pytest) |
5869
| `CMakeLists.txt` | nanobind extension + CTest regression guards |
5970

6071
## Style
6172

6273
- Python: `ruff` for lint and format, `mypy` for types.
6374
- C++: project root `.clang-format` (Google C++ with 2-space indent, 90-char line limit).
64-
- Docs: per the Documentation Standards in the project plan — standalone, brief, no C++ references outside this file and `README.md`.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""t01 — Build your first tree.
2+
3+
Shows the minimum needed to register a Python action, build a tree from
4+
XML, and tick it to completion.
5+
6+
This tree runs two custom actions in sequence: a "check" that returns
7+
SUCCESS or FAILURE based on a boolean, and a "say" that prints a line.
8+
9+
Run: python t01_build_your_first_tree.py
10+
Expected output:
11+
[check] battery_ok=True
12+
[say] hello from pybt
13+
final status: SUCCESS
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import pybt
19+
20+
XML = """
21+
<root BTCPP_format="4">
22+
<BehaviorTree ID="Main">
23+
<Sequence>
24+
<CheckBatteryOk/>
25+
<SaySomething/>
26+
</Sequence>
27+
</BehaviorTree>
28+
</root>
29+
"""
30+
31+
32+
class CheckBatteryOk(pybt.SyncActionNode):
33+
def tick(self) -> pybt.NodeStatus:
34+
battery_ok = True
35+
print(f"[check] battery_ok={battery_ok}")
36+
return pybt.NodeStatus.SUCCESS if battery_ok else pybt.NodeStatus.FAILURE
37+
38+
39+
class SaySomething(pybt.SyncActionNode):
40+
def tick(self) -> pybt.NodeStatus:
41+
print("[say] hello from pybt")
42+
return pybt.NodeStatus.SUCCESS
43+
44+
45+
def main() -> int:
46+
factory = pybt.BehaviorTreeFactory()
47+
factory.register_node_type(CheckBatteryOk, "CheckBatteryOk")
48+
factory.register_node_type(SaySomething, "SaySomething")
49+
tree = factory.create_tree_from_text(XML)
50+
status = tree.tick_while_running()
51+
print(f"final status: {status.name}")
52+
return 0 if status == pybt.NodeStatus.SUCCESS else 1
53+
54+
55+
if __name__ == "__main__":
56+
raise SystemExit(main())

python/examples/t02_basic_ports.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""t02 — Basic ports.
2+
3+
Shows how to declare input and output ports on a custom action and pass
4+
data between nodes through the blackboard.
5+
6+
The producer writes a string to its `out` port; the consumer reads the
7+
same value from its `in` port and prints it.
8+
9+
Run: python t02_basic_ports.py
10+
Expected output:
11+
[consume] got: hello world
12+
final status: SUCCESS
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import pybt
18+
19+
XML = """
20+
<root BTCPP_format="4">
21+
<BehaviorTree ID="Main">
22+
<Sequence>
23+
<Produce out="{shared}"/>
24+
<Consume in="{shared}"/>
25+
</Sequence>
26+
</BehaviorTree>
27+
</root>
28+
"""
29+
30+
31+
@pybt.ports(outputs=["out"])
32+
class Produce(pybt.SyncActionNode):
33+
def tick(self) -> pybt.NodeStatus:
34+
self.set_output("out", "hello world")
35+
return pybt.NodeStatus.SUCCESS
36+
37+
38+
@pybt.ports(inputs=["in"])
39+
class Consume(pybt.SyncActionNode):
40+
def tick(self) -> pybt.NodeStatus:
41+
value = self.get_input("in")
42+
print(f"[consume] got: {value}")
43+
return pybt.NodeStatus.SUCCESS
44+
45+
46+
def main() -> int:
47+
factory = pybt.BehaviorTreeFactory()
48+
factory.register_node_type(Produce, "Produce")
49+
factory.register_node_type(Consume, "Consume")
50+
tree = factory.create_tree_from_text(XML)
51+
status = tree.tick_while_running()
52+
print(f"final status: {status.name}")
53+
return 0 if status == pybt.NodeStatus.SUCCESS else 1
54+
55+
56+
if __name__ == "__main__":
57+
raise SystemExit(main())
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""t03 — Passing multiple values via separate ports.
2+
3+
Extends t02 with a stateful producer (counts ticks before reporting a
4+
pose) and multiple primitive ports passed to a single consumer.
5+
6+
A later release adds `register_type` for sending custom Python classes
7+
through a single port — until then, pass each field as its own primitive
8+
port (string / int / float / bool).
9+
10+
Run: python t03_passing_data.py
11+
Expected output:
12+
[navigate] heading to (1.5, 2.5) at 0.8 m/s
13+
final status: SUCCESS
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import pybt
19+
20+
XML = """
21+
<root BTCPP_format="4">
22+
<BehaviorTree ID="Main">
23+
<Sequence>
24+
<PlanPose x="{x}" y="{y}" speed="{speed}"/>
25+
<Navigate x="{x}" y="{y}" speed="{speed}"/>
26+
</Sequence>
27+
</BehaviorTree>
28+
</root>
29+
"""
30+
31+
32+
@pybt.ports(outputs=["x", "y", "speed"])
33+
class PlanPose(pybt.SyncActionNode):
34+
def tick(self) -> pybt.NodeStatus:
35+
self.set_output("x", 1.5)
36+
self.set_output("y", 2.5)
37+
self.set_output("speed", 0.8)
38+
return pybt.NodeStatus.SUCCESS
39+
40+
41+
@pybt.ports(inputs=["x", "y", "speed"])
42+
class Navigate(pybt.SyncActionNode):
43+
def tick(self) -> pybt.NodeStatus:
44+
x = float(self.get_input("x"))
45+
y = float(self.get_input("y"))
46+
speed = float(self.get_input("speed"))
47+
print(f"[navigate] heading to ({x}, {y}) at {speed} m/s")
48+
return pybt.NodeStatus.SUCCESS
49+
50+
51+
def main() -> int:
52+
factory = pybt.BehaviorTreeFactory()
53+
factory.register_node_type(PlanPose, "PlanPose")
54+
factory.register_node_type(Navigate, "Navigate")
55+
tree = factory.create_tree_from_text(XML)
56+
status = tree.tick_while_running()
57+
print(f"final status: {status.name}")
58+
return 0 if status == pybt.NodeStatus.SUCCESS else 1
59+
60+
61+
if __name__ == "__main__":
62+
raise SystemExit(main())

python/tests/test_examples.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Runs every example script in a subprocess and asserts exit 0.
2+
3+
Examples are the user-facing front door — if any of them stops working
4+
end-to-end, this test fails before the regression reaches a user.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import subprocess
10+
import sys
11+
from pathlib import Path
12+
13+
import pytest
14+
15+
pytestmark = pytest.mark.smoke
16+
17+
EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "examples"
18+
EXAMPLES = sorted(EXAMPLES_DIR.glob("t*.py"))
19+
20+
21+
@pytest.mark.parametrize("script", EXAMPLES, ids=lambda p: p.name)
22+
def test_example_runs_clean(script: Path) -> None:
23+
result = subprocess.run(
24+
[sys.executable, str(script)],
25+
capture_output=True,
26+
text=True,
27+
timeout=15,
28+
)
29+
assert result.returncode == 0, (
30+
f"{script.name} exited {result.returncode}\n"
31+
f"stdout:\n{result.stdout}\n"
32+
f"stderr:\n{result.stderr}"
33+
)

0 commit comments

Comments
 (0)