From 411c97c2bd10e556a96f14920250c5b81717b63d Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Fri, 15 May 2026 19:19:41 -0500 Subject: [PATCH 01/25] create initial scaffolding --- .gitignore | 4 ++ Cargo.toml | 2 +- bonsai-py/Cargo.toml | 22 ++++++++++ bonsai-py/README.md | 19 ++++++++ bonsai-py/pyproject.toml | 41 ++++++++++++++++++ bonsai-py/python/bonsai_py/__init__.py | 10 +++++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 522 bytes bonsai-py/python/bonsai_py/bonsai_py.abi3.so | Bin 0 -> 512888 bytes bonsai-py/python/bonsai_py/py.typed | 0 bonsai-py/src/lib.rs | 10 +++++ 10 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 bonsai-py/Cargo.toml create mode 100644 bonsai-py/README.md create mode 100644 bonsai-py/pyproject.toml create mode 100644 bonsai-py/python/bonsai_py/__init__.py create mode 100644 bonsai-py/python/bonsai_py/__pycache__/__init__.cpython-312.pyc create mode 100755 bonsai-py/python/bonsai_py/bonsai_py.abi3.so create mode 100644 bonsai-py/python/bonsai_py/py.typed create mode 100644 bonsai-py/src/lib.rs diff --git a/.gitignore b/.gitignore index cbed4d7..c7deb0c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ Cargo.lock **/*.rs.bk .idea/ + +# Python virtual environments +.venv/ +venv/ diff --git a/Cargo.toml b/Cargo.toml index 3dea7bd..cb0fed6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["bonsai", "examples"] +members = ["bonsai", "examples", "bonsai-py"] diff --git a/bonsai-py/Cargo.toml b/bonsai-py/Cargo.toml new file mode 100644 index 0000000..646741c --- /dev/null +++ b/bonsai-py/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bonsai-py" +version = "0.12.0" +edition = "2021" +rust-version = "1.80.0" +description = "Python bindings for the bonsai-bt behavior tree library" +license = "MIT" +authors = [ + "Kristoffer Solberg Rakstad ", + "Anmol Kathail ", +] +repository = "https://github.com/sollimann/bonsai.git" +homepage = "https://github.com/sollimann/bonsai" +publish = false + +[lib] +name = "bonsai_py" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.28", features = ["extension-module", "abi3-py310"] } +bonsai-bt = { path = "../bonsai", version = "0.12", features = ["visualize"] } diff --git a/bonsai-py/README.md b/bonsai-py/README.md new file mode 100644 index 0000000..c3cc03c --- /dev/null +++ b/bonsai-py/README.md @@ -0,0 +1,19 @@ +# bonsai-py - Python bindings + +Python bindings for the [bonsai-bt](https://github.com/sollimann/bonsai) +behavior-tree library. + +## Installation (dev) + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1 +pip install maturin +cd bonsai-py +maturin develop +python -c "import bonsai_py; print(bonsai_py.__version__)" +``` + +## License + +MIT - see [LICENSE](../LICENSE). diff --git a/bonsai-py/pyproject.toml b/bonsai-py/pyproject.toml new file mode 100644 index 0000000..3f79420 --- /dev/null +++ b/bonsai-py/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["maturin>=1.7,<2.0"] +build-backend = "maturin" + +[project] +name = "bonsai-py" +description = "Behavior trees in Python, powered by the bonsai-bt Rust crate." +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.10" +authors = [ + { name = "Kristoffer Solberg Rakstad", email = "kristoffer.solberg@cognite.com" }, + { name = "Anmol Kathail", email = "anmolkathail@gmail.com" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Rust", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Games/Entertainment", + "Typing :: Typed", +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/sollimann/bonsai" +Repository = "https://github.com/sollimann/bonsai" +Issues = "https://github.com/sollimann/bonsai/issues" + +[tool.maturin] +module-name = "bonsai_py" +python-source = "python" +features = ["pyo3/extension-module"] +strip = true diff --git a/bonsai-py/python/bonsai_py/__init__.py b/bonsai-py/python/bonsai_py/__init__.py new file mode 100644 index 0000000..2a69a46 --- /dev/null +++ b/bonsai-py/python/bonsai_py/__init__.py @@ -0,0 +1,10 @@ +"""bonsai-py - behavior trees in Python, powered by the Rust bonsai-bt crate.""" + +from importlib.metadata import PackageNotFoundError, version as _version + +try: + __version__ = _version("bonsai-py") +except PackageNotFoundError: # editable install before metadata is in place + __version__ = "0.0.0+unknown" + +from .bonsai_py import * # noqa: F401,F403 (re-export the compiled module) diff --git a/bonsai-py/python/bonsai_py/__pycache__/__init__.cpython-312.pyc b/bonsai-py/python/bonsai_py/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b183b502f92ec908ebca3b7a48f7eba815fa192d GIT binary patch literal 522 zcmX|7F>ljA6n@WkY8QwqMJ%ltjt~h2XxxQ~imiaC6h&#K&z7Ex-Sf`Yn@3+ecaQgf1%R8smp%lyegb0mTj%!4eogPLiW^pW O8f?K^zvsTgp8E%$SD4-a literal 0 HcmV?d00001 diff --git a/bonsai-py/python/bonsai_py/bonsai_py.abi3.so b/bonsai-py/python/bonsai_py/bonsai_py.abi3.so new file mode 100755 index 0000000000000000000000000000000000000000..ec129265ecc3ecd2ee512c0303f94e56ef187baf GIT binary patch literal 512888 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≫It8L;YkMu6#6xT@%1 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%0A3w*E`Mq-q%vMxdJ>0QahJ3do&d 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

ZJO}SCct}|Io9Ue*FFnRYcFY+ZQ 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 From c50f10346109233da64b57e2eceb7af9bc6f6a3b Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Fri, 15 May 2026 20:09:38 -0500 Subject: [PATCH 03/25] add status enum, and action args --- bonsai-py/src/action_args.rs | 41 ++++++++++++++++++++++++++++++ bonsai-py/src/lib.rs | 10 +++++++- bonsai-py/src/status.rs | 49 ++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 bonsai-py/src/action_args.rs create mode 100644 bonsai-py/src/status.rs diff --git a/bonsai-py/src/action_args.rs b/bonsai-py/src/action_args.rs new file mode 100644 index 0000000..65d86bb --- /dev/null +++ b/bonsai-py/src/action_args.rs @@ -0,0 +1,41 @@ +use bonsai_bt::{ActionArgs, Event}; +use pyo3::prelude::*; + +/// Action callback arguments. +/// +/// Constructed by the tick bridge and passed to the user's callback. +/// The Rust `ActionArgs::event` field is intentionally not exposed — +/// Python users only see `dt` and `action`. +#[pyclass(frozen, name = "ActionArgs")] +pub struct PyActionArgs { + /// Remaining delta time in seconds. + #[pyo3(get)] + pub dt: f64, + /// The user-supplied action value (whatever was passed to `bt.Action(...)`). + #[pyo3(get)] + pub action: Py, +} + +#[pymethods] +impl PyActionArgs { + #[new] + fn py_new(dt: f64, action: Py) -> Self { + Self { dt, action } + } + + fn __repr__(&self, py: Python<'_>) -> PyResult { + let action_repr = self.action.bind(py).repr()?.to_string(); + Ok(format!("ActionArgs(dt={}, action={})", self.dt, action_repr)) + } +} + +impl PyActionArgs { + /// Build a `PyActionArgs` from the Rust `ActionArgs` that the tick + /// callback receives. Hot path — one `clone_ref` plus an `f64` copy. + pub fn from_rust(args: &ActionArgs>, py: Python<'_>) -> Self { + Self { + dt: args.dt, + action: args.action.clone_ref(py), + } + } +} diff --git a/bonsai-py/src/lib.rs b/bonsai-py/src/lib.rs index 34d6cec..9d796b0 100644 --- a/bonsai-py/src/lib.rs +++ b/bonsai-py/src/lib.rs @@ -1,10 +1,18 @@ use pyo3::prelude::*; +mod action_args; +mod status; + +use action_args::PyActionArgs; +use status::PyStatus; + /// Python bindings for the bonsai-bt behavior-tree library. /// /// Construct trees with the factory functions (Sequence, Action, Wait, ...), /// wrap one in `BT(tree, blackboard)`, and drive it with `bt.tick(dt, callback)`. #[pymodule] -fn bonsai_py(_py: Python<'_>, _m: &Bound<'_, PyModule>) -> PyResult<()> { +fn bonsai_py(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/bonsai-py/src/status.rs b/bonsai-py/src/status.rs new file mode 100644 index 0000000..7f6a0e2 --- /dev/null +++ b/bonsai-py/src/status.rs @@ -0,0 +1,49 @@ +use bonsai_bt::Status; +use pyo3::prelude::*; + +/// Behavior-tree node result. +/// +/// Mirrors `bonsai_bt::Status`. Comparable to `int` +/// (`Status.Success == 0`, `Failure == 1`, `Running == 2`) and usable +/// as a `dict` key or `set` member. +#[pyclass(eq, eq_int, hash, frozen, from_py_object, name = "Status")] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum PyStatus { + Success, + Failure, + Running, +} + +impl From for PyStatus { + fn from(s: Status) -> Self { + match s { + Status::Success => PyStatus::Success, + Status::Failure => PyStatus::Failure, + Status::Running => PyStatus::Running, + } + } +} + +impl From for Status { + fn from(s: PyStatus) -> Self { + match s { + PyStatus::Success => Status::Success, + PyStatus::Failure => Status::Failure, + PyStatus::Running => Status::Running, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip_through_rust() { + for s in [Status::Success, Status::Failure, Status::Running] { + let py: PyStatus = s.into(); + let back: Status = py.into(); + assert_eq!(s, back); + } + } +} From 13cd7fd66b9b8f3f58919c9c5f475a226c46809e Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Fri, 15 May 2026 23:00:01 -0500 Subject: [PATCH 04/25] pickle --- bonsai-py/src/action_args.rs | 2 +- bonsai-py/src/status.rs | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/bonsai-py/src/action_args.rs b/bonsai-py/src/action_args.rs index 65d86bb..2f6244e 100644 --- a/bonsai-py/src/action_args.rs +++ b/bonsai-py/src/action_args.rs @@ -6,7 +6,7 @@ use pyo3::prelude::*; /// Constructed by the tick bridge and passed to the user's callback. /// The Rust `ActionArgs::event` field is intentionally not exposed — /// Python users only see `dt` and `action`. -#[pyclass(frozen, name = "ActionArgs")] +#[pyclass(frozen, module = "bonsai_py", name = "ActionArgs")] pub struct PyActionArgs { /// Remaining delta time in seconds. #[pyo3(get)] diff --git a/bonsai-py/src/status.rs b/bonsai-py/src/status.rs index 7f6a0e2..c52d8f0 100644 --- a/bonsai-py/src/status.rs +++ b/bonsai-py/src/status.rs @@ -6,7 +6,7 @@ use pyo3::prelude::*; /// Mirrors `bonsai_bt::Status`. Comparable to `int` /// (`Status.Success == 0`, `Failure == 1`, `Running == 2`) and usable /// as a `dict` key or `set` member. -#[pyclass(eq, eq_int, hash, frozen, from_py_object, name = "Status")] +#[pyclass(eq, eq_int, hash, frozen, from_py_object, module = "bonsai_py", name = "Status")] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub enum PyStatus { Success, @@ -14,6 +14,25 @@ pub enum PyStatus { Running, } +#[pymethods] +impl PyStatus { + /// Pickle support: name the singleton by class + variant name, since + /// PyO3 simple enums refuse construction by call (`Status(0)` raises). + fn __reduce__<'py>( + &self, + py: Python<'py>, + ) -> PyResult<(Bound<'py, PyAny>, (Bound<'py, PyAny>, &'static str))> { + let getattr = py.import("builtins")?.getattr("getattr")?; + let cls = py.get_type::().into_any(); + let name = match self { + PyStatus::Success => "Success", + PyStatus::Failure => "Failure", + PyStatus::Running => "Running", + }; + Ok((getattr, (cls, name))) + } +} + impl From for PyStatus { fn from(s: Status) -> Self { match s { From b58e318ff92a1f2774cf8bda1837bd9634cdd0d2 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Fri, 15 May 2026 23:31:51 -0500 Subject: [PATCH 05/25] add tests --- bonsai-py/scripts/verify.py | 217 ++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 bonsai-py/scripts/verify.py diff --git a/bonsai-py/scripts/verify.py b/bonsai-py/scripts/verify.py new file mode 100644 index 0000000..8b25061 --- /dev/null +++ b/bonsai-py/scripts/verify.py @@ -0,0 +1,217 @@ +r""" +Verification script for bonsai-py. + +WHAT THIS IS + Python-side assertions for the installed `bonsai_py` extension module. + Every test runs on every invocation; the script exits 0 on full pass, + 1 on the first failure, 2 on a usage error. + +USAGE + From the repository root, with a venv that has the wheel installed: + + source .venv/bin/activate # macOS / Linux / WSL + .\.venv\Scripts\Activate.ps1 # Windows PowerShell + + Then: + + python bonsai-py/scripts/verify.py + +PREREQUISITES + cargo check -p bonsai-py + cd bonsai-py && maturin develop + + `maturin develop` produces the abi3 wheel and installs it editable into + the active venv. This script imports the installed `bonsai_py` module — + if it fails with `ImportError`, you forgot to `maturin develop`. + +WHAT THIS IS NOT + Not a pytest suite. Long-term behavior coverage lives under + `bonsai-py/tests/` and is run via pytest separately. +""" + +from __future__ import annotations + +import sys + + +def test_module_scaffolding() -> None: + """Module imports cleanly; version metadata and docstring present.""" + import bonsai_py + + assert bonsai_py.__version__ == "0.12.0", bonsai_py.__version__ + print(" OK: __version__ == 0.12.0") + + assert bonsai_py.__doc__, "missing module docstring" + print(" OK: module docstring present") + + +def test_status_and_action_args() -> None: + """Status enum + ActionArgs class: semantics, pickle, copy, identity.""" + import copy + import pickle + + import bonsai_py as bt + + assert hasattr(bt, "Status") and hasattr(bt, "ActionArgs") + print(" OK: Status and ActionArgs exported") + + s = bt.Status + assert s.Success == s.Success + assert s.Success != s.Failure + assert s.Success == 0 and s.Failure == 1 and s.Running == 2 + assert int(s.Success) == 0 + assert hash(s.Success) == hash(s.Success) + assert {s.Success: "ok"}[s.Success] == "ok" + assert s.Success is s.Success + assert repr(s.Success) == "Status.Success" + print(" OK: Status semantics (eq, eq_int, hash, identity, repr)") + + assert bt.Status.__module__ == "bonsai_py", bt.Status.__module__ + assert bt.ActionArgs.__module__ == "bonsai_py", bt.ActionArgs.__module__ + print(" OK: __module__ == 'bonsai_py' on both classes") + + for v in (s.Success, s.Failure, s.Running): + assert pickle.loads(pickle.dumps(v)) is v + for v in (s.Success, s.Failure, s.Running): + assert copy.copy(v) is v + assert copy.deepcopy(v) is v + print(" OK: pickle / copy.copy / copy.deepcopy preserve Status singleton identity") + + a = bt.ActionArgs(0.5, "inc") + assert a.dt == 0.5 + assert a.action == "inc" + assert repr(a) == "ActionArgs(dt=0.5, action='inc')" + try: + a.dt = 0.7 + except AttributeError: + pass + else: + raise AssertionError("ActionArgs should be frozen (got mutation)") + print(" OK: ActionArgs construct + readonly + repr") + + bb = {"k": 1} + a2 = bt.ActionArgs(0.0, bb) + assert a2.action is bb, "action identity not preserved through FFI" + a2.action["k"] = 2 + assert bb["k"] == 2, "mutation via args.action did not persist" + print(" OK: action identity preserved across FFI boundary") + + +def test_behavior_factories() -> None: + """14 tree-construction factories: exported, build trees, validate inputs.""" + import bonsai_py as bt + + factories = ( + "Action", "Wait", "WaitForever", + "Invert", "AlwaysSucceed", + "Sequence", "Select", "WhenAll", "WhenAny", "After", "Race", + "If", "While", "WhileAll", + ) + for name in factories: + assert hasattr(bt, name), f"missing factory: {name}" + assert callable(getattr(bt, name)), f"not callable: {name}" + print(f" OK: all {len(factories)} factories exported") + + # 27-node tree mirroring the Rust visualizer-smoke example. + tree = bt.Sequence([ + bt.If( + bt.Action("low_hp"), + bt.AlwaysSucceed(bt.Action("flee")), + bt.Action("regroup"), + ), + bt.Select([ + bt.Sequence([ + bt.Action("acquire_target"), + bt.WhenAll([bt.Action("aim"), bt.Action("track")]), + ]), + bt.Race([bt.Action("dodge"), bt.Wait(2.0)]), + bt.Invert(bt.Action("enemy_visible")), + ]), + bt.While(bt.Action("has_ammo"), [bt.Action("fire"), bt.Wait(0.3)]), + bt.After([bt.Action("cooldown"), bt.Action("ready_signal")]), + bt.WhenAny([bt.Action("victory_check"), bt.Action("retreat_signal")]), + ]) + assert repr(tree) == "Sequence(5)" + print(" OK: 27-node nested tree constructs (5 top-level children)") + + wait = bt.Wait(1.0) + reuse = bt.Sequence([wait, wait, wait]) + assert repr(reuse) == "Sequence(3)" + assert repr(wait) == "Wait(1)" + print(" OK: subtree reuse (same Behavior referenced 3x)") + + for label, call in ( + ("While(cond, [])", lambda: bt.While(bt.Action("x"), [])), + ("WhileAll(cond, [])", lambda: bt.WhileAll(bt.Action("x"), [])), + ("Wait(NaN)", lambda: bt.Wait(float("nan"))), + ): + try: + call() + except ValueError: + pass + else: + raise AssertionError(f"{label} should raise ValueError") + print(" OK: validation guards (empty While/WhileAll body, NaN Wait)") + + # Pass-through cases — must NOT raise. + bt.Sequence([]); bt.Select([]) + bt.WhenAll([]); bt.WhenAny([]); bt.After([]); bt.Race([]) + bt.Wait(-1.0); bt.Wait(float("inf")); bt.Wait(1); bt.Wait(0.0) + print(" OK: empty composites + negative/inf/int/zero Wait pass through") + + tree2 = bt.If( + cond=bt.Action("c"), + on_success=bt.Action("s"), + on_failure=bt.Action("f"), + ) + assert repr(tree2) == "If(...)" + print(" OK: keyword arguments accepted on If") + + assert repr(bt.Action({"k": 1})) == "Action(...)" + assert repr(bt.Invert(bt.Action("x"))) == "Invert(...)" + assert repr(bt.WaitForever()) == "WaitForever" + print(" OK: __repr__ is one-line bounded summary") + + +# --------------------------------------------------------------------------- +# Registry — append new test functions above and add them to this list. +# Do not reorder or remove existing entries. +# --------------------------------------------------------------------------- +TESTS = [ + test_module_scaffolding, + test_status_and_action_args, + test_behavior_factories, +] + + +def main() -> int: + if len(sys.argv) > 1: + print( + f"usage: {sys.argv[0]} (no arguments; runs every test)", + file=sys.stderr, + ) + return 2 + + for fn in TESTS: + print(f"=== {fn.__name__} ===") + try: + fn() + except AssertionError as e: + print(f" FAIL: {e}", file=sys.stderr) + return 1 + except ImportError as e: + print( + f" FAIL: import error ({e}) — did you run `maturin develop`?", + file=sys.stderr, + ) + return 1 + except Exception as e: # noqa: BLE001 — surface anything unexpected + print(f" FAIL: {type(e).__name__}: {e}", file=sys.stderr) + return 1 + + print(f"\nAll {len(TESTS)} tests passed.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From a1523fef125567d535c1aac9dce16bf623a2eaa1 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Fri, 15 May 2026 23:48:15 -0500 Subject: [PATCH 06/25] add behaviors --- bonsai-py/scripts/verify.py | 31 ++++++ bonsai-py/src/behavior.rs | 207 ++++++++++++++++++++++++++++++++++++ bonsai-py/src/lib.rs | 20 ++++ 3 files changed, 258 insertions(+) create mode 100644 bonsai-py/src/behavior.rs diff --git a/bonsai-py/scripts/verify.py b/bonsai-py/scripts/verify.py index 8b25061..ab3148b 100644 --- a/bonsai-py/scripts/verify.py +++ b/bonsai-py/scripts/verify.py @@ -172,6 +172,37 @@ def test_behavior_factories() -> None: assert repr(bt.WaitForever()) == "WaitForever" print(" OK: __repr__ is one-line bounded summary") + assert bt.Behavior.__module__ == "bonsai_py", bt.Behavior.__module__ + print(" OK: Behavior.__module__ == 'bonsai_py'") + + assert repr(bt.Action(None)) == "Action(...)" + print(" OK: Action(None) is a legal leaf") + + # Identity-based equality holds for every variant. Two distinct + # constructions of the same shape must compare as distinct objects. + builders = [ + ("Action", lambda: bt.Action("x")), + ("Wait", lambda: bt.Wait(1.0)), + ("WaitForever", lambda: bt.WaitForever()), + ("Invert", lambda: bt.Invert(bt.Action("x"))), + ("AlwaysSucceed", lambda: bt.AlwaysSucceed(bt.Action("x"))), + ("Sequence", lambda: bt.Sequence([bt.Action("x")])), + ("Select", lambda: bt.Select([bt.Action("x")])), + ("WhenAll", lambda: bt.WhenAll([bt.Action("x")])), + ("WhenAny", lambda: bt.WhenAny([bt.Action("x")])), + ("After", lambda: bt.After([bt.Action("x")])), + ("Race", lambda: bt.Race([bt.Action("x")])), + ("If", lambda: bt.If(bt.Action("c"), bt.Action("s"), bt.Action("f"))), + ("While", lambda: bt.While(bt.Action("c"), [bt.Action("b")])), + ("WhileAll", lambda: bt.WhileAll(bt.Action("c"), [bt.Action("b")])), + ] + for label, make in builders: + a, b = make(), make() + assert a is not b, f"{label}: distinct constructions returned same object" + assert (a == b) is False, f"{label}: structural == returned True" + assert a != b, f"{label}: != returned False" + print(f" OK: identity-based equality across all {len(builders)} variants") + # --------------------------------------------------------------------------- # Registry — append new test functions above and add them to this list. diff --git a/bonsai-py/src/behavior.rs b/bonsai-py/src/behavior.rs new file mode 100644 index 0000000..456eca7 --- /dev/null +++ b/bonsai-py/src/behavior.rs @@ -0,0 +1,207 @@ +use bonsai_bt::Behavior; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +/// An opaque behavior-tree node. +/// +/// Construct via the factory functions (`Sequence`, `Action`, `Wait`, ...) +/// at the module level. Subtrees are reusable - the same `Behavior` +/// can appear as a child of multiple parents. +#[pyclass(unsendable, frozen, module = "bonsai_py", name = "Behavior")] +pub struct PyBehavior { + pub(crate) inner: Behavior>, +} + +impl PyBehavior { + fn wrap(inner: Behavior>) -> Self { + Self { inner } + } +} + +#[pymethods] +impl PyBehavior { + fn __repr__(&self) -> String { + match &self.inner { + Behavior::Wait(t) => format!("Wait({t})"), + Behavior::WaitForever => "WaitForever".to_string(), + Behavior::Action(_) => "Action(...)".to_string(), + Behavior::Invert(_) => "Invert(...)".to_string(), + Behavior::AlwaysSucceed(_) => "AlwaysSucceed(...)".to_string(), + Behavior::Select(v) => format!("Select({})", v.len()), + Behavior::If(_, _, _) => "If(...)".to_string(), + Behavior::Sequence(v) => format!("Sequence({})", v.len()), + Behavior::While(_, body) => format!("While({})", body.len()), + Behavior::WhileAll(_, body) => format!("WhileAll({})", body.len()), + Behavior::WhenAll(v) => format!("WhenAll({})", v.len()), + Behavior::WhenAny(v) => format!("WhenAny({})", v.len()), + Behavior::After(v) => format!("After({})", v.len()), + Behavior::Race(v) => format!("Race({})", v.len()), + } + } +} + +/// GIL-aware recursive clone of a `Behavior>` tree. +/// +/// `Py` is not `Clone` in PyO3 0.28 (clone() can't prove the GIL +/// is held), so the derived `Behavior::clone()` is unavailable for our +/// instantiation. This walks the tree manually, bumping the refcount on +/// each action via `clone_ref(py)`. +fn clone_tree(b: &Behavior>, py: Python<'_>) -> Behavior> { + match b { + Behavior::Wait(t) => Behavior::Wait(*t), + Behavior::WaitForever => Behavior::WaitForever, + Behavior::Action(a) => Behavior::Action(a.clone_ref(py)), + Behavior::Invert(c) => Behavior::Invert(Box::new(clone_tree(c, py))), + Behavior::AlwaysSucceed(c) => Behavior::AlwaysSucceed(Box::new(clone_tree(c, py))), + Behavior::Select(v) => Behavior::Select(clone_vec(v, py)), + Behavior::If(c, s, f) => Behavior::If( + Box::new(clone_tree(c, py)), + Box::new(clone_tree(s, py)), + Box::new(clone_tree(f, py)), + ), + Behavior::Sequence(v) => Behavior::Sequence(clone_vec(v, py)), + Behavior::While(c, body) => Behavior::While(Box::new(clone_tree(c, py)), clone_vec(body, py)), + Behavior::WhileAll(c, body) => { + Behavior::WhileAll(Box::new(clone_tree(c, py)), clone_vec(body, py)) + } + Behavior::WhenAll(v) => Behavior::WhenAll(clone_vec(v, py)), + Behavior::WhenAny(v) => Behavior::WhenAny(clone_vec(v, py)), + Behavior::After(v) => Behavior::After(clone_vec(v, py)), + Behavior::Race(v) => Behavior::Race(clone_vec(v, py)), + } +} + +fn clone_vec(v: &[Behavior>], py: Python<'_>) -> Vec>> { + v.iter().map(|b| clone_tree(b, py)).collect() +} + +fn collect_children( + children: Vec>, + py: Python<'_>, +) -> Vec>> { + children.iter().map(|c| clone_tree(&c.inner, py)).collect() +} + +// ----- Leaves -------------------------------------------------------------- + +#[pyfunction] +#[pyo3(name = "Action")] +pub fn action_fn(action: Py) -> PyBehavior { + PyBehavior::wrap(Behavior::Action(action)) +} + +#[pyfunction] +#[pyo3(name = "Wait")] +pub fn wait_fn(seconds: f64) -> PyResult { + if seconds.is_nan() { + return Err(PyValueError::new_err("Wait: seconds must not be NaN")); + } + Ok(PyBehavior::wrap(Behavior::Wait(seconds))) +} + +#[pyfunction] +#[pyo3(name = "WaitForever")] +pub fn wait_forever_fn() -> PyBehavior { + PyBehavior::wrap(Behavior::WaitForever) +} + +// ----- Decorators ---------------------------------------------------------- + +#[pyfunction] +#[pyo3(name = "Invert")] +pub fn invert_fn(py: Python<'_>, child: PyRef<'_, PyBehavior>) -> PyBehavior { + PyBehavior::wrap(Behavior::Invert(Box::new(clone_tree(&child.inner, py)))) +} + +#[pyfunction] +#[pyo3(name = "AlwaysSucceed")] +pub fn always_succeed_fn(py: Python<'_>, child: PyRef<'_, PyBehavior>) -> PyBehavior { + PyBehavior::wrap(Behavior::AlwaysSucceed(Box::new(clone_tree(&child.inner, py)))) +} + +// ----- Composites (Vec) ----------------------------------------- + +#[pyfunction] +#[pyo3(name = "Sequence")] +pub fn sequence_fn(py: Python<'_>, children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::Sequence(collect_children(children, py))) +} + +#[pyfunction] +#[pyo3(name = "Select")] +pub fn select_fn(py: Python<'_>, children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::Select(collect_children(children, py))) +} + +#[pyfunction] +#[pyo3(name = "WhenAll")] +pub fn when_all_fn(py: Python<'_>, children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::WhenAll(collect_children(children, py))) +} + +#[pyfunction] +#[pyo3(name = "WhenAny")] +pub fn when_any_fn(py: Python<'_>, children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::WhenAny(collect_children(children, py))) +} + +#[pyfunction] +#[pyo3(name = "After")] +pub fn after_fn(py: Python<'_>, children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::After(collect_children(children, py))) +} + +#[pyfunction] +#[pyo3(name = "Race")] +pub fn race_fn(py: Python<'_>, children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::Race(collect_children(children, py))) +} + +// ----- Control flow -------------------------------------------------------- + +#[pyfunction] +#[pyo3(name = "If")] +pub fn if_fn( + py: Python<'_>, + cond: PyRef<'_, PyBehavior>, + on_success: PyRef<'_, PyBehavior>, + on_failure: PyRef<'_, PyBehavior>, +) -> PyBehavior { + PyBehavior::wrap(Behavior::If( + Box::new(clone_tree(&cond.inner, py)), + Box::new(clone_tree(&on_success.inner, py)), + Box::new(clone_tree(&on_failure.inner, py)), + )) +} + +#[pyfunction] +#[pyo3(name = "While")] +pub fn while_fn( + py: Python<'_>, + cond: PyRef<'_, PyBehavior>, + body: Vec>, +) -> PyResult { + if body.is_empty() { + return Err(PyValueError::new_err("While: body must not be empty")); + } + Ok(PyBehavior::wrap(Behavior::While( + Box::new(clone_tree(&cond.inner, py)), + collect_children(body, py), + ))) +} + +#[pyfunction] +#[pyo3(name = "WhileAll")] +pub fn while_all_fn( + py: Python<'_>, + cond: PyRef<'_, PyBehavior>, + body: Vec>, +) -> PyResult { + if body.is_empty() { + return Err(PyValueError::new_err("WhileAll: body must not be empty")); + } + Ok(PyBehavior::wrap(Behavior::WhileAll( + Box::new(clone_tree(&cond.inner, py)), + collect_children(body, py), + ))) +} diff --git a/bonsai-py/src/lib.rs b/bonsai-py/src/lib.rs index 9d796b0..3019b3a 100644 --- a/bonsai-py/src/lib.rs +++ b/bonsai-py/src/lib.rs @@ -1,9 +1,14 @@ use pyo3::prelude::*; mod action_args; +mod behavior; mod status; use action_args::PyActionArgs; +use behavior::{ + action_fn, after_fn, always_succeed_fn, if_fn, invert_fn, race_fn, select_fn, sequence_fn, + wait_fn, wait_forever_fn, when_all_fn, when_any_fn, while_all_fn, while_fn, PyBehavior, +}; use status::PyStatus; /// Python bindings for the bonsai-bt behavior-tree library. @@ -14,5 +19,20 @@ use status::PyStatus; fn bonsai_py(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(action_fn, m)?)?; + m.add_function(wrap_pyfunction!(wait_fn, m)?)?; + m.add_function(wrap_pyfunction!(wait_forever_fn, m)?)?; + m.add_function(wrap_pyfunction!(invert_fn, m)?)?; + m.add_function(wrap_pyfunction!(always_succeed_fn, m)?)?; + m.add_function(wrap_pyfunction!(sequence_fn, m)?)?; + m.add_function(wrap_pyfunction!(select_fn, m)?)?; + m.add_function(wrap_pyfunction!(when_all_fn, m)?)?; + m.add_function(wrap_pyfunction!(when_any_fn, m)?)?; + m.add_function(wrap_pyfunction!(after_fn, m)?)?; + m.add_function(wrap_pyfunction!(race_fn, m)?)?; + m.add_function(wrap_pyfunction!(if_fn, m)?)?; + m.add_function(wrap_pyfunction!(while_fn, m)?)?; + m.add_function(wrap_pyfunction!(while_all_fn, m)?)?; Ok(()) } From 7c9ab29d46283a6aaf728b468e624ff790a61b8a Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sat, 16 May 2026 00:11:32 -0500 Subject: [PATCH 07/25] first attempt at bt.rs --- bonsai-py/scripts/verify.py | 97 +++++++++++++++++++++++++++ bonsai-py/src/action_args.rs | 6 +- bonsai-py/src/behavior.rs | 124 ++++++++++++++++------------------- bonsai-py/src/bt.rs | 117 +++++++++++++++++++++++++++++++++ bonsai-py/src/lib.rs | 9 ++- 5 files changed, 281 insertions(+), 72 deletions(-) create mode 100644 bonsai-py/src/bt.rs diff --git a/bonsai-py/scripts/verify.py b/bonsai-py/scripts/verify.py index ab3148b..3809a34 100644 --- a/bonsai-py/scripts/verify.py +++ b/bonsai-py/scripts/verify.py @@ -204,6 +204,102 @@ def test_behavior_factories() -> None: print(f" OK: identity-based equality across all {len(builders)} variants") +def test_bt_tick_and_telemetry() -> None: + """BT class: tick callback semantics, blackboard, reset, telemetry.""" + import socket + + import bonsai_py as bt + + # Doctest equivalent: 5 ticks of 0.5s, expect count == 1. + tree = bt.Sequence([ + bt.Wait(1.0), bt.Action("inc"), + bt.Wait(1.0), bt.Action("inc"), + bt.Wait(0.5), bt.Action("dec"), + ]) + bb = {"count": 0} + tree_bt = bt.BT(tree, bb) + acc = 0 + + def cb(args, _blackboard): + nonlocal acc + if args.action == "inc": + acc += 1 + return (bt.Status.Success, args.dt) + if args.action == "dec": + acc -= 1 + return (bt.Status.Success, args.dt) + return bt.RUNNING + + for _ in range(5): + tree_bt.tick(0.5, cb) + blackboard = tree_bt.blackboard() + blackboard["count"] = acc + assert bb["count"] == 1, bb["count"] + assert tree_bt.tick_count() == 5 + print(" OK: doctest equivalent — count == 1 after 5 ticks") + + assert bt.RUNNING == (bt.Status.Running, 0.0) + print(" OK: bt.RUNNING == (Status.Running, 0.0)") + + # Callback exception propagates with traceback / message intact. + bad_bt = bt.BT(bt.Sequence([bt.Action("boom")]), None) + + def raising_cb(_args, _bb): + raise ValueError("boom") + + try: + bad_bt.tick(0.5, raising_cb) + except ValueError as e: + assert str(e) == "boom", str(e) + print(" OK: callback ValueError propagates with message intact") + else: + raise AssertionError("expected ValueError from tick") + + # Callback returning a non-tuple is rejected by extract. + bad_shape = bt.BT(bt.Sequence([bt.Action("x")]), None) + + def bad_return_cb(_args, _bb): + return "not a tuple" + + try: + bad_shape.tick(0.5, bad_return_cb) + except Exception as e: + msg = str(e).lower() + assert "extract" in msg or "tuple" in msg or isinstance(e, TypeError), e + print(f" OK: bad return shape rejected ({type(e).__name__})") + else: + raise AssertionError("expected error from malformed return") + + # is_finished + reset_bt cycle. + quick = bt.BT(bt.Action("done"), None) + + def done_cb(_args, _bb): + return (bt.Status.Success, 0.0) + + assert not quick.is_finished() + quick.tick(0.0, done_cb) + assert quick.is_finished() + assert quick.tick(0.0, done_cb) is None + quick.reset_bt() + assert not quick.is_finished() + print(" OK: is_finished + reset_bt cycle") + + # with_telemetry: chainable + bind-failure raises OSError. + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + port = s.getsockname()[1] + chained = bt.BT(bt.Action("x"), None).with_telemetry(port) + assert chained is not None, "with_telemetry should return self for chaining" + print(f" OK: with_telemetry({port}) chainable") + + try: + bt.BT(bt.Action("x"), None).with_telemetry(port) + except OSError as e: + print(f" OK: with_telemetry on bound port raises OSError ({type(e).__name__})") + else: + raise AssertionError("expected OSError on already-bound port") + + # --------------------------------------------------------------------------- # Registry — append new test functions above and add them to this list. # Do not reorder or remove existing entries. @@ -212,6 +308,7 @@ def test_behavior_factories() -> None: test_module_scaffolding, test_status_and_action_args, test_behavior_factories, + test_bt_tick_and_telemetry, ] diff --git a/bonsai-py/src/action_args.rs b/bonsai-py/src/action_args.rs index 2f6244e..76fc1e0 100644 --- a/bonsai-py/src/action_args.rs +++ b/bonsai-py/src/action_args.rs @@ -1,6 +1,8 @@ use bonsai_bt::{ActionArgs, Event}; use pyo3::prelude::*; +use crate::behavior::PyAction; + /// Action callback arguments. /// /// Constructed by the tick bridge and passed to the user's callback. @@ -32,10 +34,10 @@ impl PyActionArgs { impl PyActionArgs { /// Build a `PyActionArgs` from the Rust `ActionArgs` that the tick /// callback receives. Hot path — one `clone_ref` plus an `f64` copy. - pub fn from_rust(args: &ActionArgs>, py: Python<'_>) -> Self { + pub(crate) fn from_rust(args: &ActionArgs, py: Python<'_>) -> Self { Self { dt: args.dt, - action: args.action.clone_ref(py), + action: args.action.0.clone_ref(py), } } } diff --git a/bonsai-py/src/behavior.rs b/bonsai-py/src/behavior.rs index 456eca7..f07d262 100644 --- a/bonsai-py/src/behavior.rs +++ b/bonsai-py/src/behavior.rs @@ -2,6 +2,33 @@ use bonsai_bt::Behavior; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; +/// Wrapper around `Py` that satisfies `bonsai_bt::BT`'s +/// `A: Clone + Debug` bounds. +/// +/// `Py` itself is not `Clone` in PyO3 0.28 (the trait was removed +/// because `.clone()` cannot statically prove the GIL is held). We satisfy +/// the bound by acquiring the GIL inside the `Clone` impl via +/// `Python::attach` and forwarding to `clone_ref(py)`. Inside a +/// `#[pymethods]` context the GIL is already held, so re-entry is cheap. +pub(crate) struct PyAction(pub(crate) Py); + +impl Clone for PyAction { + fn clone(&self) -> Self { + Python::attach(|py| PyAction(self.0.clone_ref(py))) + } +} + +impl std::fmt::Debug for PyAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Show the Python repr in tree-definition output (used by the + // telemetry visualizer). Fall back to a placeholder if repr fails. + Python::attach(|py| match self.0.bind(py).repr() { + Ok(s) => write!(f, "{s}"), + Err(_) => write!(f, ""), + }) + } +} + /// An opaque behavior-tree node. /// /// Construct via the factory functions (`Sequence`, `Action`, `Wait`, ...) @@ -9,11 +36,11 @@ use pyo3::prelude::*; /// can appear as a child of multiple parents. #[pyclass(unsendable, frozen, module = "bonsai_py", name = "Behavior")] pub struct PyBehavior { - pub(crate) inner: Behavior>, + pub(crate) inner: Behavior, } impl PyBehavior { - fn wrap(inner: Behavior>) -> Self { + fn wrap(inner: Behavior) -> Self { Self { inner } } } @@ -40,46 +67,8 @@ impl PyBehavior { } } -/// GIL-aware recursive clone of a `Behavior>` tree. -/// -/// `Py` is not `Clone` in PyO3 0.28 (clone() can't prove the GIL -/// is held), so the derived `Behavior::clone()` is unavailable for our -/// instantiation. This walks the tree manually, bumping the refcount on -/// each action via `clone_ref(py)`. -fn clone_tree(b: &Behavior>, py: Python<'_>) -> Behavior> { - match b { - Behavior::Wait(t) => Behavior::Wait(*t), - Behavior::WaitForever => Behavior::WaitForever, - Behavior::Action(a) => Behavior::Action(a.clone_ref(py)), - Behavior::Invert(c) => Behavior::Invert(Box::new(clone_tree(c, py))), - Behavior::AlwaysSucceed(c) => Behavior::AlwaysSucceed(Box::new(clone_tree(c, py))), - Behavior::Select(v) => Behavior::Select(clone_vec(v, py)), - Behavior::If(c, s, f) => Behavior::If( - Box::new(clone_tree(c, py)), - Box::new(clone_tree(s, py)), - Box::new(clone_tree(f, py)), - ), - Behavior::Sequence(v) => Behavior::Sequence(clone_vec(v, py)), - Behavior::While(c, body) => Behavior::While(Box::new(clone_tree(c, py)), clone_vec(body, py)), - Behavior::WhileAll(c, body) => { - Behavior::WhileAll(Box::new(clone_tree(c, py)), clone_vec(body, py)) - } - Behavior::WhenAll(v) => Behavior::WhenAll(clone_vec(v, py)), - Behavior::WhenAny(v) => Behavior::WhenAny(clone_vec(v, py)), - Behavior::After(v) => Behavior::After(clone_vec(v, py)), - Behavior::Race(v) => Behavior::Race(clone_vec(v, py)), - } -} - -fn clone_vec(v: &[Behavior>], py: Python<'_>) -> Vec>> { - v.iter().map(|b| clone_tree(b, py)).collect() -} - -fn collect_children( - children: Vec>, - py: Python<'_>, -) -> Vec>> { - children.iter().map(|c| clone_tree(&c.inner, py)).collect() +fn collect_children(children: Vec>) -> Vec> { + children.iter().map(|c| c.inner.clone()).collect() } // ----- Leaves -------------------------------------------------------------- @@ -87,7 +76,7 @@ fn collect_children( #[pyfunction] #[pyo3(name = "Action")] pub fn action_fn(action: Py) -> PyBehavior { - PyBehavior::wrap(Behavior::Action(action)) + PyBehavior::wrap(Behavior::Action(PyAction(action))) } #[pyfunction] @@ -109,52 +98,52 @@ pub fn wait_forever_fn() -> PyBehavior { #[pyfunction] #[pyo3(name = "Invert")] -pub fn invert_fn(py: Python<'_>, child: PyRef<'_, PyBehavior>) -> PyBehavior { - PyBehavior::wrap(Behavior::Invert(Box::new(clone_tree(&child.inner, py)))) +pub fn invert_fn(child: PyRef<'_, PyBehavior>) -> PyBehavior { + PyBehavior::wrap(Behavior::Invert(Box::new(child.inner.clone()))) } #[pyfunction] #[pyo3(name = "AlwaysSucceed")] -pub fn always_succeed_fn(py: Python<'_>, child: PyRef<'_, PyBehavior>) -> PyBehavior { - PyBehavior::wrap(Behavior::AlwaysSucceed(Box::new(clone_tree(&child.inner, py)))) +pub fn always_succeed_fn(child: PyRef<'_, PyBehavior>) -> PyBehavior { + PyBehavior::wrap(Behavior::AlwaysSucceed(Box::new(child.inner.clone()))) } // ----- Composites (Vec) ----------------------------------------- #[pyfunction] #[pyo3(name = "Sequence")] -pub fn sequence_fn(py: Python<'_>, children: Vec>) -> PyBehavior { - PyBehavior::wrap(Behavior::Sequence(collect_children(children, py))) +pub fn sequence_fn(children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::Sequence(collect_children(children))) } #[pyfunction] #[pyo3(name = "Select")] -pub fn select_fn(py: Python<'_>, children: Vec>) -> PyBehavior { - PyBehavior::wrap(Behavior::Select(collect_children(children, py))) +pub fn select_fn(children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::Select(collect_children(children))) } #[pyfunction] #[pyo3(name = "WhenAll")] -pub fn when_all_fn(py: Python<'_>, children: Vec>) -> PyBehavior { - PyBehavior::wrap(Behavior::WhenAll(collect_children(children, py))) +pub fn when_all_fn(children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::WhenAll(collect_children(children))) } #[pyfunction] #[pyo3(name = "WhenAny")] -pub fn when_any_fn(py: Python<'_>, children: Vec>) -> PyBehavior { - PyBehavior::wrap(Behavior::WhenAny(collect_children(children, py))) +pub fn when_any_fn(children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::WhenAny(collect_children(children))) } #[pyfunction] #[pyo3(name = "After")] -pub fn after_fn(py: Python<'_>, children: Vec>) -> PyBehavior { - PyBehavior::wrap(Behavior::After(collect_children(children, py))) +pub fn after_fn(children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::After(collect_children(children))) } #[pyfunction] #[pyo3(name = "Race")] -pub fn race_fn(py: Python<'_>, children: Vec>) -> PyBehavior { - PyBehavior::wrap(Behavior::Race(collect_children(children, py))) +pub fn race_fn(children: Vec>) -> PyBehavior { + PyBehavior::wrap(Behavior::Race(collect_children(children))) } // ----- Control flow -------------------------------------------------------- @@ -162,22 +151,20 @@ pub fn race_fn(py: Python<'_>, children: Vec>) -> PyBehavi #[pyfunction] #[pyo3(name = "If")] pub fn if_fn( - py: Python<'_>, cond: PyRef<'_, PyBehavior>, on_success: PyRef<'_, PyBehavior>, on_failure: PyRef<'_, PyBehavior>, ) -> PyBehavior { PyBehavior::wrap(Behavior::If( - Box::new(clone_tree(&cond.inner, py)), - Box::new(clone_tree(&on_success.inner, py)), - Box::new(clone_tree(&on_failure.inner, py)), + Box::new(cond.inner.clone()), + Box::new(on_success.inner.clone()), + Box::new(on_failure.inner.clone()), )) } #[pyfunction] #[pyo3(name = "While")] pub fn while_fn( - py: Python<'_>, cond: PyRef<'_, PyBehavior>, body: Vec>, ) -> PyResult { @@ -185,15 +172,14 @@ pub fn while_fn( return Err(PyValueError::new_err("While: body must not be empty")); } Ok(PyBehavior::wrap(Behavior::While( - Box::new(clone_tree(&cond.inner, py)), - collect_children(body, py), + Box::new(cond.inner.clone()), + collect_children(body), ))) } #[pyfunction] #[pyo3(name = "WhileAll")] pub fn while_all_fn( - py: Python<'_>, cond: PyRef<'_, PyBehavior>, body: Vec>, ) -> PyResult { @@ -201,7 +187,7 @@ pub fn while_all_fn( return Err(PyValueError::new_err("WhileAll: body must not be empty")); } Ok(PyBehavior::wrap(Behavior::WhileAll( - Box::new(clone_tree(&cond.inner, py)), - collect_children(body, py), + Box::new(cond.inner.clone()), + collect_children(body), ))) } diff --git a/bonsai-py/src/bt.rs b/bonsai-py/src/bt.rs new file mode 100644 index 0000000..71c1c69 --- /dev/null +++ b/bonsai-py/src/bt.rs @@ -0,0 +1,117 @@ +use bonsai_bt::{Event, Status, UpdateArgs, BT}; +use pyo3::exceptions::{PyOSError, PyRuntimeError}; +use pyo3::prelude::*; + +use crate::action_args::PyActionArgs; +use crate::behavior::{PyAction, PyBehavior}; +use crate::status::PyStatus; + +const POISONED_MSG: &str = + "BT was invalidated by a failed with_telemetry call; construct a new BT"; + +/// A behavior-tree executor wrapping `bonsai_bt::BT`. +/// +/// Construct from a tree and a blackboard, then drive with `.tick(dt, callback)`. +/// The callback receives `(args, blackboard)` and must return `(Status, float)`. +#[pyclass(unsendable, module = "bonsai_py", name = "BT")] +pub struct PyBT { + inner: Option>>, +} + +impl PyBT { + fn require_inner(&self) -> PyResult<&BT>> { + self.inner + .as_ref() + .ok_or_else(|| PyRuntimeError::new_err(POISONED_MSG)) + } + + fn require_inner_mut(&mut self) -> PyResult<&mut BT>> { + self.inner + .as_mut() + .ok_or_else(|| PyRuntimeError::new_err(POISONED_MSG)) + } +} + +#[pymethods] +impl PyBT { + #[new] + fn py_new(behavior: PyRef<'_, PyBehavior>, blackboard: Py) -> Self { + let tree = behavior.inner.clone(); + Self { + inner: Some(BT::new(tree, blackboard)), + } + } + + fn tick( + &mut self, + py: Python<'_>, + dt: f64, + callback: Py, + ) -> PyResult> { + let inner = self.require_inner_mut()?; + let event: Event = UpdateArgs { dt }.into(); + let mut cb_err: Option = None; + let result = inner.tick(&event, &mut |args, bb: &mut Py| { + if cb_err.is_some() { + return (Status::Failure, 0.0); + } + let py_args = PyActionArgs::from_rust(&args, py); + let bb_ref = bb.clone_ref(py); + match callback.call1(py, (py_args, bb_ref)) { + Ok(ret) => match ret.extract::<(PyStatus, f64)>(py) { + Ok((s, remaining)) => (s.into(), remaining), + Err(e) => { + cb_err = Some(e); + (Status::Failure, 0.0) + } + }, + Err(e) => { + cb_err = Some(e); + (Status::Failure, 0.0) + } + } + }); + if let Some(e) = cb_err { + return Err(e); + } + Ok(result.map(|(s, dt)| (s.into(), dt))) + } + + fn blackboard(&self, py: Python<'_>) -> PyResult> { + Ok(self.require_inner()?.blackboard().clone_ref(py)) + } + + fn reset_bt(&mut self) -> PyResult<()> { + self.require_inner_mut()?.reset_bt(); + Ok(()) + } + + fn tick_count(&self) -> PyResult { + Ok(self.require_inner()?.tick_count()) + } + + fn is_finished(&self) -> PyResult { + Ok(self.require_inner()?.is_finished()) + } + + #[pyo3(signature = (port, host = "127.0.0.1"))] + fn with_telemetry<'py>( + mut slf: PyRefMut<'py, Self>, + port: u16, + host: &str, + ) -> PyResult> { + let inner = slf + .inner + .take() + .ok_or_else(|| PyRuntimeError::new_err(POISONED_MSG))?; + match inner.with_telemetry_at(host, port) { + Ok(new_inner) => { + slf.inner = Some(new_inner); + Ok(slf) + } + Err(e) => Err(PyOSError::new_err(format!( + "with_telemetry({host}:{port}) failed: {e}" + ))), + } + } +} diff --git a/bonsai-py/src/lib.rs b/bonsai-py/src/lib.rs index 3019b3a..7923f65 100644 --- a/bonsai-py/src/lib.rs +++ b/bonsai-py/src/lib.rs @@ -2,6 +2,7 @@ use pyo3::prelude::*; mod action_args; mod behavior; +mod bt; mod status; use action_args::PyActionArgs; @@ -9,6 +10,7 @@ use behavior::{ action_fn, after_fn, always_succeed_fn, if_fn, invert_fn, race_fn, select_fn, sequence_fn, wait_fn, wait_forever_fn, when_all_fn, when_any_fn, while_all_fn, while_fn, PyBehavior, }; +use bt::PyBT; use status::PyStatus; /// Python bindings for the bonsai-bt behavior-tree library. @@ -16,10 +18,11 @@ use status::PyStatus; /// Construct trees with the factory functions (Sequence, Action, Wait, ...), /// wrap one in `BT(tree, blackboard)`, and drive it with `bt.tick(dt, callback)`. #[pymodule] -fn bonsai_py(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { +fn bonsai_py(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(action_fn, m)?)?; m.add_function(wrap_pyfunction!(wait_fn, m)?)?; m.add_function(wrap_pyfunction!(wait_forever_fn, m)?)?; @@ -34,5 +37,9 @@ fn bonsai_py(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(if_fn, m)?)?; m.add_function(wrap_pyfunction!(while_fn, m)?)?; m.add_function(wrap_pyfunction!(while_all_fn, m)?)?; + + // Convenience constant matching Rust's `bonsai_bt::RUNNING`. + m.add("RUNNING", (PyStatus::Running, 0.0_f64).into_pyobject(py)?)?; + Ok(()) } From 04c5f28da3b85929da490c5b0b595be2f4bdda58 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sat, 16 May 2026 00:15:26 -0500 Subject: [PATCH 08/25] cleanup --- .claude/claude_plan.md | 716 ------------------------------------ .claude/settings.local.json | 7 - 2 files changed, 723 deletions(-) delete mode 100644 .claude/claude_plan.md delete mode 100644 .claude/settings.local.json diff --git a/.claude/claude_plan.md b/.claude/claude_plan.md deleted file mode 100644 index e249a30..0000000 --- a/.claude/claude_plan.md +++ /dev/null @@ -1,716 +0,0 @@ -# Plan: Python bindings for `bonsai-bt` via PyO3 - -## Context - -`bonsai-bt` is a pure-Rust behavior-tree library ([bonsai/src/lib.rs](bonsai/src/lib.rs)). Its core surface is small — a recursive `Behavior` enum, a `BT` executor with a `tick()` method that takes a user closure, plus an optional WebSocket visualizer attached via `BT::with_telemetry(port)` (see [bonsai/src/bt_telemetry.rs:94](bonsai/src/bt_telemetry.rs#L94)). There are no existing bindings, no C deps, and `BT` carries no lifetimes — a clean fit for PyO3. - -Goal: expose the full library to Python so users can build behavior trees, tick them with a Python callback, and open the same browser visualizer. Action and blackboard types will be arbitrary `PyObject`s so Python users aren't constrained to a fixed shape. The crate will be a new workspace member and ship as a published wheel on PyPI. - -## Resolved decisions - -| Decision | Choice | -|---|---| -| Action type in Python | Arbitrary `PyObject` (strings, dicts, dataclasses — anything) | -| Blackboard type | Arbitrary `PyObject` (live reference, mutations persist) | -| Visualizer (`with_telemetry`) | Included via `visualize` feature | -| Crate location | New workspace member `bonsai-py/` | -| Distribution | Full PyPI pipeline (wheels + sdist on tag) | -| PyPI dist name | `bonsai-py` (Python import is `bonsai_py` — PyPI's automatic hyphen→underscore mapping) | -| `BT.with_telemetry(port)` | Returns `self` (chainable); two-line usage also documented | -| `ActionArgs` fields | `dt: float`, `action: Any` only (no `event`) | -| Execution mode | Sequential — each section completes & is reviewed before the next | -| Python version floor | 3.10+ via `abi3-py310` | - -## Crate layout - -Add a third workspace member `bonsai-py/` (sibling of `bonsai/` and `examples/`). Update [Cargo.toml](Cargo.toml) workspace `members` to `["bonsai", "examples", "bonsai-py"]`. - -``` -bonsai-py/ -├── Cargo.toml # cdylib, pyo3 + bonsai-bt deps -├── pyproject.toml # maturin build backend, package metadata -├── README.md -├── src/ -│ ├── lib.rs # #[pymodule] root, registers classes/functions -│ ├── behavior.rs # PyBehavior + factory functions -│ ├── bt.rs # PyBT class wrapping BT -│ ├── status.rs # PyStatus enum -│ └── action_args.rs # PyActionArgs passed to user callback -├── python/bonsai_py/ -│ ├── __init__.py # re-export from the compiled module -│ └── py.typed # PEP 561 marker -├── stubs/bonsai_py.pyi # type stubs for IDE/mypy -├── tests/test_bt.py # pytest suite -└── examples/visualizer_smoke.py -``` - -Key dependency choices in `bonsai-py/Cargo.toml`: -- `pyo3 = { version = "0.28", features = ["extension-module", "abi3-py310"] }` -- `bonsai-bt = { path = "../bonsai", version = "0.12", features = ["visualize"] }` -- `[lib] crate-type = ["cdylib"]` - -## Rust → Python type mapping - -| Rust type | Python exposure | -|--------------------------------------|-------------------------------------------------------------------| -| `Behavior` | `Behavior` opaque `#[pyclass]`; built via factory funcs | -| `BT` | `BT` `#[pyclass(unsendable)]` — PyObject is not `Send` | -| `Status` (Success/Failure/Running) | `Status` `#[pyclass(eq, eq_int)]` enum | -| `ActionArgs<'_, Event, PyObject>` | `ActionArgs` `#[pyclass]` with `.dt`, `.action` (no `.event`) | -| `with_telemetry(port) -> io::Result` | `BT.with_telemetry(port)` chainable, raises `OSError` on bind fail | - -**Why `unsendable`:** `PyObject` isn't `Send`. `#[pyclass(unsendable)]` tells PyO3 to panic if `BT` crosses thread boundaries — safe because all calls happen under the GIL. - -## Python API surface (target shape) - -```python -import bonsai_py as bt - -tree = bt.Sequence([ - bt.Wait(1.0), - bt.Action("inc"), - bt.If(bt.Action("low_hp"), - bt.AlwaysSucceed(bt.Action("flee")), - bt.Action("regroup")), - bt.While(bt.Action("has_ammo"), - [bt.Action("fire"), bt.Wait(0.3)]), -]) - -# Both styles work — with_telemetry returns self: -tree_bt = bt.BT(tree, {"count": 0}).with_telemetry(8910) -# or: -tree_bt = bt.BT(tree, {"count": 0}) -tree_bt.with_telemetry(8910) - -def on_action(args, bb): - if args.action == "inc": - bb["count"] += 1 - return (bt.Status.Success, args.dt) - return (bt.Status.Running, 0.0) - -status, remaining_dt = tree_bt.tick(dt=0.5, callback=on_action) -print(tree_bt.blackboard()["count"], tree_bt.is_finished()) -tree_bt.reset_bt() -``` - ---- - -# Agent-ready sections - -Sections are designed for sequential execution. Each is self-contained: a downstream agent reading only its own section plus the **shared overview above** has everything needed. Acceptance criteria define the handoff to the next section. - ---- - -## Section 1 — Scaffolding & build setup - -**Goal.** Stand up `bonsai-py/` so that `maturin develop` produces an importable (empty) Python module. - -**Depends on:** nothing. - -### Resolved decisions (Section-1 scope) - -| Decision | Choice | Rationale | -|---|---|---| -| Python version floor | `abi3-py310` | Python 3.8/3.9 EOL by 2026; py310 lets us use modern typing in stubs in §5. | -| PyO3 version | `0.28` (caret pin, latest stable confirmed via `cargo search pyo3` on 2026-05-15) | Mature `Bound<>` API; matches Section 4's tick-bridge sketch verbatim. | -| bonsai-bt dep style | Dual `path = "../bonsai", version = "0.12"` | Local builds use path; sdist consumers resolve from crates.io. **Requires `bonsai-bt 0.12.x` published to crates.io before any sdist upload.** | -| `f32` feature | Not exposed | Python floats are 64-bit; enabling f32 would silently lose precision. | -| `visualize` feature | Always enabled in `bonsai-py/Cargo.toml` | Resolved upstream; keeps the Python wheel self-contained. | -| Single name across layers | Cargo package = `bonsai-py`; PyPI distribution = `bonsai-py`; Python import = `bonsai_py` | The PyPI↔import hyphen↔underscore mapping is automatic across the Python ecosystem; users still type one name. | -| `[lib].name` | `bonsai_py` | Controls the produced `.so`/`.pyd` filename; must match the `#[pymodule]` function name and pyproject `module-name`. | -| License | MIT | Mirrors the parent crate. | -| Publish to crates.io? | No (`publish = false`) | The binding crate is built into wheels, not distributed via crates.io. | - -### Files to create - -Exact file contents follow — paste as-is. - -#### `bonsai-py/Cargo.toml` - -```toml -[package] -name = "bonsai-py" -version = "0.12.0" -edition = "2021" -rust-version = "1.80.0" -description = "Python bindings for the bonsai-bt behavior tree library" -license = "MIT" -authors = [ - "Kristoffer Solberg Rakstad ", - "Anmol Kathail ", -] -repository = "https://github.com/sollimann/bonsai.git" -homepage = "https://github.com/sollimann/bonsai" -publish = false - -[lib] -name = "bonsai_py" -crate-type = ["cdylib"] - -[dependencies] -pyo3 = { version = "0.28", features = ["extension-module", "abi3-py310"] } -bonsai-bt = { path = "../bonsai", version = "0.12", features = ["visualize"] } -``` - -Notes on each line worth understanding: -- `name = "bonsai-py"` is the **Cargo package name** and also the **PyPI distribution name** (set in `pyproject.toml` below). Python imports replace the hyphen with an underscore, giving `import bonsai_py`. This is the universal PyPI↔import convention — users only learn one name. -- `[lib].name = "bonsai_py"` controls the compiled artifact filename. PyO3 requires this to match the `#[pymodule]` function name; maturin requires it to match `tool.maturin.module-name`. All three must be **`bonsai_py`** (underscored) for `import bonsai_py` to find `PyInit_bonsai_py`. -- `crate-type = ["cdylib"]` is what produces the dynamic library Python loads. Not `rlib`, not `dylib`. -- `pyo3` features: - - `extension-module` tells PyO3 we're building a Python C extension (skips linking libpython, needed on Linux for portable wheels). - - `abi3-py310` enables the stable ABI floor at Python 3.10. One built wheel works for 3.10, 3.11, 3.12, 3.13, and future 3.x versions. -- `bonsai-bt = { path = ..., version = "0.12" }` — the **dual** form is load-bearing. With path only, Cargo refuses to package (sdist build fails). With version only, local dev wouldn't pick up uncommitted changes in `../bonsai/`. Both together: local cargo uses path; published sdist falls back to crates.io. - -#### `bonsai-py/pyproject.toml` - -```toml -[build-system] -requires = ["maturin>=1.7,<2.0"] -build-backend = "maturin" - -[project] -name = "bonsai-py" -description = "Behavior trees in Python, powered by the bonsai-bt Rust crate." -readme = "README.md" -license = { text = "MIT" } -requires-python = ">=3.10" -authors = [ - { name = "Kristoffer Solberg Rakstad", email = "kristoffer.solberg@cognite.com" }, - { name = "Anmol Kathail", email = "anmolkathail@gmail.com" }, -] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Rust", - "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Games/Entertainment", - "Typing :: Typed", -] -dynamic = ["version"] - -[project.urls] -Homepage = "https://github.com/sollimann/bonsai" -Repository = "https://github.com/sollimann/bonsai" -Issues = "https://github.com/sollimann/bonsai/issues" - -[tool.maturin] -module-name = "bonsai_py" -python-source = "python" -features = ["pyo3/extension-module"] -strip = true -``` - -Notes: -- `dynamic = ["version"]` — maturin auto-populates the project version from `Cargo.toml`'s `package.version`. Don't duplicate it here, or you risk drift between the wheel filename and the package metadata. -- `python-source = "python"` — required so maturin recognizes `python/bonsai_py/` as the package layout and places the compiled `.so` *inside* the `bonsai_py/` Python package (not at the top level). Without this, `from .bonsai_py import *` in `__init__.py` fails. -- `strip = true` — strips debug symbols from the built `cdylib`, shrinking the wheel by ~5-10 MB. -- No `include = [...]` for `../bonsai/**` — superseded by the dual-version dep. Adding it would bundle bonsai's source into the sdist redundantly. - -#### `bonsai-py/src/lib.rs` - -```rust -use pyo3::prelude::*; - -/// Python bindings for the bonsai-bt behavior-tree library. -/// -/// Construct trees with the factory functions (Sequence, Action, Wait, …), -/// wrap one in `BT(tree, blackboard)`, and drive it with `bt.tick(dt, callback)`. -#[pymodule] -fn bonsai_py(_py: Python<'_>, _m: &Bound<'_, PyModule>) -> PyResult<()> { - Ok(()) -} -``` - -Notes: -- Function name `bonsai_py` **must** match `[lib].name` and `tool.maturin.module-name`. PyO3 emits `PyInit_bonsai_py`; the dynamic loader looks for that symbol in `bonsai_py.so`. -- The `&Bound<'_, PyModule>` parameter shape is the PyO3 ≥0.22 signature. If pinning to an older PyO3, switch to `&PyModule`. -- The module docstring shows up under `help(bonsai_py)` and on PyPI — keep it short but useful even at the scaffolding stage. - -#### `bonsai-py/python/bonsai_py/__init__.py` - -```python -"""bonsai-py — behavior trees in Python, powered by the Rust bonsai-bt crate.""" - -from importlib.metadata import PackageNotFoundError, version as _version - -try: - __version__ = _version("bonsai-py") -except PackageNotFoundError: # editable install before metadata is in place - __version__ = "0.0.0+unknown" - -from .bonsai_py import * # noqa: F401,F403 (re-export the compiled module) -``` - -Notes: -- `importlib.metadata.version("bonsai-py")` reads from the installed wheel's METADATA. This keeps `__version__` in lockstep with the PyPI name without hard-coding. -- The `from .bonsai_py import *` line is what makes `bonsai_py.BT`, `bonsai_py.Status`, etc. resolve at top level once Sections 2–4 land. `# noqa: F401,F403` is for linters; remove if the project doesn't use ruff/flake8. - -#### `bonsai-py/python/bonsai_py/py.typed` - -Empty file. PEP 561 marker — type checkers (`mypy`, `pyright`) only honor inline types or stub files if this marker is present. - -#### `bonsai-py/README.md` - -````markdown -# bonsai-py — Python bindings - -Python bindings for the [bonsai-bt](https://github.com/sollimann/bonsai) -behavior-tree library. - -> Scaffolding only — public API arrives in subsequent commits. See the -> repository for the full design plan. - -## Installation (dev) - -```bash -python -m venv .venv -source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1 -pip install maturin -cd bonsai-py -maturin develop -python -c "import bonsai_py; print(bonsai_py.__version__)" -``` - -## License - -MIT — see [LICENSE](../LICENSE). -```` - -Real placeholder (not blank), so a PyPI render won't break if a release is cut before Section 9 fills it in. - -### Files to modify - -#### Root [Cargo.toml](Cargo.toml) - -```toml -[workspace] -resolver = "2" -members = ["bonsai", "examples", "bonsai-py"] -``` - -Append `"bonsai-py"` to the existing two-entry list. Keep `resolver = "2"` (already there per commit `341b7a7`). No other change. - -### Edge cases & pitfalls - -The implementer should read this list **before** typing anything — most pitfalls produce confusing errors hours into debugging. - -1. **Hyphen vs underscore drift.** One conceptual name (`bonsai-py`) flows through every layer, but Python module identifiers can't contain hyphens — so the *import* name is `bonsai_py` (underscore). Concretely: PyPI distribution `bonsai-py` ↔ Cargo package `bonsai-py` ↔ `[lib].name` `bonsai_py` ↔ `#[pymodule]` fn `bonsai_py` ↔ `tool.maturin.module-name` `bonsai_py` ↔ `import bonsai_py`. If the underscored form drifts (e.g. `[lib].name = "bonsai_bt"`), the import fails at runtime with `ImportError: dynamic module does not define module export function (PyInit_*)`. Quick check: grep for `bonsai_py` and `bonsai-py` and confirm those are the only two forms in the binding crate. - -2. **`python-source` layout is exact.** `python/bonsai_py/__init__.py` — both directory names matter. If you name the inner dir `python/bonsai/`, maturin will install the package as `bonsai`, not `bonsai_py`, and the C extension import inside `__init__.py` will mismatch. - -3. **Path dependency without version.** `bonsai-bt = { path = "../bonsai" }` (no `version`) works for `cargo build` but causes `cargo package` (and hence `maturin sdist`) to fail with `cannot package a workspace member with a path dependency without a version`. The dual form solves this. - -4. **bonsai-bt not on crates.io yet?** Check `https://crates.io/crates/bonsai-bt`. If version 0.12.0 isn't there, the dual dep still works for local builds but `maturin sdist` will refuse to package because the version isn't resolvable from a registry. **Action:** before publishing any Python sdist (Section 8/9), confirm `cargo search bonsai-bt` shows ≥0.12.0. If it doesn't, run `cargo publish -p bonsai-bt` from the workspace root first. Wheels are unaffected — only sdist needs registry resolution. - -5. **abi3 feature drift.** Some PyO3 features (custom `__hash__`, certain magic methods) need conditional `#[cfg(...)]` to compile under abi3. Section 1 won't hit this since the module is empty, but document it: when Section 2 adds `#[pyclass(eq, eq_int)]`, verify it compiles with `abi3-py310` (it should — `eq_int` is abi3-safe). - -6. **`f32` feature must NOT be enabled.** Python's `float` is always 64-bit (IEEE-754 double). Compiling bonsai-bt with `f32` and passing `f64` from Python down to a `f32`-typed `dt` would either fail to compile or silently truncate. Keep `features = ["visualize"]` only. - -7. **`cargo build` from workspace root pulls Python headers.** Building `bonsai-py` requires the C Python development headers. On Linux CI without them, `cargo build` (no `-p`) will fail. For Section 1's verification this isn't a blocker (we test `-p bonsai-py` explicitly and `-p bonsai-bt`), but Section 8 will need to either (a) configure CI to install Python dev headers before `cargo build`, or (b) avoid workspace-wide `cargo build` in Rust-only CI. Flag this in the section so the implementer mentions it in the PR description. - -8. **Windows wheel build needs MSVC.** Not Section 1's job, but document: developers on Windows must have the MSVC build tools installed (`rustup default stable-msvc` and Visual Studio Build Tools). `maturin develop` will fail with a linker error otherwise. - -9. **`maturin develop` requires an active venv.** It refuses to install into the system Python. The verification steps below activate one explicitly. - -10. **README path resolution.** `readme = "README.md"` in `pyproject.toml` is relative to `pyproject.toml`, not to the workspace root. The placeholder README **must exist** at `bonsai-py/README.md` before `maturin develop` runs, or the build fails before reaching Rust compilation. - -11. **`publish = false` on the Cargo package** prevents accidental `cargo publish` from pushing the binding crate to crates.io (where it would never resolve correctly). The Python wheel is the only published artifact. - -12. **`Cargo.lock` churn.** Adding `bonsai-py` will materially expand the workspace `Cargo.lock` (pyo3 pulls a sizeable dep graph). Expect the lockfile diff in the PR to be large. This is normal and not a regression. - -### Python-usage considerations baked into the scaffold - -Even though Section 1 ships no API, several scaffolding choices were made specifically to make later sections' Python ergonomics work: - -- **`py.typed` from day one** — once Section 5 lands stubs, IDE autocomplete works in editable installs without any additional config. -- **`python-source = "python"`** — lets us add pure-Python helpers later (e.g. context managers, dataclass adapters) without rebuilding the wheel. -- **`__version__` via `importlib.metadata`** — common Python convention; matches what users expect from `pip show bonsai-py`. -- **`abi3-py310`** — users on 3.10/3.11/3.12/3.13 install the *same* wheel; no per-Python-minor fanout. Saves CI time and disk for users. -- **Module docstring in `lib.rs`** — `help(bonsai_py)` will print something meaningful from day one, which sets expectations for the API doc style in later sections. - -### Verification (sequential — each step must pass before continuing) - -Run from the repository root unless noted. **Each step is a complete command the implementer can copy-paste.** - -1. **Rust crate compiles in isolation:** - ```bash - cargo check -p bonsai-py - ``` - Expect: exit 0, warnings allowed but no errors. First run will download pyo3 and friends — may take 1–2 min. - -2. **Workspace still builds end-to-end (core crate unaffected):** - ```bash - cargo build -p bonsai-bt - cargo test -p bonsai-bt - ``` - Expect: existing tests still pass. If they don't, Section 1 has corrupted workspace settings — investigate before continuing. - -3. **Create a fresh venv:** - ```bash - python -m venv .venv - source .venv/bin/activate # macOS/Linux - .\.venv\Scripts\Activate.ps1 # Windows PowerShell - ``` - -4. **Install maturin:** - ```bash - pip install -U "maturin>=1.7,<2.0" - ``` - -5. **Build & install the extension into the venv:** - ```bash - cd bonsai-py - maturin develop - ``` - Expect a line like `📦 Installed bonsai-py-0.12.0`. The compiled artifact name should contain `abi3` — confirm by inspecting `maturin develop -v` output or: - ```bash - python -c "import bonsai_py, os; print(os.path.basename(bonsai_py.__file__))" - ``` - Expect a filename like `bonsai_py.abi3.so` (Linux/macOS) or `bonsai_py.pyd` (Windows — note Windows abi3 wheels embed the tag in the wheel filename, not the `.pyd`). - -6. **Import works and version metadata is present:** - ```bash - python -c "import bonsai_py; print(bonsai_py.__version__); print(bonsai_py.__doc__)" - ``` - Expect: - ``` - 0.12.0 - bonsai-py — behavior trees in Python, powered by the Rust bonsai-bt crate. - ``` - -7. **Build a release wheel (no install) and verify the filename:** - ```bash - maturin build --release - ls target/wheels/ - ``` - Expect a single file matching `bonsai_py-0.12.0-cp310-abi3-.whl`. The `cp310-abi3` substring confirms abi3-py310 is active. (PyPI normalizes the distribution name `bonsai-py` to `bonsai_py` in wheel filenames — this is expected.) - -If all seven steps pass, Section 1 is complete and Section 2 may begin. - -### Acceptance criteria (for the reviewer) - -- All files listed in **Files to create** exist with the specified content (auto-verifiable: `git diff main --stat` shows the expected set). -- Root `Cargo.toml` `members` includes `"bonsai-py"`. -- Verification steps 1–7 all pass on a clean checkout. -- The PR description confirms `abi3-py310` resolves cleanly on `pyo3 = "0.28"` (see Open Question #1). -- No changes to `bonsai/` or `examples/` source — Section 1 is additive only. (`git diff main -- bonsai/ examples/` should be empty.) - -### Open questions for the implementer (resolve before merging) - -1. **`abi3-py310` feature on `pyo3 = "0.28"` — sanity check.** PyO3 has been pruning older `abi3-py3X` features over time. Confirm `0.28` still exposes `abi3-py310` before pinning by running `cargo add pyo3 --features extension-module,abi3-py310` in a scratch directory. If it doesn't resolve, drop one minor version of PyO3 at a time until it does, and note the resolved pair in the PR description. -2. **`bonsai-bt 0.12.0` on crates.io.** Not blocking for Section 1's verification (which uses path), but the implementer should confirm `cargo search bonsai-bt` returns a `0.12.x` entry. If not, file a follow-up ticket on Section 9 to publish bonsai-bt first. - -### Handoff checklist - -The implementer should treat this section as complete only after: - -- [ ] All 6 files created with content matching this spec. -- [ ] Root `Cargo.toml` updated. -- [ ] Verification steps 1–7 captured in the PR description (screenshot or pasted output). -- [ ] Open questions 1–2 above resolved or filed as follow-up tickets. -- [ ] CI passes on the PR (note: existing `rust-ci.yml` may need a small tweak from item #7 in pitfalls — coordinate with the user if it fails). - ---- - -## Section 2 — `PyStatus` and `PyActionArgs` - -**Goal.** Wrap the two leaf value types that the user-facing API exposes. - -**Depends on:** Section 1. - -**Files to create:** -- [bonsai-py/src/status.rs](bonsai-py/src/status.rs) -- [bonsai-py/src/action_args.rs](bonsai-py/src/action_args.rs) - -**Files to modify:** -- [bonsai-py/src/lib.rs](bonsai-py/src/lib.rs) — add `mod status; mod action_args;` and register `PyStatus`, `PyActionArgs` on the module. - -**Key references:** -- Rust `Status` enum: [bonsai/src/status.rs](bonsai/src/status.rs). -- Rust `ActionArgs<'_, E, A>`: [bonsai/src/state.rs:18-27](bonsai/src/state.rs#L18-L27). - -**Implementation notes:** -- `PyStatus` is a `#[pyclass(eq, eq_int, name = "Status")]` enum with three variants: `Success`, `Failure`, `Running`. Add `From` + `From` conversions in this file. -- `PyActionArgs` is `#[pyclass(name = "ActionArgs")]` with two `#[pyo3(get)]` fields: `dt: f64`, `action: PyObject`. **No `event` field** (resolved decision). -- Provide a Rust-side helper `PyActionArgs::from_rust(args: &ActionArgs, py: Python) -> Self` that clones the action `PyObject` for the Python user. This is what the tick bridge in Section 4 will call. - -**Acceptance criteria:** -- `cargo build -p bonsai-py` passes. -- `maturin develop` succeeds; from Python: `bonsai_py.Status.Success == bonsai_py.Status.Success` and `bonsai_py.Status.Success != bonsai_py.Status.Failure`. -- An ad-hoc Rust unit test constructs a `PyActionArgs` and reads `dt`, `action`. - ---- - -## Section 3 — `PyBehavior` and factory functions - -**Goal.** Let Python construct a `Behavior` tree. - -**Depends on:** Section 1. - -**Files to create:** -- [bonsai-py/src/behavior.rs](bonsai-py/src/behavior.rs). - -**Files to modify:** -- [bonsai-py/src/lib.rs](bonsai-py/src/lib.rs) — register `PyBehavior` and add `m.add_function(...)` for each factory. - -**Key references:** -- Rust `Behavior` enum (14 variants): [bonsai/src/behavior.rs:12](bonsai/src/behavior.rs#L12). -- The exact factory list is in [bonsai/src/lib.rs:121-124](bonsai/src/lib.rs#L121-L124). - -**Implementation notes:** -- `PyBehavior` is opaque to Python: `#[pyclass(name = "Behavior")] struct PyBehavior { inner: Behavior }`. No fields exposed. (Power users can introspect via repr in a later iteration.) -- Implement 14 `#[pyfunction]` factories that build a `Behavior` and wrap it in `PyBehavior`: - - **Leaves:** `Action(action: PyObject) -> PyBehavior`, `Wait(seconds: f64)`, `WaitForever()` - - **Decorators:** `Invert(child: PyBehavior)`, `AlwaysSucceed(child: PyBehavior)` - - **Composites:** `Sequence(children: Vec)`, `Select(children: Vec)`, `WhenAll(...)`, `WhenAny(...)`, `After(...)`, `Race(...)` - - **Control flow:** `If(cond: PyBehavior, ok: PyBehavior, ko: PyBehavior)`, `While(cond: PyBehavior, body: Vec)`, `WhileAll(cond: PyBehavior, body: Vec)` -- Factories take `PyBehavior` by value (the user's variable is consumed). Internally we move `inner` out. -- Add `__repr__` on `PyBehavior` that prints the variant name + arity. Helps debugging. -- Watch the `While` / `WhileAll` panic-on-empty-body documented in [bonsai/src/behavior.rs:54-55](bonsai/src/behavior.rs#L54-L55): raise `ValueError` from Python *before* calling the Rust constructor. Don't let a Rust panic cross the FFI boundary. - -**Acceptance criteria:** -- All 14 factories importable from Python. -- Construct the full tree from [examples/src/visualizer_smoke/main.rs:75-94](examples/src/visualizer_smoke/main.rs#L75-L94) from Python without panics. -- `bt.While(bt.Action("x"), [])` raises `ValueError`. - ---- - -## Section 4 — `PyBT` and the callback bridge - -**Goal.** The hot path: `BT(tree, blackboard).tick(dt, callback)` works end-to-end. - -**Depends on:** Sections 2 and 3. - -**Files to create:** -- [bonsai-py/src/bt.rs](bonsai-py/src/bt.rs). - -**Files to modify:** -- [bonsai-py/src/lib.rs](bonsai-py/src/lib.rs) — register `PyBT`. - -**Key references:** -- Rust `BT` struct + `tick`: [bonsai/src/bt.rs](bonsai/src/bt.rs). -- `with_telemetry`: [bonsai/src/bt_telemetry.rs:94](bonsai/src/bt_telemetry.rs#L94). -- Doctest example (target behavior to match): [bonsai/src/lib.rs:39-118](bonsai/src/lib.rs#L39-L118). - -**API to expose** (all `#[pymethods]`): -- `__init__(behavior: PyBehavior, blackboard: PyObject)` -- `tick(dt: float, callback: Callable[[ActionArgs, Any], tuple[Status, float]]) -> Optional[tuple[Status, float]]` -- `blackboard() -> PyObject` (live reference — mutations to it from Python persist into the BT) -- `reset_bt() -> None` -- `tick_count() -> int` -- `is_finished() -> bool` -- `with_telemetry(port: int, host: str = "127.0.0.1") -> PyBT` (chainable; raises `OSError` if bind fails) - -**The tick bridge.** This is the only non-trivial code in the binding. Sketch: - -```rust -#[pymethods] -impl PyBT { - fn tick(&mut self, py: Python<'_>, dt: f64, callback: PyObject) - -> PyResult> - { - let event: Event = UpdateArgs { dt }.into(); - let mut cb_err: Option = None; - let result = self.inner.tick(&event, &mut |args, bb: &mut PyObject| { - if cb_err.is_some() { return (Status::Failure, 0.0); } // short-circuit after error - let py_args = PyActionArgs::from_rust(&args, py); - match callback.call1(py, (py_args, bb.clone_ref(py))) { - Ok(ret) => match ret.extract::<(PyStatus, f64)>(py) { - Ok((s, dt)) => (s.into(), dt), - Err(e) => { cb_err = Some(e); (Status::Failure, 0.0) } - }, - Err(e) => { cb_err = Some(e); (Status::Failure, 0.0) } - } - }); - if let Some(e) = cb_err { return Err(e); } - Ok(result.map(|(s, dt)| (s.into(), dt))) - } -} -``` - -**Implementation notes:** -- **GIL strategy:** hold the GIL for the whole tick. The closure runs synchronously and is always Python-bound, so `allow_threads` + `with_gil` adds overhead without benefit. (We can revisit if a future async path appears.) -- **Error propagation:** Python exceptions from the callback can't unwind through Rust. Capture in `cb_err` and propagate. Short-circuit subsequent action calls in the same tick to avoid spurious sibling callbacks after an error. -- **Blackboard.** `inner.bb` is the actual `PyObject` — `blackboard()` returns `self.inner.blackboard().clone_ref(py)`. Same Python object, refcount bumped. -- **`with_telemetry`:** wrap the Rust `io::Result`. Use `pyo3::exceptions::PyOSError` for bind failures. Returning `Self` from a `#[pymethods]` consuming method requires careful PyO3 ownership — easier to take `&mut self`, call `with_telemetry_at` semantically (port/host stored, server started), and return a clone or `Py`. Look at [bonsai/src/bt_telemetry.rs:162](bonsai/src/bt_telemetry.rs#L162) — the underlying call takes `mut self` by value. Workaround: pop `inner` into a temporary, run `with_telemetry_at`, put it back. Or refactor to use `&mut self`. Document the approach in code. - -**Acceptance criteria:** -- Python can run the doctest equivalent and observe `count == 1` after 5 ticks of 0.5s (matching [bonsai/src/lib.rs:99-113](bonsai/src/lib.rs#L99-L113)). -- A callback that `raise ValueError("boom")` causes `bt.tick()` to raise `ValueError("boom")` with traceback intact. -- `bt.with_telemetry(8910)` returns the same `BT` (chainable) and starts a listening socket on `127.0.0.1:8910`. -- `bt.with_telemetry(8910)` while the port is already bound raises `OSError`. - ---- - -## Section 5 — Type stubs - -**Goal.** Editor autocomplete + `mypy`-friendly types. - -**Depends on:** Section 4 (final method signatures must be locked in). - -### Approach decision (evaluate before writing code) - -Two viable approaches — **the implementer should pick one at the start of Section 5 and document the choice in the PR description.** - -**Option A — Hand-written stubs (default if B fails compatibility check).** -Write `bonsai-py/stubs/bonsai_py.pyi` by hand, mirroring the API from Section 4. Simple, no extra build-time dep, full control over the wording — but the stub file and the Rust signatures drift independently, so every API change in Sections 2–4 requires a parallel `.pyi` edit. Maintenance cost compounds. - -**Option B — Auto-generate stubs via `pyo3-stub-gen` (preferred if compatible).** -[`pyo3-stub-gen`](https://crates.io/crates/pyo3-stub-gen) (`0.22.2` on crates.io as of 2026-05-15) reads `#[pyo3(...)]` annotations on `#[pyclass]`/`#[pyfunction]` items and emits a `.pyi` file. The stub generation runs as a small binary built into the crate and invoked by `cargo run --bin stub_gen` (typically wired into CI). Trade-offs: -- ✅ Stubs stay in lockstep with the Rust source automatically. -- ✅ Annotations live next to the functions they document. -- ⚠ Adds a build-time dep (`pyo3-stub-gen` + `pyo3-stub-gen-derive`). -- ⚠ **Compatibility check required:** `pyo3-stub-gen 0.22.x` was published before PyO3 `0.28`; the implementer must verify `pyo3-stub-gen` supports PyO3 `0.28` (check the crate's README and Cargo.toml's `pyo3 = "..."` line). If it lags, either wait for an update or fall back to Option A. - -**Recommendation:** start by running `cargo info pyo3-stub-gen` and inspecting its current PyO3 compatibility. If it supports `0.28` (or a newer matching version exists), use Option B. Otherwise Option A. - -### Files to create - -- Option A: [bonsai-py/stubs/bonsai_py.pyi](bonsai-py/stubs/bonsai_py.pyi) (hand-written). -- Option B: [bonsai-py/src/bin/stub_gen.rs](bonsai-py/src/bin/stub_gen.rs) (small binary that calls the generator) + add `#[gen_stub_pyclass]` / `#[gen_stub_pyfunction]` macros throughout the existing Rust files from Sections 2–4. The generated `bonsai_py.pyi` lives at the same path under `stubs/` (or wherever maturin's `include` glob picks it up). - -### Implementation notes (apply to either option) - -- Use `Any` for `action` and the blackboard. Use `Callable[[ActionArgs, Any], tuple[Status, float]]` for the tick callback. -- `Status` is an `IntEnum`-like class — expose `Success`, `Failure`, `Running` as class attributes. -- Factory functions are stubbed at module level: `def Sequence(children: list[Behavior]) -> Behavior: ...` etc. -- In `bonsai-py/pyproject.toml` `[tool.maturin]` add an `include` glob so the `.pyi` and `py.typed` ship in the wheel. -- If Option B is chosen, wire `cargo run --bin stub_gen` into CI (Section 8) so generated stubs are regenerated and diffed on every PR — drift between annotations and committed `.pyi` should fail the build. - -### Acceptance criteria - -- `mypy --strict` on a small example script that imports `bonsai_py` passes with no errors. -- VS Code/PyCharm shows autocomplete on `bt.Status.` and `bt.BT(...).`. -- The PR description states which option was chosen and (for Option B) the exact `pyo3-stub-gen` version pinned alongside the PyO3 0.28 compatibility result. - ---- - -## Section 6 — Test suite - -**Goal.** Behavioral parity with the Rust test suite. - -**Depends on:** Section 4. - -**Files to create:** -- [bonsai-py/tests/test_bt.py](bonsai-py/tests/test_bt.py). -- [bonsai-py/tests/conftest.py](bonsai-py/tests/conftest.py) (optional, fixtures). - -**Tests to write** (one pytest each): -1. **Doctest port** — translate [bonsai/src/lib.rs:39-118](bonsai/src/lib.rs#L39-L118) literally; assert counter values at each step. Covers `Sequence`, `Wait`, `Action`, blackboard mutation, `reset_bt`, `tick_count`, `is_finished`. -2. **Variant coverage** — one test per non-trivial variant. Mirror cases in [bonsai/tests/bt_tests.rs](bonsai/tests/bt_tests.rs): - - `Select` first-success short-circuit - - `If` true/false branches - - `Invert` flips Success↔Failure - - `AlwaysSucceed` swallows Failure - - `While` repeats until cond fails - - `WhileAll` checks cond only between iterations - - `WhenAll` AND-parallel, `WhenAny` OR-parallel - - `Race` first-to-complete (success OR failure) - - `After` sequential success in parallel -3. **Callback error propagation** — callback raises `ValueError`; assert `tick()` propagates it. -4. **Empty `While` body validation** — `bt.While(bt.Action("x"), [])` raises `ValueError`. -5. **`with_telemetry` bind failure** — bind port 8910 twice; second call raises `OSError`. - -**Acceptance criteria:** -- `cd bonsai-py && maturin develop && pytest -v` is green. -- Tests run in <5s total. - ---- - -## Section 7 — Visualizer smoke example - -**Goal.** Confirm the WebSocket visualizer works through Python end-to-end. - -**Depends on:** Section 4. - -**Files to create:** -- [bonsai-py/examples/visualizer_smoke.py](bonsai-py/examples/visualizer_smoke.py). - -**Key references:** -- Rust original: [examples/src/visualizer_smoke/main.rs](examples/src/visualizer_smoke/main.rs). - -**Implementation notes:** -- Direct line-for-line port. Same 27-node tree, same phase-offset color rotation, same 400 ms wall sleep. -- The Rust example uses string slices for actions; in Python use plain `str` — matches naturally. -- `bt.with_telemetry(8910)` chains: `bt = BT(build_tree(), {}).with_telemetry(8910)`. - -**Manual verification protocol** (document in the script's module docstring): -1. `python examples/visualizer_smoke.py` -2. Open -3. Confirm: 27 nodes render, leaves cycle through green/yellow/red every ~400 ms. -4. `Ctrl-C` and restart → browser reconnects within 1 s. - -**Acceptance criteria:** -- Script runs without exceptions for at least 30 s. -- All manual checks above pass. - ---- - -## Section 8 — CI wheel build matrix - -**Goal.** Multi-platform wheels build on push and on tag. - -**Depends on:** Sections 1, 6 (tests must exist to run in CI). - -**Files to create:** -- [.github/workflows/python-wheels.yml](.github/workflows/python-wheels.yml). - -**Implementation notes:** -- Use [`PyO3/maturin-action`](https://github.com/PyO3/maturin-action) (de-facto standard). -- Matrix: - - Linux: `manylinux_2_17_x86_64`, `manylinux_2_17_aarch64` (cross-build via docker) - - macOS: `universal2` (single wheel covers x86_64 + arm64) - - Windows: `x86_64` -- One abi3 wheel per platform covers Python 3.8+ — don't fan out across Python minor versions. -- Triggers: push to `main` (test build, don't publish), tag matching `py-v*` (publish to PyPI). -- Add a step that runs `pytest` on the built wheel inside the matrix (install wheel into a fresh venv, then test). -- PyPI publish step uses `pypa/gh-action-pypi-publish` with `PYPI_API_TOKEN` secret. **Recommend a TestPyPI dry-run job triggered on `py-test-*` tags first.** - -**Acceptance criteria:** -- A push to a feature branch produces green builds for all three platforms. -- Wheels are uploaded as GitHub Actions artifacts. -- A test tag pushes a wheel to TestPyPI; `pip install -i https://test.pypi.org/simple/ bonsai-py` works in a fresh venv. - ---- - -## Section 9 — Docs & release process - -**Goal.** Users can find and install the package. - -**Depends on:** Sections 4, 7, 8. - -**Files to modify:** -- [bonsai-py/README.md](bonsai-py/README.md) — quickstart, install, link to visualizer example, link back to the core Rust crate. -- [README.md](README.md) (root) — short Python section pointing at the wheel: `pip install bonsai-py`. - -**Files to create:** -- [bonsai-py/CHANGELOG.md](bonsai-py/CHANGELOG.md) — initial entry: `0.1.0 — first PyO3 binding, parity with bonsai-bt 0.12.0`. - -**Versioning policy** (document in `bonsai-py/README.md`): -- Python package version tracks the Rust crate version it wraps (e.g. Rust `bonsai-bt 0.12.0` → `bonsai-py 0.12.0` on PyPI). -- Hot-fix releases on the Python side only bump a `postN` suffix (PEP 440), e.g. `0.12.0.post1`. - -**Acceptance criteria:** -- README renders correctly on PyPI (no broken Markdown after `maturin upload`). -- A tagged release pushes both a sdist and wheels for all platforms. - ---- - -## Integration verification (final, after all sections) - -Run once everything is in place: - -1. Fresh venv: `python -m venv .venv && source .venv/bin/activate` -2. `pip install bonsai-py` (from PyPI, not the local path). -3. Run [bonsai-py/examples/visualizer_smoke.py](bonsai-py/examples/visualizer_smoke.py) and walk through Section 7's manual checks. -4. `pytest bonsai-py/tests/` against the installed wheel. -5. Verify `import bonsai_py; help(bonsai_py.BT)` shows the documented surface. - -If all five pass, the binding is ready to announce. diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index e90285d..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(git rm *)" - ] - } -} From 6c5a6738941e4b67cc1ded10d233bc2bb74cb570 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sat, 16 May 2026 10:19:49 -0500 Subject: [PATCH 09/25] add missing tests --- bonsai-py/scripts/verify.py | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/bonsai-py/scripts/verify.py b/bonsai-py/scripts/verify.py index 3809a34..58a4496 100644 --- a/bonsai-py/scripts/verify.py +++ b/bonsai-py/scripts/verify.py @@ -299,6 +299,64 @@ def done_cb(_args, _bb): else: raise AssertionError("expected OSError on already-bound port") + assert bt.BT.__module__ == "bonsai_py", bt.BT.__module__ + print(" OK: BT.__module__ == 'bonsai_py'") + + bb_in = {"x": 1} + identity_bt = bt.BT(bt.Action("noop"), bb_in) + assert identity_bt.blackboard() is bb_in + print(" OK: blackboard() preserves Python object identity") + + sentinel_action = {"k": 42} + sentinel_bb = {"y": 1} + seen = [] + + def identity_cb(args, bbref): + seen.append((args.action is sentinel_action, bbref is sentinel_bb)) + return (bt.Status.Success, 0.0) + + bt.BT(bt.Action(sentinel_action), sentinel_bb).tick(0.0, identity_cb) + assert seen == [(True, True)], seen + print(" OK: action + bb identity preserved through tick callback") + + # WhenAll short-circuit: callback raise on child[1] must NOT invoke child[2]. + order = [] + + def short_circuit_cb(args, _bb): + order.append(args.action) + if args.action == "b": + raise ValueError("stop") + return (bt.Status.Success, 0.0) + + parallel = bt.BT( + bt.WhenAll([bt.Action("a"), bt.Action("b"), bt.Action("c")]), + None, + ) + try: + parallel.tick(0.0, short_circuit_cb) + except ValueError: + pass + assert order == ["a", "b"], f"expected ['a','b'] (short-circuit), got {order}" + print(" OK: WhenAll short-circuits sibling callbacks after a raise") + + # Poisoned BT (after with_telemetry bind failure) refuses subsequent calls. + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + port2 = s.getsockname()[1] + _holder = bt.BT(bt.Action("x"), None).with_telemetry(port2) # keep alive + victim = bt.BT(bt.Action("y"), None) + try: + victim.with_telemetry(port2) + except OSError: + pass + try: + victim.tick(0.0, lambda _a, _b: (bt.Status.Success, 0.0)) + except RuntimeError as e: + assert "invalidated" in str(e), str(e) + print(" OK: poisoned BT raises RuntimeError on subsequent tick") + else: + raise AssertionError("expected RuntimeError on poisoned BT") + # --------------------------------------------------------------------------- # Registry — append new test functions above and add them to this list. From b44f2ee4ba3f4f4747f869d17a9eb57cad77f8fc Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sat, 16 May 2026 10:53:46 -0500 Subject: [PATCH 10/25] first attempt at stubs gen --- bonsai-py/Cargo.toml | 13 ++- bonsai-py/python/bonsai_py/__init__.py | 12 +++ bonsai-py/python/bonsai_py/__init__.pyi | 127 ++++++++++++++++++++++++ bonsai-py/scripts/regen-stubs.sh | 38 +++++++ bonsai-py/scripts/verify.py | 45 +++++++++ bonsai-py/src/action_args.rs | 3 + bonsai-py/src/behavior.rs | 17 ++++ bonsai-py/src/bin/stub_gen.rs | 15 +++ bonsai-py/src/bt.rs | 3 + bonsai-py/src/lib.rs | 5 + bonsai-py/src/status.rs | 3 + 11 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 bonsai-py/python/bonsai_py/__init__.pyi create mode 100755 bonsai-py/scripts/regen-stubs.sh create mode 100644 bonsai-py/src/bin/stub_gen.rs diff --git a/bonsai-py/Cargo.toml b/bonsai-py/Cargo.toml index 646741c..c96608b 100644 --- a/bonsai-py/Cargo.toml +++ b/bonsai-py/Cargo.toml @@ -15,8 +15,17 @@ publish = false [lib] name = "bonsai_py" -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "stub_gen" +path = "src/bin/stub_gen.rs" [dependencies] -pyo3 = { version = "0.28", features = ["extension-module", "abi3-py310"] } +# Note: `extension-module` is enabled by maturin via pyproject.toml's +# `[tool.maturin].features` setting. Keeping it out of the default feature +# list lets `cargo run --bin stub_gen` link libpython for the regular binary +# path; maturin still activates it for the wheel build. +pyo3 = { version = "0.28", features = ["abi3-py310"] } bonsai-bt = { path = "../bonsai", version = "0.12", features = ["visualize"] } +pyo3-stub-gen = "0.22.3" diff --git a/bonsai-py/python/bonsai_py/__init__.py b/bonsai-py/python/bonsai_py/__init__.py index 2a69a46..fe3d91d 100644 --- a/bonsai-py/python/bonsai_py/__init__.py +++ b/bonsai-py/python/bonsai_py/__init__.py @@ -8,3 +8,15 @@ __version__ = "0.0.0+unknown" from .bonsai_py import * # noqa: F401,F403 (re-export the compiled module) + +__all__ = [ + # types + "Status", "ActionArgs", "Behavior", "BT", + # factories (leaves, decorators, composites, control flow) + "Action", "Wait", "WaitForever", + "Invert", "AlwaysSucceed", + "Sequence", "Select", "WhenAll", "WhenAny", "After", "Race", + "If", "While", "WhileAll", + # constants + "RUNNING", +] diff --git a/bonsai-py/python/bonsai_py/__init__.pyi b/bonsai-py/python/bonsai_py/__init__.pyi new file mode 100644 index 0000000..b5d1b63 --- /dev/null +++ b/bonsai-py/python/bonsai_py/__init__.pyi @@ -0,0 +1,127 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401, F403, F405 + +import builtins +import enum +import typing +__all__ = [ + "Action", + "ActionArgs", + "After", + "AlwaysSucceed", + "BT", + "Behavior", + "If", + "Invert", + "Race", + "Select", + "Sequence", + "Status", + "Wait", + "WaitForever", + "WhenAll", + "WhenAny", + "While", + "WhileAll", + "RUNNING", +] + +@typing.final +class ActionArgs: + r""" + Action callback arguments. + + Constructed by the tick bridge and passed to the user's callback. + The Rust `ActionArgs::event` field is intentionally not exposed — + Python users only see `dt` and `action`. + """ + @property + def dt(self) -> builtins.float: + r""" + Remaining delta time in seconds. + """ + @property + def action(self) -> typing.Any: + r""" + The user-supplied action value (whatever was passed to `bt.Action(...)`). + """ + def __new__(cls, dt: builtins.float, action: typing.Any) -> ActionArgs: ... + def __repr__(self) -> builtins.str: ... + +@typing.final +class BT: + r""" + A behavior-tree executor wrapping `bonsai_bt::BT`. + + Construct from a tree and a blackboard, then drive with `.tick(dt, callback)`. + The callback receives `(args, blackboard)` and must return `(Status, float)`. + """ + def __new__(cls, behavior: Behavior, blackboard: typing.Any) -> BT: ... + def tick(self, dt: builtins.float, callback: typing.Any) -> typing.Optional[tuple[Status, builtins.float]]: ... + def blackboard(self) -> typing.Any: ... + def reset_bt(self) -> None: ... + def tick_count(self) -> builtins.int: ... + def is_finished(self) -> builtins.bool: ... + def with_telemetry(self, port: builtins.int, host: builtins.str = '127.0.0.1') -> BT: ... + +@typing.final +class Behavior: + r""" + An opaque behavior-tree node. + + Construct via the factory functions (`Sequence`, `Action`, `Wait`, ...) + at the module level. Subtrees are reusable - the same `Behavior` + can appear as a child of multiple parents. + """ + def __repr__(self) -> builtins.str: ... + +@typing.final +class Status(enum.Enum): + r""" + Behavior-tree node result. + + Mirrors `bonsai_bt::Status`. Comparable to `int` + (`Status.Success == 0`, `Failure == 1`, `Running == 2`) and usable + as a `dict` key or `set` member. + """ + Success = ... + Failure = ... + Running = ... + + def __reduce__(self) -> tuple[typing.Any, tuple[typing.Any, builtins.str]]: + r""" + Pickle support: name the singleton by class + variant name, since + PyO3 simple enums refuse construction by call (`Status(0)` raises). + """ + +def Action(action: typing.Any) -> Behavior: ... + +def After(children: typing.Sequence[Behavior]) -> Behavior: ... + +def AlwaysSucceed(child: Behavior) -> Behavior: ... + +def If(cond: Behavior, on_success: Behavior, on_failure: Behavior) -> Behavior: ... + +def Invert(child: Behavior) -> Behavior: ... + +def Race(children: typing.Sequence[Behavior]) -> Behavior: ... + +def Select(children: typing.Sequence[Behavior]) -> Behavior: ... + +def Sequence(children: typing.Sequence[Behavior]) -> Behavior: ... + +def Wait(seconds: builtins.float) -> Behavior: ... + +def WaitForever() -> Behavior: ... + +def WhenAll(children: typing.Sequence[Behavior]) -> Behavior: ... + +def WhenAny(children: typing.Sequence[Behavior]) -> Behavior: ... + +def While(cond: Behavior, body: typing.Sequence[Behavior]) -> Behavior: ... + +def WhileAll(cond: Behavior, body: typing.Sequence[Behavior]) -> Behavior: ... + + +RUNNING: typing.Final[tuple[Status, builtins.float]] +r"""Convenience constant: ``(Status.Running, 0.0)`` - return from a tick callback to keep the action running.""" diff --git a/bonsai-py/scripts/regen-stubs.sh b/bonsai-py/scripts/regen-stubs.sh new file mode 100755 index 0000000..a1dce0d --- /dev/null +++ b/bonsai-py/scripts/regen-stubs.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Regenerate the type stub and apply manual touch-ups (RUNNING constant). +# Run after editing any #[gen_stub_*] annotation. +# +# Usage: ./bonsai-py/scripts/regen-stubs.sh +set -euo pipefail + +cd "$(dirname "$0")/.." # cd into bonsai-py/ +cargo run --quiet --bin stub_gen -p bonsai-py + +# pyo3-stub-gen 0.22 reads pyproject.toml and writes the stub to +# python/bonsai_py/__init__.pyi automatically. +STUB=python/bonsai_py/__init__.pyi +if [ ! -f "$STUB" ]; then + echo "ERROR: $STUB was not generated. Did the binary fail silently?" >&2 + exit 1 +fi + +# pyo3-stub-gen doesn't introspect m.add() module-level constants, so +# the RUNNING declaration must be appended manually (idempotent). +if ! grep -q "^RUNNING: " "$STUB"; then + { + echo "" + echo "RUNNING: typing.Final[tuple[Status, builtins.float]]" + echo 'r"""Convenience constant: ``(Status.Running, 0.0)`` - return from a tick callback to keep the action running."""' + } >> "$STUB" +fi + +# Add RUNNING to __all__ if not already present. +if ! grep -q '"RUNNING"' "$STUB"; then + # Insert before the closing ']' of the __all__ list. + sed -i '/^__all__ = \[/,/^]/{/^]/i\ "RUNNING", +}' "$STUB" +fi + +# Strip trailing whitespace defensively. +sed -i 's/[[:space:]]*$//' "$STUB" +echo "Regenerated $STUB" diff --git a/bonsai-py/scripts/verify.py b/bonsai-py/scripts/verify.py index 58a4496..2e8eeb6 100644 --- a/bonsai-py/scripts/verify.py +++ b/bonsai-py/scripts/verify.py @@ -358,6 +358,50 @@ def short_circuit_cb(args, _bb): raise AssertionError("expected RuntimeError on poisoned BT") +def test_type_stubs() -> None: + """Type stub is present, valid syntax, declares every public symbol.""" + import ast + from pathlib import Path + + import bonsai_py + + stub_path = Path(bonsai_py.__file__).parent / "__init__.pyi" + assert stub_path.exists(), f"missing stub: {stub_path}" + print(f" OK: stub present at {stub_path.name}") + + tree = ast.parse(stub_path.read_text()) + print(" OK: stub parses as valid Python syntax") + + declared: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, (ast.ClassDef, ast.FunctionDef)): + declared.add(node.name) + elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): + declared.add(node.target.id) + + expected = set(bonsai_py.__all__) + missing = expected - declared + assert not missing, f"stub missing names: {sorted(missing)}" + print(f" OK: all {len(expected)} __all__ names present in stub") + + # The stub-side __all__ must agree with the package __all__ on the + # full public surface. Drift between the two is the most common cause + # of "mypy sees Status but `from bonsai_py import *` doesn't". + stub_all: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Assign) and any( + isinstance(t, ast.Name) and t.id == "__all__" for t in node.targets + ): + if isinstance(node.value, ast.List): + stub_all = { + elt.value for elt in node.value.elts if isinstance(elt, ast.Constant) + } + if stub_all: + diff = expected.symmetric_difference(stub_all) + assert not diff, f"package and stub __all__ disagree: {sorted(diff)}" + print(" OK: package and stub __all__ agree") + + # --------------------------------------------------------------------------- # Registry — append new test functions above and add them to this list. # Do not reorder or remove existing entries. @@ -367,6 +411,7 @@ def short_circuit_cb(args, _bb): test_status_and_action_args, test_behavior_factories, test_bt_tick_and_telemetry, + test_type_stubs, ] diff --git a/bonsai-py/src/action_args.rs b/bonsai-py/src/action_args.rs index 76fc1e0..60437df 100644 --- a/bonsai-py/src/action_args.rs +++ b/bonsai-py/src/action_args.rs @@ -1,5 +1,6 @@ use bonsai_bt::{ActionArgs, Event}; use pyo3::prelude::*; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::behavior::PyAction; @@ -8,6 +9,7 @@ use crate::behavior::PyAction; /// Constructed by the tick bridge and passed to the user's callback. /// The Rust `ActionArgs::event` field is intentionally not exposed — /// Python users only see `dt` and `action`. +#[gen_stub_pyclass] #[pyclass(frozen, module = "bonsai_py", name = "ActionArgs")] pub struct PyActionArgs { /// Remaining delta time in seconds. @@ -18,6 +20,7 @@ pub struct PyActionArgs { pub action: Py, } +#[gen_stub_pymethods] #[pymethods] impl PyActionArgs { #[new] diff --git a/bonsai-py/src/behavior.rs b/bonsai-py/src/behavior.rs index f07d262..83553d8 100644 --- a/bonsai-py/src/behavior.rs +++ b/bonsai-py/src/behavior.rs @@ -1,6 +1,7 @@ use bonsai_bt::Behavior; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyfunction, gen_stub_pymethods}; /// Wrapper around `Py` that satisfies `bonsai_bt::BT`'s /// `A: Clone + Debug` bounds. @@ -34,6 +35,7 @@ impl std::fmt::Debug for PyAction { /// Construct via the factory functions (`Sequence`, `Action`, `Wait`, ...) /// at the module level. Subtrees are reusable - the same `Behavior` /// can appear as a child of multiple parents. +#[gen_stub_pyclass] #[pyclass(unsendable, frozen, module = "bonsai_py", name = "Behavior")] pub struct PyBehavior { pub(crate) inner: Behavior, @@ -45,6 +47,7 @@ impl PyBehavior { } } +#[gen_stub_pymethods] #[pymethods] impl PyBehavior { fn __repr__(&self) -> String { @@ -73,12 +76,14 @@ fn collect_children(children: Vec>) -> Vec) -> PyBehavior { PyBehavior::wrap(Behavior::Action(PyAction(action))) } +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "Wait")] pub fn wait_fn(seconds: f64) -> PyResult { @@ -88,6 +93,7 @@ pub fn wait_fn(seconds: f64) -> PyResult { Ok(PyBehavior::wrap(Behavior::Wait(seconds))) } +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "WaitForever")] pub fn wait_forever_fn() -> PyBehavior { @@ -96,12 +102,14 @@ pub fn wait_forever_fn() -> PyBehavior { // ----- Decorators ---------------------------------------------------------- +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "Invert")] pub fn invert_fn(child: PyRef<'_, PyBehavior>) -> PyBehavior { PyBehavior::wrap(Behavior::Invert(Box::new(child.inner.clone()))) } +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "AlwaysSucceed")] pub fn always_succeed_fn(child: PyRef<'_, PyBehavior>) -> PyBehavior { @@ -110,36 +118,42 @@ pub fn always_succeed_fn(child: PyRef<'_, PyBehavior>) -> PyBehavior { // ----- Composites (Vec) ----------------------------------------- +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "Sequence")] pub fn sequence_fn(children: Vec>) -> PyBehavior { PyBehavior::wrap(Behavior::Sequence(collect_children(children))) } +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "Select")] pub fn select_fn(children: Vec>) -> PyBehavior { PyBehavior::wrap(Behavior::Select(collect_children(children))) } +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "WhenAll")] pub fn when_all_fn(children: Vec>) -> PyBehavior { PyBehavior::wrap(Behavior::WhenAll(collect_children(children))) } +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "WhenAny")] pub fn when_any_fn(children: Vec>) -> PyBehavior { PyBehavior::wrap(Behavior::WhenAny(collect_children(children))) } +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "After")] pub fn after_fn(children: Vec>) -> PyBehavior { PyBehavior::wrap(Behavior::After(collect_children(children))) } +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "Race")] pub fn race_fn(children: Vec>) -> PyBehavior { @@ -148,6 +162,7 @@ pub fn race_fn(children: Vec>) -> PyBehavior { // ----- Control flow -------------------------------------------------------- +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "If")] pub fn if_fn( @@ -162,6 +177,7 @@ pub fn if_fn( )) } +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "While")] pub fn while_fn( @@ -177,6 +193,7 @@ pub fn while_fn( ))) } +#[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "WhileAll")] pub fn while_all_fn( diff --git a/bonsai-py/src/bin/stub_gen.rs b/bonsai-py/src/bin/stub_gen.rs new file mode 100644 index 0000000..773fff8 --- /dev/null +++ b/bonsai-py/src/bin/stub_gen.rs @@ -0,0 +1,15 @@ +//! Stub-generator binary. Builds the `.pyi` from the `#[gen_stub_*]` +//! annotations sprinkled across the binding crate. +//! +//! Run with `cargo run --bin stub_gen -p bonsai-py`. pyo3-stub-gen reads +//! `pyproject.toml` to determine the package layout and writes the stub +//! to `python/bonsai_py/__init__.pyi`. The companion `scripts/regen-stubs.sh` +//! appends the manual `RUNNING` constant declaration afterwards. + +use pyo3_stub_gen::Result; + +fn main() -> Result<()> { + let stub = bonsai_py::stub_info()?; + stub.generate()?; + Ok(()) +} diff --git a/bonsai-py/src/bt.rs b/bonsai-py/src/bt.rs index 71c1c69..d5e29a5 100644 --- a/bonsai-py/src/bt.rs +++ b/bonsai-py/src/bt.rs @@ -1,6 +1,7 @@ use bonsai_bt::{Event, Status, UpdateArgs, BT}; use pyo3::exceptions::{PyOSError, PyRuntimeError}; use pyo3::prelude::*; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::action_args::PyActionArgs; use crate::behavior::{PyAction, PyBehavior}; @@ -13,6 +14,7 @@ const POISONED_MSG: &str = /// /// Construct from a tree and a blackboard, then drive with `.tick(dt, callback)`. /// The callback receives `(args, blackboard)` and must return `(Status, float)`. +#[gen_stub_pyclass] #[pyclass(unsendable, module = "bonsai_py", name = "BT")] pub struct PyBT { inner: Option>>, @@ -32,6 +34,7 @@ impl PyBT { } } +#[gen_stub_pymethods] #[pymethods] impl PyBT { #[new] diff --git a/bonsai-py/src/lib.rs b/bonsai-py/src/lib.rs index 7923f65..37f8e57 100644 --- a/bonsai-py/src/lib.rs +++ b/bonsai-py/src/lib.rs @@ -1,4 +1,5 @@ use pyo3::prelude::*; +use pyo3_stub_gen::define_stub_info_gatherer; mod action_args; mod behavior; @@ -43,3 +44,7 @@ fn bonsai_py(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } + +// Add pyo3-stub-gen: emits `pub fn stub_info() -> ...` that the +// `stub_gen` binary calls to collect every #[gen_stub_*] annotated item. +define_stub_info_gatherer!(stub_info); diff --git a/bonsai-py/src/status.rs b/bonsai-py/src/status.rs index c52d8f0..045e634 100644 --- a/bonsai-py/src/status.rs +++ b/bonsai-py/src/status.rs @@ -1,11 +1,13 @@ use bonsai_bt::Status; use pyo3::prelude::*; +use pyo3_stub_gen::derive::{gen_stub_pyclass_enum, gen_stub_pymethods}; /// Behavior-tree node result. /// /// Mirrors `bonsai_bt::Status`. Comparable to `int` /// (`Status.Success == 0`, `Failure == 1`, `Running == 2`) and usable /// as a `dict` key or `set` member. +#[gen_stub_pyclass_enum] #[pyclass(eq, eq_int, hash, frozen, from_py_object, module = "bonsai_py", name = "Status")] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub enum PyStatus { @@ -14,6 +16,7 @@ pub enum PyStatus { Running, } +#[gen_stub_pymethods] #[pymethods] impl PyStatus { /// Pickle support: name the singleton by class + variant name, since From f79836b89ac0e31341c0968ac7f8f48a998b89c8 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sat, 16 May 2026 10:58:40 -0500 Subject: [PATCH 11/25] add stub gen to pre commit checks --- .pre-commit-config.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ddbb47..f53bb17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,3 +50,10 @@ repos: pass_filenames: false types: [file, rust] language: system + - id: regen-stubs + name: regenerate bonsai_py type stub + description: Regenerate python/bonsai_py/__init__.pyi from #[gen_stub_*] annotations. If the regenerated stub differs from the committed version, the hook fails so the developer can stage the update. + entry: bash bonsai-py/scripts/regen-stubs.sh + language: system + files: ^(bonsai-py/src/.*\.rs|bonsai-py/python/bonsai_py/__init__\.pyi)$ + pass_filenames: false From fc7fd4090594e11119158e3d60f064048a221762 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sat, 16 May 2026 11:10:11 -0500 Subject: [PATCH 12/25] do not leak bytecode --- bonsai-py/pyproject.toml | 2 ++ bonsai-py/scripts/verify.py | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/bonsai-py/pyproject.toml b/bonsai-py/pyproject.toml index 3f79420..12813aa 100644 --- a/bonsai-py/pyproject.toml +++ b/bonsai-py/pyproject.toml @@ -39,3 +39,5 @@ module-name = "bonsai_py" python-source = "python" features = ["pyo3/extension-module"] strip = true +# Exclude local bytecode caches from the wheel. +exclude = ["python/**/__pycache__/**", "python/**/*.pyc"] diff --git a/bonsai-py/scripts/verify.py b/bonsai-py/scripts/verify.py index 2e8eeb6..bd7a821 100644 --- a/bonsai-py/scripts/verify.py +++ b/bonsai-py/scripts/verify.py @@ -402,6 +402,60 @@ def test_type_stubs() -> None: print(" OK: package and stub __all__ agree") +def test_mypy_strict() -> None: + """mypy --strict accepts a sample script using the typed surface.""" + import importlib.util + import os + import subprocess + import sys + import tempfile + + if importlib.util.find_spec("mypy") is None: + print(" SKIP: mypy not installed (pip install mypy to enable)") + return + + sample = ( + "import bonsai_py as bt\n" + "\n" + "def cb(args: bt.ActionArgs, bb: object) -> tuple[bt.Status, float]:\n" + ' if args.action == "inc":\n' + " return (bt.Status.Success, args.dt)\n" + " return bt.RUNNING\n" + "\n" + 'tree = bt.Sequence([bt.Action("inc"), bt.Wait(1)]) # int coerces to float\n' + 'tree_bt = bt.BT(tree, {"count": 0})\n' + "for _ in range(3):\n" + " res: tuple[bt.Status, float] | None = tree_bt.tick(0.5, cb)\n" + " if res is None:\n" + " tree_bt.reset_bt()\n" + "\n" + 'chained: bt.BT = bt.BT(bt.Action("x"), None).with_telemetry(0, host="0.0.0.0")\n' + ) + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as f: + f.write(sample) + sample_path = f.name + + try: + result = subprocess.run( + [sys.executable, "-m", "mypy", "--strict", sample_path], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + raise AssertionError( + f"mypy --strict failed (exit {result.returncode}):\n" + f"--- stdout ---\n{result.stdout}\n" + f"--- stderr ---\n{result.stderr}" + ) + print(" OK: mypy --strict accepts the sample script") + finally: + os.unlink(sample_path) + + # --------------------------------------------------------------------------- # Registry — append new test functions above and add them to this list. # Do not reorder or remove existing entries. @@ -412,6 +466,7 @@ def test_type_stubs() -> None: test_behavior_factories, test_bt_tick_and_telemetry, test_type_stubs, + test_mypy_strict, ] From 0203c6ce37d162e1ae4fcf8b2db6b3e4b8817f66 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sat, 16 May 2026 16:15:10 -0500 Subject: [PATCH 13/25] add pytest suite, also add to CI --- .github/workflows/rust-pr.yml | 22 ++ bonsai-py/Cargo.toml | 5 +- bonsai-py/pyproject.toml | 8 + bonsai-py/scripts/README.md | 30 ++ bonsai-py/tests/README.md | 59 +++ bonsai-py/tests/__init__.py | 0 bonsai-py/tests/conftest.py | 50 +++ bonsai-py/tests/test_action_args.py | 82 +++++ bonsai-py/tests/test_behavior.py | 360 +++++++++++++++++++ bonsai-py/tests/test_bt.py | 229 ++++++++++++ bonsai-py/tests/test_drift.py | 69 ++++ bonsai-py/tests/test_module.py | 47 +++ bonsai-py/tests/test_performance.py | 53 +++ bonsai-py/tests/test_status.py | 75 ++++ bonsai-py/tests/test_telemetry.py | 60 ++++ bonsai-py/tests/test_threading_and_pickle.py | 61 ++++ 16 files changed, 1206 insertions(+), 4 deletions(-) create mode 100644 bonsai-py/scripts/README.md create mode 100644 bonsai-py/tests/README.md create mode 100644 bonsai-py/tests/__init__.py create mode 100644 bonsai-py/tests/conftest.py create mode 100644 bonsai-py/tests/test_action_args.py create mode 100644 bonsai-py/tests/test_behavior.py create mode 100644 bonsai-py/tests/test_bt.py create mode 100644 bonsai-py/tests/test_drift.py create mode 100644 bonsai-py/tests/test_module.py create mode 100644 bonsai-py/tests/test_performance.py create mode 100644 bonsai-py/tests/test_status.py create mode 100644 bonsai-py/tests/test_telemetry.py create mode 100644 bonsai-py/tests/test_threading_and_pickle.py diff --git a/.github/workflows/rust-pr.yml b/.github/workflows/rust-pr.yml index 5026a5f..f1e41fa 100644 --- a/.github/workflows/rust-pr.yml +++ b/.github/workflows/rust-pr.yml @@ -28,3 +28,25 @@ jobs: run: cargo build --examples - name: Run tests run: cargo test --verbose + pytest: + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.10", "3.13"] + steps: + - name: Install libudev + run: sudo apt-get update && sudo apt-get install -y libudev-dev pkg-config librust-alsa-sys-dev + - uses: actions/checkout@v6.0.2 + - uses: actions/setup-python@v6.2.0 + with: + python-version: ${{ matrix.python }} + - uses: dtolnay/rust-toolchain@stable + - name: Install maturin and test deps + run: pip install "maturin>=1.7,<2.0" pytest pytest-timeout mypy + - name: Build and install bonsai-py + working-directory: bonsai-py + run: maturin develop --release + - name: Run pytest + run: pytest -v bonsai-py/tests/ + - name: Run verify.py (sanity) + run: python bonsai-py/scripts/verify.py diff --git a/bonsai-py/Cargo.toml b/bonsai-py/Cargo.toml index c96608b..21a2c16 100644 --- a/bonsai-py/Cargo.toml +++ b/bonsai-py/Cargo.toml @@ -5,10 +5,7 @@ edition = "2021" rust-version = "1.80.0" description = "Python bindings for the bonsai-bt behavior tree library" license = "MIT" -authors = [ - "Kristoffer Solberg Rakstad ", - "Anmol Kathail ", -] +authors = ["Kristoffer Solberg Rakstad "] repository = "https://github.com/sollimann/bonsai.git" homepage = "https://github.com/sollimann/bonsai" publish = false diff --git a/bonsai-py/pyproject.toml b/bonsai-py/pyproject.toml index 12813aa..8a6560d 100644 --- a/bonsai-py/pyproject.toml +++ b/bonsai-py/pyproject.toml @@ -41,3 +41,11 @@ features = ["pyo3/extension-module"] strip = true # Exclude local bytecode caches from the wheel. exclude = ["python/**/__pycache__/**", "python/**/*.pyc"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +markers = [ + "perf: performance budget tests (always-on; generous bounds)", + "bench: heavy microbenchmarks (opt-in via `pytest -m bench`)", +] +timeout = 10 diff --git a/bonsai-py/scripts/README.md b/bonsai-py/scripts/README.md new file mode 100644 index 0000000..d8135c9 --- /dev/null +++ b/bonsai-py/scripts/README.md @@ -0,0 +1,30 @@ +# bonsai-py scripts + +Developer scripts for working on the `bonsai_py` extension module. + +## Scripts + +| Script | Purpose | +|---|---| +| [regen-stubs.sh](regen-stubs.sh) | Regenerates `python/bonsai_py/__init__.pyi` from the `#[gen_stub_*]` annotations on the Rust side. Run after editing any annotated `#[pyclass]` / `#[pyfunction]` / `#[pymethods]`. Also runs automatically via the `regen-stubs` pre-commit hook and is enforced in CI. | +| [verify.py](verify.py) | End-to-end smoke test of the installed wheel: imports `bonsai_py`, exercises every public API (Status, ActionArgs, all 14 factories, BT tick/telemetry, type stub presence, mypy `--strict` on a sample script). Standalone — not a pytest suite (the long-form behavior coverage lives in [../tests/](../tests/)). | + +## Running + +From the repository root, with a venv that has the wheel installed (`maturin develop --release` from `bonsai-py/`): + +```bash +# Regenerate stubs after editing Rust annotations +bash bonsai-py/scripts/regen-stubs.sh + +# Run the verification smoke test (every test always runs; exits 0 on success) +python bonsai-py/scripts/verify.py +``` + +## Dependencies + +`verify.py` uses `mypy` for the strict-typing check (skipped with a warning if `mypy` is not installed): + +```bash +pip install mypy +``` diff --git a/bonsai-py/tests/README.md b/bonsai-py/tests/README.md new file mode 100644 index 0000000..61cfd15 --- /dev/null +++ b/bonsai-py/tests/README.md @@ -0,0 +1,59 @@ +# bonsai-py tests + +Pytest suite for the `bonsai_py` extension module. Primarily used to prevent any drift between the Rust bindings (`bonsai-py/src/*.rs`) and the Python surface: every `#[pyclass]`, `#[pymethods]`, and `#[pyfunction]` has at least one Python test exercising it. + +## Test files + +| File | What it covers | +|---|---| +| [conftest.py](conftest.py) | Shared fixtures: `free_port`, `noop_callback`, `counting_callback`, `basic_tree`. | +| [test_status.py](test_status.py) | `Status` enum: variants, `eq_int` discriminants (0/1/2), equality, singleton identity, hash, repr, `__module__`, pickle/copy round-trip preserves singleton identity. | +| [test_action_args.py](test_action_args.py) | `ActionArgs`: construct with str/None/dict/object/int-dt; `dt` and `action` are read-only; repr format; `__module__`. | +| [test_behavior.py](test_behavior.py) | All 14 factories exported and callable; each builds with expected repr; validation guards (empty `While`/`WhileAll` body, NaN `Wait`); empty composites + neg/inf/int `Wait` pass through; subtree reuse; kwargs on `If`; tuple accepted / generator rejected for children; identity-based equality per variant; ported Rust `behavior_tests.rs` cases (immediate termination, wait timing, select short-circuit, if branches, invert, always-succeed, when-all, while-loop). | +| [test_bt.py](test_bt.py) | `BT` construction (dict/None/custom blackboard); doctest-equivalent five-tick port; tick return shape; tick on finished returns None; `tick_count` survives `reset_bt`; callback exceptions propagate; bad return shape rejected; `WhenAll` short-circuits siblings after a raise; NaN dt sanitized to 0.0; blackboard identity + mutation persistence; action identity through callback; `reset_bt` preserves blackboard; `RUNNING` constant value and usage. | +| [test_telemetry.py](test_telemetry.py) | `with_telemetry` is chainable; accepts `host` kwarg; second bind on same port raises `OSError`; binding to unreachable IP raises `OSError`; failed `with_telemetry` poisons the BT (every subsequent method raises `RuntimeError`). | +| [test_module.py](test_module.py) | `__version__ == "0.12.0"`; module docstring present; `__all__` lists exactly the 19 expected names; all names accessible; `RUNNING` is `(Status.Running, 0.0)`; `.pyi` stub ships with the wheel. | +| [test_threading_and_pickle.py](test_threading_and_pickle.py) | `BT` is unsendable across threads (PyO3 `PanicException`); `BT`, `Behavior`, `ActionArgs` are unpicklable; `Status` IS picklable (multiprocessing-friendly). | +| [test_performance.py](test_performance.py) | `@pytest.mark.perf`: 100 ticks under 500 ms, 1000 constructions under 5 s. `@pytest.mark.bench` (opt-in): tick throughput microbenchmark. | +| [test_drift.py](test_drift.py) | Checks that every Rust `#[pyo3(name=...)]` symbol appears in `__all__`; every `#[pyclass(name=..., module='bonsai_py')]` appears in `__all__`; every name in `__all__` is mentioned in at least one other test file. | + +## Running + +From the repository root, with the wheel installed in your venv (`maturin develop --release` from `bonsai-py/`): + +```bash +# Full suite (default — runs perf budget tests, skips benchmarks) +pytest bonsai-py/tests/ + +# Verbose +pytest -v bonsai-py/tests/ + +# Single file +pytest -v bonsai-py/tests/test_status.py + +# Single test +pytest -v bonsai-py/tests/test_bt.py::TestTick::test_doctest_equivalent + +# Drift gate only +pytest -v bonsai-py/tests/test_drift.py + +# Skip perf budget tests +pytest -v -m "not perf" bonsai-py/tests/ + +# Run microbenchmarks only (prints throughput; no assertions) +pytest -v -m bench bonsai-py/tests/ +``` + +A `pytest-timeout` of 10 seconds per test is configured in [pyproject.toml](../pyproject.toml). If you change a test that legitimately needs longer, bump the per-test timeout with `@pytest.mark.timeout(30)`. + +## Dependencies + +```bash +pip install pytest pytest-timeout +``` + +`mypy` is used by [../scripts/verify.py](../scripts/verify.py), not by this suite directly. + +## CI + +The `pytest` job in [.github/workflows/rust-pr.yml](../../.github/workflows/rust-pr.yml) runs this suite on Python 3.10 and 3.13 (matrix) on every PR, after building the wheel in release mode. diff --git a/bonsai-py/tests/__init__.py b/bonsai-py/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bonsai-py/tests/conftest.py b/bonsai-py/tests/conftest.py new file mode 100644 index 0000000..dd7cf54 --- /dev/null +++ b/bonsai-py/tests/conftest.py @@ -0,0 +1,50 @@ +"""Shared fixtures for bonsai-py tests.""" +from __future__ import annotations + +import socket +from typing import Any, Callable + +import pytest + +import bonsai_py as bt + + +@pytest.fixture +def free_port() -> int: + """Return a free TCP port (kernel-assigned).""" + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + return int(s.getsockname()[1]) + + +@pytest.fixture +def noop_callback() -> Callable[[Any, Any], tuple[Any, float]]: + """A callback that returns Success immediately with no side effects.""" + + def cb(_args: Any, _bb: Any) -> tuple[Any, float]: + return (bt.Status.Success, 0.0) + + return cb + + +@pytest.fixture +def counting_callback() -> tuple[Callable[[Any, Any], tuple[Any, float]], list[Any]]: + """A callback that records every action it sees and returns Success.""" + calls: list[Any] = [] + + def cb(args: Any, _bb: Any) -> tuple[Any, float]: + calls.append(args.action) + return (bt.Status.Success, args.dt) + + return cb, calls + + +@pytest.fixture +def basic_tree() -> bt.Behavior: + """A small reusable tree: Sequence([Wait(0.5), Action('inc'), Wait(0.5), Action('inc')]).""" + return bt.Sequence([ + bt.Wait(0.5), + bt.Action("inc"), + bt.Wait(0.5), + bt.Action("inc"), + ]) diff --git a/bonsai-py/tests/test_action_args.py b/bonsai-py/tests/test_action_args.py new file mode 100644 index 0000000..e23c269 --- /dev/null +++ b/bonsai-py/tests/test_action_args.py @@ -0,0 +1,82 @@ +"""ActionArgs: construction, identity, frozen, repr.""" +from __future__ import annotations + +import math + +import pytest + +import bonsai_py as bt + + +class TestActionArgsConstruction: + def test_construct_with_string_action(self) -> None: + """String action is stored verbatim and accessible via .action.""" + a = bt.ActionArgs(0.5, "inc") + assert a.dt == 0.5 + assert a.action == "inc" + + def test_construct_with_none_action(self) -> None: + """None is a valid action value (preserved by identity).""" + a = bt.ActionArgs(0.0, None) + assert a.action is None + + def test_construct_with_arbitrary_object(self) -> None: + """Any Python object can be an action; identity is preserved.""" + sentinel = object() + a = bt.ActionArgs(0.1, sentinel) + assert a.action is sentinel + + def test_construct_with_dict_action(self) -> None: + """Dict actions are passed by reference — mutations are visible.""" + d = {"type": "fire"} + a = bt.ActionArgs(0.0, d) + assert a.action is d + + def test_construct_with_int_dt(self) -> None: + """int dt coerces to float at the FFI boundary.""" + a = bt.ActionArgs(1, "x") + assert a.dt == 1.0 + assert isinstance(a.dt, float) + + @pytest.mark.parametrize("dt", [float("nan"), float("inf"), float("-inf")]) + def test_construct_with_special_dt(self, dt: float) -> None: + """NaN / +inf / -inf dt are accepted at construction — no guard at the FFI boundary.""" + a = bt.ActionArgs(dt, "x") + if math.isnan(dt): + assert math.isnan(a.dt) + else: + assert a.dt == dt + + +class TestActionArgsImmutability: + def test_dt_readonly(self) -> None: + """ActionArgs.dt is frozen — assignment raises AttributeError.""" + a = bt.ActionArgs(0.5, "x") + with pytest.raises(AttributeError): + a.dt = 0.7 # type: ignore[misc] + + def test_action_readonly(self) -> None: + """ActionArgs.action is frozen — assignment raises AttributeError.""" + a = bt.ActionArgs(0.5, "x") + with pytest.raises(AttributeError): + a.action = "y" # type: ignore[misc] + + +class TestActionArgsRepr: + @pytest.mark.parametrize( + "dt, action, expected", + [ + (0.5, "inc", "ActionArgs(dt=0.5, action='inc')"), + (0.0, None, "ActionArgs(dt=0, action=None)"), + (1.0, 42, "ActionArgs(dt=1, action=42)"), + ], + ) + def test_repr_format(self, dt: float, action: object, expected: str) -> None: + """repr() renders as `ActionArgs(dt=..., action=...)` with Python repr on the action.""" + assert repr(bt.ActionArgs(dt, action)) == expected + + +class TestActionArgsModuleAttribution: + def test_module(self) -> None: + """ActionArgs.__module__ is `bonsai_py` (required for pickle / introspection).""" + assert bt.ActionArgs.__module__ == "bonsai_py" diff --git a/bonsai-py/tests/test_behavior.py b/bonsai-py/tests/test_behavior.py new file mode 100644 index 0000000..347e9cb --- /dev/null +++ b/bonsai-py/tests/test_behavior.py @@ -0,0 +1,360 @@ +"""Behavior class + 14 factory functions + ports of Rust behavior_tests.""" +from __future__ import annotations + +from typing import Any, Callable + +import pytest + +import bonsai_py as bt + +FACTORY_NAMES = ( + "Action", "Wait", "WaitForever", + "Invert", "AlwaysSucceed", + "Sequence", "Select", "WhenAll", "WhenAny", "After", "Race", + "If", "While", "WhileAll", +) + + +class TestFactoriesPresent: + @pytest.mark.parametrize("name", FACTORY_NAMES) + def test_factory_exported(self, name: str) -> None: + """Each of the 14 factory names is importable and callable.""" + assert hasattr(bt, name), f"missing factory {name}" + assert callable(getattr(bt, name)), f"{name} not callable" + + def test_factory_count(self) -> None: + """Exactly 14 factory names tracked — guards against silent additions.""" + assert len(FACTORY_NAMES) == 14 + + +def _trivial(label: str) -> bt.Behavior: + return bt.Action(label) + + +class TestFactoryConstruction: + @pytest.mark.parametrize( + "build, expected_repr", + [ + (lambda: bt.Action("x"), "Action(...)"), + (lambda: bt.Wait(1.0), "Wait(1)"), + (lambda: bt.WaitForever(), "WaitForever"), + (lambda: bt.Invert(_trivial("c")), "Invert(...)"), + (lambda: bt.AlwaysSucceed(_trivial("c")), "AlwaysSucceed(...)"), + (lambda: bt.Sequence([_trivial("a"), _trivial("b")]), "Sequence(2)"), + (lambda: bt.Select([_trivial("a")]), "Select(1)"), + (lambda: bt.WhenAll([_trivial("a")]), "WhenAll(1)"), + (lambda: bt.WhenAny([_trivial("a"), _trivial("b")]), "WhenAny(2)"), + (lambda: bt.After([_trivial("a")]), "After(1)"), + (lambda: bt.Race([_trivial("a"), _trivial("b")]), "Race(2)"), + (lambda: bt.If(_trivial("c"), _trivial("s"), _trivial("f")), "If(...)"), + (lambda: bt.While(_trivial("c"), [_trivial("b")]), "While(1)"), + (lambda: bt.WhileAll(_trivial("c"), [_trivial("b")]), "WhileAll(1)"), + ], + ) + def test_each_factory_builds( + self, build: Callable[[], bt.Behavior], expected_repr: str + ) -> None: + """Each factory builds a Behavior with the expected bounded repr.""" + node = build() + assert isinstance(node, bt.Behavior) + assert repr(node) == expected_repr + + +class TestValidationGuards: + def test_while_empty_body_raises(self) -> None: + """While with empty body raises ValueError (would panic in Rust).""" + with pytest.raises(ValueError, match="must not be empty"): + bt.While(_trivial("c"), []) + + def test_whileall_empty_body_raises(self) -> None: + """WhileAll with empty body raises ValueError (would panic in Rust).""" + with pytest.raises(ValueError, match="must not be empty"): + bt.WhileAll(_trivial("c"), []) + + def test_wait_nan_raises(self) -> None: + """Wait(NaN) raises ValueError at the Python boundary, never reaches Rust.""" + with pytest.raises(ValueError, match="NaN"): + bt.Wait(float("nan")) + + @pytest.mark.parametrize( + "build", + [ + lambda: bt.Sequence([]), + lambda: bt.Select([]), + lambda: bt.WhenAll([]), + lambda: bt.WhenAny([]), + lambda: bt.After([]), + lambda: bt.Race([]), + ], + ) + def test_other_empty_composites_allowed( + self, build: Callable[[], bt.Behavior] + ) -> None: + """Empty Sequence/Select/etc. are allowed (don't panic in Rust).""" + node = build() + assert isinstance(node, bt.Behavior) + + @pytest.mark.parametrize("value", [-1.0, 0.0, float("inf"), 1]) + def test_wait_passthrough_values(self, value: float) -> None: + """Negative, zero, inf, int -- all accepted at the boundary.""" + assert isinstance(bt.Wait(value), bt.Behavior) + + +class TestSubtreeReuse: + def test_leaf_reuse(self) -> None: + """The same Behavior leaf can appear as a child of one parent multiple times.""" + wait = bt.Wait(1.0) + tree = bt.Sequence([wait, wait, wait]) + assert repr(tree) == "Sequence(3)" + + def test_nested_subtree_reuse(self) -> None: + """Nested subtrees are reusable too — the same composite can be a child multiple times.""" + inner = bt.Sequence([bt.Wait(0.1), bt.Action("x")]) + outer = bt.Sequence([inner, inner, inner]) + assert repr(outer) == "Sequence(3)" + + def test_subtree_reused_across_bts(self) -> None: + """The same Behavior root can drive multiple independent BTs without interference.""" + subtree = bt.Sequence([bt.Action("a"), bt.Action("b")]) + calls1: list[Any] = [] + calls2: list[Any] = [] + + def make_cb(out: list[Any]) -> Callable[[Any, Any], tuple[bt.Status, float]]: + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + out.append(args.action) + return (bt.Status.Success, 0.0) + return cb + + bt.BT(subtree, None).tick(0.0, make_cb(calls1)) + bt.BT(subtree, None).tick(0.0, make_cb(calls2)) + assert calls1 == ["a", "b"] + assert calls2 == ["a", "b"] + + +class TestArgumentParsing: + def test_kwargs_on_if(self) -> None: + """If accepts cond / on_success / on_failure as keyword arguments.""" + tree = bt.If( + cond=bt.Action("c"), + on_success=bt.Action("s"), + on_failure=bt.Action("f"), + ) + assert repr(tree) == "If(...)" + + def test_tuple_accepted_for_children(self) -> None: + """PyO3 Vec extractor accepts indexable sequences, not just list.""" + tree = bt.Sequence((bt.Action("a"), bt.Action("b"))) + assert repr(tree) == "Sequence(2)" + + def test_generator_rejected_for_children(self) -> None: + """Generators are NOT accepted (extractor needs random access).""" + with pytest.raises(TypeError): + bt.Sequence(bt.Action(x) for x in ["a", "b"]) + + +class TestIdentityEquality: + @pytest.mark.parametrize( + "build", + [ + lambda: bt.Action("x"), + lambda: bt.Wait(1.0), + lambda: bt.WaitForever(), + lambda: bt.Invert(bt.Action("x")), + lambda: bt.AlwaysSucceed(bt.Action("x")), + lambda: bt.Sequence([bt.Action("x")]), + lambda: bt.Select([bt.Action("x")]), + lambda: bt.WhenAll([bt.Action("x")]), + lambda: bt.WhenAny([bt.Action("x")]), + lambda: bt.After([bt.Action("x")]), + lambda: bt.Race([bt.Action("x")]), + lambda: bt.If(bt.Action("c"), bt.Action("s"), bt.Action("f")), + lambda: bt.While(bt.Action("c"), [bt.Action("b")]), + lambda: bt.WhileAll(bt.Action("c"), [bt.Action("b")]), + ], + ) + def test_identity_based_eq_per_variant( + self, build: Callable[[], bt.Behavior] + ) -> None: + """Two structurally-identical Behaviors compare unequal — equality is identity.""" + a, b = build(), build() + assert a is not b + assert (a == b) is False + assert a != b + + +class TestBehaviorAttribution: + def test_module(self) -> None: + """Behavior.__module__ is `bonsai_py` (required for pickle / introspection).""" + assert bt.Behavior.__module__ == "bonsai_py" + + +# ---------- Ports of Rust behavior_tests.rs (golden-truth equivalence) ---------- + +class TestBehaviorRustParity: + """Tests ported from bonsai/tests/behavior_tests.rs. If a Rust test changes, + its Python counterpart must change too. Main drift gate against Rust semantics.""" + + def test_immediate_termination(self) -> None: + """A 0.0s tick runs all leaves; reset_bt then re-runs the whole sequence.""" + acc = [0] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + if args.action == "inc": + acc[0] += 1 + return (bt.Status.Success, args.dt) + + tree = bt.Sequence([bt.Action("inc"), bt.Action("inc")]) + b = bt.BT(tree, None) + b.tick(0.0, cb) + assert acc[0] == 2 + assert b.is_finished() + b.reset_bt() + b.tick(1.0, cb) + assert acc[0] == 4 + assert b.is_finished() + + def test_sequence_of_wait_then_action(self) -> None: + """A single tick with enough dt completes both Wait and the trailing Action.""" + seen: list[Any] = [] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + seen.append(args.action) + return (bt.Status.Success, args.dt) + + b = bt.BT(bt.Sequence([bt.Wait(1.0), bt.Action("inc")]), None) + b.tick(1.0, cb) + assert seen == ["inc"] + + def test_wait_half_then_half(self) -> None: + """Two 0.5s ticks accumulate to clear a 1.0s Wait before the Action fires.""" + seen: list[Any] = [] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + seen.append(args.action) + return (bt.Status.Success, args.dt) + + b = bt.BT(bt.Sequence([bt.Wait(1.0), bt.Action("inc")]), None) + b.tick(0.5, cb) + assert seen == [] + b.tick(0.5, cb) + assert seen == ["inc"] + + def test_select_succeed_on_first(self) -> None: + """Select short-circuits at the first Success; later siblings are never invoked.""" + calls: list[Any] = [] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + calls.append(args.action) + return (bt.Status.Success, args.dt) + + tree = bt.Select([bt.Action("a"), bt.Action("b"), bt.Action("c")]) + b = bt.BT(tree, None) + b.tick(0.1, cb) + assert calls == ["a"] + + def test_select_first_failure_tries_next(self) -> None: + """Select advances past Failures and reports Success at the first successful child.""" + calls: list[Any] = [] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + calls.append(args.action) + if args.action == "fail": + return (bt.Status.Failure, args.dt) + return (bt.Status.Success, args.dt) + + tree = bt.Select([bt.Action("fail"), bt.Action("ok")]) + b = bt.BT(tree, None) + result = b.tick(0.1, cb) + assert result is not None + status, _ = result + assert status == bt.Status.Success + assert calls == ["fail", "ok"] + + def test_if_true_branch(self) -> None: + """If runs on_success when the condition succeeds; on_failure is not invoked.""" + seen: list[Any] = [] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + seen.append(args.action) + return (bt.Status.Success, args.dt) + + tree = bt.If(bt.Action("cond_true"), bt.Action("yes"), bt.Action("no")) + b = bt.BT(tree, None) + b.tick(0.1, cb) + assert "yes" in seen + assert "no" not in seen + + def test_if_false_branch(self) -> None: + """If runs on_failure when the condition fails; on_success is not invoked.""" + seen: list[Any] = [] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + seen.append(args.action) + if args.action == "cond_false": + return (bt.Status.Failure, args.dt) + return (bt.Status.Success, args.dt) + + tree = bt.If(bt.Action("cond_false"), bt.Action("yes"), bt.Action("no")) + b = bt.BT(tree, None) + b.tick(0.1, cb) + assert "no" in seen + assert "yes" not in seen + + def test_invert_swaps_outcomes(self) -> None: + """Invert flips Success <-> Failure on the child's return status.""" + def yields_success(_a: Any, _b: Any) -> tuple[bt.Status, float]: + return (bt.Status.Success, 0.0) + + def yields_failure(_a: Any, _b: Any) -> tuple[bt.Status, float]: + return (bt.Status.Failure, 0.0) + + b1 = bt.BT(bt.Invert(bt.Action("x")), None) + r = b1.tick(0.0, yields_success) + assert r is not None + assert r[0] == bt.Status.Failure + + b2 = bt.BT(bt.Invert(bt.Action("x")), None) + r = b2.tick(0.0, yields_failure) + assert r is not None + assert r[0] == bt.Status.Success + + def test_always_succeed_swallows_failure(self) -> None: + """AlwaysSucceed coerces a child's Failure into Success.""" + def yields_failure(_a: Any, _b: Any) -> tuple[bt.Status, float]: + return (bt.Status.Failure, 0.0) + + b = bt.BT(bt.AlwaysSucceed(bt.Action("x")), None) + r = b.tick(0.0, yields_failure) + assert r is not None + assert r[0] == bt.Status.Success + + def test_when_all_waits_for_all(self) -> None: + """WhenAll blocks the parent Sequence until both parallel children finish.""" + seen: list[Any] = [] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + seen.append(args.action) + return (bt.Status.Success, args.dt) + + tree = bt.Sequence([ + bt.WhenAll([bt.Wait(0.5), bt.Wait(1.0)]), + bt.Action("after"), + ]) + b = bt.BT(tree, None) + b.tick(0.5, cb) + assert seen == [] + b.tick(0.5, cb) + assert seen == ["after"] + + def test_while_loops_until_cond_fails(self) -> None: + """While re-runs its body each iteration while the cond stays Running.""" + seen: list[str] = [] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + seen.append(args.action) + return (bt.Status.Success, args.dt) + + tree = bt.While(bt.Wait(50.0), [bt.Wait(0.5), bt.Action("tick"), bt.Wait(0.5)]) + b = bt.BT(tree, None) + b.tick(10.0, cb) + assert seen.count("tick") == 10 diff --git a/bonsai-py/tests/test_bt.py b/bonsai-py/tests/test_bt.py new file mode 100644 index 0000000..5104a2a --- /dev/null +++ b/bonsai-py/tests/test_bt.py @@ -0,0 +1,229 @@ +"""BT class: tick mechanics, callback errors, blackboard, reset, finished state.""" +from __future__ import annotations + +from typing import Any, Callable + +import pytest + +import bonsai_py as bt + + +class TestBTConstruction: + def test_dict_blackboard(self) -> None: + """A dict can serve as the blackboard; round-trips by equality.""" + b = bt.BT(bt.Action("x"), {"k": 1}) + assert b.blackboard() == {"k": 1} + + def test_none_blackboard(self) -> None: + """None is a legal blackboard for trees that don't need shared state.""" + b = bt.BT(bt.Action("x"), None) + assert b.blackboard() is None + + def test_custom_blackboard(self) -> None: + """Any Python object can be the blackboard; identity is preserved.""" + class State: + def __init__(self) -> None: + self.counter = 0 + + s = State() + b = bt.BT(bt.Action("x"), s) + assert b.blackboard() is s + + def test_module(self) -> None: + """BT.__module__ is `bonsai_py` (required for pickle / introspection).""" + assert bt.BT.__module__ == "bonsai_py" + + +class TestTick: + def test_doctest_equivalent(self) -> None: + """Line-for-line port of the bonsai/src/lib.rs Rust doctest: 5 ticks at 0.5s land count==1.""" + tree = bt.Sequence([ + bt.Wait(1.0), bt.Action("inc"), + bt.Wait(1.0), bt.Action("inc"), + bt.Wait(0.5), bt.Action("dec"), + ]) + bb = {"count": 0} + b = bt.BT(tree, bb) + acc = 0 + + def cb(args: Any, _blackboard: Any) -> tuple[bt.Status, float]: + nonlocal acc + if args.action == "inc": + acc += 1 + return (bt.Status.Success, args.dt) + if args.action == "dec": + acc -= 1 + return (bt.Status.Success, args.dt) + return bt.RUNNING + + for _ in range(5): + b.tick(0.5, cb) + bb["count"] = acc + assert bb["count"] == 1 + assert b.tick_count() == 5 + + def test_tick_returns_status_and_dt( + self, + basic_tree: bt.Behavior, + noop_callback: Callable[[Any, Any], tuple[Any, float]], + ) -> None: + """tick() returns a (Status, float) tuple when the BT has not yet finished.""" + b = bt.BT(basic_tree, None) + result = b.tick(2.0, noop_callback) + assert result is not None + status, remaining = result + assert isinstance(status, bt.Status) + assert isinstance(remaining, float) + + def test_tick_on_finished_returns_none(self) -> None: + """Once is_finished() is True, every subsequent tick() returns None.""" + def done(_a: Any, _b: Any) -> tuple[bt.Status, float]: + return (bt.Status.Success, 0.0) + + b = bt.BT(bt.Action("x"), None) + b.tick(0.0, done) + assert b.is_finished() + assert b.tick(0.0, done) is None + + def test_reset_bt_on_unstarted_tree(self) -> None: + """reset_bt() on a never-ticked BT is a no-op; subsequent tick still works normally.""" + b = bt.BT(bt.Action("x"), None) + b.reset_bt() + assert not b.is_finished() + assert b.tick_count() == 0 + result = b.tick(0.0, lambda _a, _bb: (bt.Status.Success, 0.0)) + assert result is not None + + def test_tick_count_survives_reset( + self, + noop_callback: Callable[[Any, Any], tuple[Any, float]], + ) -> None: + """tick_count accumulates across ticks and persists across reset_bt (never zeroed).""" + b = bt.BT(bt.Wait(10.0), None) + for _ in range(3): + b.tick(0.1, noop_callback) + assert b.tick_count() == 3 + b.reset_bt() + assert b.tick_count() == 3, "tick_count must survive reset_bt" + assert not b.is_finished() + + +class TestCallbackContract: + def test_callback_exception_propagates_with_message(self) -> None: + """A Python exception raised inside the callback bubbles up through tick() intact.""" + def boom(_a: Any, _b: Any) -> tuple[bt.Status, float]: + raise ValueError("boom") + + b = bt.BT(bt.Action("x"), None) + with pytest.raises(ValueError, match="boom"): + b.tick(0.0, boom) + + def test_callback_wrong_return_shape_rejected(self) -> None: + """A callback that returns a non-(Status, float) value is rejected by the extractor.""" + def bad(_a: Any, _b: Any) -> str: + return "not a tuple" + + b = bt.BT(bt.Action("x"), None) + with pytest.raises(Exception): + b.tick(0.0, bad) + + def test_when_all_short_circuits_on_callback_raise(self) -> None: + """After a callback raises on a WhenAll child, later siblings in the same tick are not invoked.""" + order: list[str] = [] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + order.append(args.action) + if args.action == "b": + raise ValueError("stop") + return (bt.Status.Success, 0.0) + + tree = bt.WhenAll([bt.Action("a"), bt.Action("b"), bt.Action("c")]) + b = bt.BT(tree, None) + with pytest.raises(ValueError): + b.tick(0.0, cb) + assert order == ["a", "b"] + + def test_callback_returns_nan_dt_sanitized(self) -> None: + """A NaN dt returned from the callback is sanitized by upstream BT and never surfaces to Python.""" + import math + + def nan_cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + if args.action == "x": + return (bt.Status.Running, float("nan")) + return (bt.Status.Success, args.dt) + + b = bt.BT(bt.Sequence([bt.Action("x")]), None) + result = b.tick(1.0, nan_cb) + assert result is not None + _, dt = result + assert not math.isnan(dt) + + +class TestBlackboard: + def test_blackboard_identity_preserved(self) -> None: + """blackboard() returns the same Python object passed to BT() — not a copy.""" + bb = {"count": 0} + b = bt.BT(bt.Action("x"), bb) + assert b.blackboard() is bb + + def test_blackboard_mutation_persists_via_callback(self) -> None: + """Mutations done through the callback's blackboard handle persist in the original object.""" + bb = {"count": 0} + + def inc(_args: Any, blackboard: Any) -> tuple[bt.Status, float]: + blackboard["count"] += 1 + return (bt.Status.Success, 0.0) + + b = bt.BT(bt.Sequence([bt.Action("x"), bt.Action("x")]), bb) + b.tick(0.0, inc) + assert bb["count"] == 2 + + def test_action_identity_through_callback(self) -> None: + """The action object passed to bt.Action(...) arrives at the callback by identity, not equality.""" + sentinel = object() + seen: list[bool] = [] + + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + seen.append(args.action is sentinel) + return (bt.Status.Success, 0.0) + + b = bt.BT(bt.Action(sentinel), None) + b.tick(0.0, cb) + assert seen == [True] + + def test_reset_preserves_blackboard(self) -> None: + """reset_bt() rewinds tree state but does NOT touch the blackboard contents.""" + bb = {"count": 5} + b = bt.BT(bt.Action("x"), bb) + + def done(_a: Any, _b: Any) -> tuple[bt.Status, float]: + return (bt.Status.Success, 0.0) + + b.tick(0.0, done) + b.reset_bt() + assert b.blackboard() is bb + assert bb["count"] == 5 + + +class TestRunningConstant: + def test_value(self) -> None: + """bt.RUNNING is the tuple (Status.Running, 0.0).""" + assert bt.RUNNING == (bt.Status.Running, 0.0) + + def test_first_element_is_running(self) -> None: + """Unpacking bt.RUNNING yields Status.Running as the first element.""" + status, _ = bt.RUNNING + assert status is bt.Status.Running + + def test_used_as_callback_return(self) -> None: + """`return bt.RUNNING` from a callback is a valid Running tick — BT stays unfinished.""" + + def keep_running(_a: Any, _b: Any) -> tuple[bt.Status, float]: + return bt.RUNNING + + b = bt.BT(bt.Action("x"), None) + result = b.tick(0.0, keep_running) + assert result is not None + status, _ = result + assert status == bt.Status.Running + assert not b.is_finished() diff --git a/bonsai-py/tests/test_drift.py b/bonsai-py/tests/test_drift.py new file mode 100644 index 0000000..b41bebf --- /dev/null +++ b/bonsai-py/tests/test_drift.py @@ -0,0 +1,69 @@ +"""Drift gate: every Rust binding has at least one exercising Python test. + +Two parity checks: +1. Every `#[pyfunction]` in `bonsai-py/src/*.rs` (with a `#[pyo3(name = "X")]` + override) appears in `bonsai_py.__all__`. +2. Every public `#[pyclass]` (with `module = "bonsai_py"` set) appears in + `bonsai_py.__all__`. + +If a Rust contributor adds a binding and forgets to: + - add it to __all__, + - add a Python test exercising it, +then `test_no_unexercised_factories` fails CI. +""" +from __future__ import annotations + +import re +from pathlib import Path + +import bonsai_py as bt + +SRC_DIR = Path(__file__).resolve().parent.parent / "src" + +RUST_NAME_RE = re.compile(r'#\[pyo3\(\s*name\s*=\s*"([A-Za-z_][A-Za-z0-9_]*)"\s*\)\]') +PYCLASS_NAME_RE = re.compile( + r'#\[pyclass\([^\]]*\bname\s*=\s*"([A-Za-z_][A-Za-z0-9_]*)"[^\]]*\)\]' +) + + +def _names_from_rust(pattern: re.Pattern[str]) -> set[str]: + names: set[str] = set() + for path in SRC_DIR.rglob("*.rs"): + names.update(pattern.findall(path.read_text(encoding="utf-8"))) + return names + + +def test_every_rust_pyfunction_in_all() -> None: + """Every Rust #[pyo3(name=...)] symbol scanned from src/*.rs must appear in bonsai_py.__all__.""" + rust_names = _names_from_rust(RUST_NAME_RE) + rust_names = {n for n in rust_names if not n.startswith("_")} + missing = rust_names - set(bt.__all__) + assert not missing, ( + f"Rust declares these #[pyo3(name=...)] symbols but they're missing " + f"from bonsai_py.__all__: {sorted(missing)}." + ) + + +def test_every_rust_pyclass_in_all() -> None: + """Every Rust #[pyclass(name=...)] must appear in bonsai_py.__all__.""" + rust_classes = _names_from_rust(PYCLASS_NAME_RE) + missing = rust_classes - set(bt.__all__) + assert not missing, ( + f"Rust declares these pyclasses but they're missing from __all__: " + f"{sorted(missing)}" + ) + + +def test_no_unexercised_factories() -> None: + """Every name in __all__ is mentioned in at least one other test file — catches added-but-untested symbols.""" + tests_dir = Path(__file__).resolve().parent + test_text = "" + for p in tests_dir.glob("test_*.py"): + if p.name == "test_drift.py": + continue + test_text += p.read_text(encoding="utf-8") + + unexercised = [name for name in bt.__all__ if name not in test_text] + assert not unexercised, ( + f"These public names have no test mentioning them: {unexercised}." + ) diff --git a/bonsai-py/tests/test_module.py b/bonsai-py/tests/test_module.py new file mode 100644 index 0000000..eeab305 --- /dev/null +++ b/bonsai-py/tests/test_module.py @@ -0,0 +1,47 @@ +"""Module-level surface: __version__, __all__, __doc__, RUNNING.""" +from __future__ import annotations + +import bonsai_py as bt + + +def test_version_present() -> None: + """bt.__version__ pins the wheel version (0.12.0); bump per release.""" + assert bt.__version__ == "0.12.0" + + +def test_docstring_present() -> None: + """Module docstring is non-empty and mentions behavior trees.""" + assert bt.__doc__ + assert "behavior" in bt.__doc__.lower() + + +def test_all_contents() -> None: + """__all__ contains exactly the 4 types + 14 factories + RUNNING = 19 names.""" + expected = { + "Status", "ActionArgs", "Behavior", "BT", + "Action", "Wait", "WaitForever", + "Invert", "AlwaysSucceed", + "Sequence", "Select", "WhenAll", "WhenAny", "After", "Race", + "If", "While", "WhileAll", + "RUNNING", + } + assert set(bt.__all__) == expected + + +def test_all_names_are_accessible() -> None: + """Every name listed in __all__ is actually attached to the module.""" + for name in bt.__all__: + assert hasattr(bt, name), f"missing {name}" + + +def test_running_constant() -> None: + """bt.RUNNING is the immutable tuple (Status.Running, 0.0).""" + assert bt.RUNNING == (bt.Status.Running, 0.0) + assert isinstance(bt.RUNNING, tuple) + + +def test_stub_present() -> None: + """The auto-generated .pyi stub ships alongside the wheel.""" + from pathlib import Path + stub = Path(bt.__file__).parent / "__init__.pyi" + assert stub.exists() diff --git a/bonsai-py/tests/test_performance.py b/bonsai-py/tests/test_performance.py new file mode 100644 index 0000000..7f8e03c --- /dev/null +++ b/bonsai-py/tests/test_performance.py @@ -0,0 +1,53 @@ +"""Performance budget -- generous bounds to avoid CI flakiness. Heavy +benchmarks gated behind `@pytest.mark.bench` (run with `pytest -m bench`).""" +from __future__ import annotations + +import time +from typing import Any + +import pytest + +import bonsai_py as bt + + +@pytest.mark.perf +class TestTickBudget: + def test_simple_tick_100_iterations_under_500ms(self) -> None: + """100 BT(tree).tick(cb) round-trips complete under 500 ms — catches order-of-magnitude regressions.""" + + def cb(_a: Any, _b: Any) -> tuple[bt.Status, float]: + return (bt.Status.Success, 0.0) + + # Hoist tree construction; each iter still needs a fresh BT because + # the tree finishes after one tick and tick() returns None thereafter. + tree = bt.Sequence([bt.Action("x")]) + start = time.perf_counter() + for _ in range(100): + bt.BT(tree, None).tick(0.0, cb) + elapsed = time.perf_counter() - start + assert elapsed < 0.5, f"100 ticks took {elapsed*1000:.0f}ms (budget: 500 ms)" + + def test_construction_under_5_seconds_per_1000(self) -> None: + """1000 Sequence([Action, Wait]) constructions fit in 5 seconds — guards against construction-time blowups.""" + start = time.perf_counter() + for _ in range(1000): + bt.Sequence([bt.Action("a"), bt.Wait(0.1)]) + elapsed = time.perf_counter() - start + assert elapsed < 5.0, f"1000 constructions took {elapsed*1000:.0f}ms" + + +@pytest.mark.bench +class TestBenchmarks: + """Microbenchmarks for local profiling. Run with `pytest -m bench`.""" + + def test_bench_tick_throughput(self) -> None: + """Prints achieved ticks/sec for 10_000 BT.tick round-trips — informational, no assertion.""" + import timeit + + def one_tick() -> None: + b = bt.BT(bt.Action("x"), None) + b.tick(0.0, lambda _a, _bb: (bt.Status.Success, 0.0)) + + n = 10_000 + elapsed = timeit.timeit(one_tick, number=n) + print(f"\nbench: {n} ticks in {elapsed:.3f}s = {n/elapsed:.0f} ticks/sec") diff --git a/bonsai-py/tests/test_status.py b/bonsai-py/tests/test_status.py new file mode 100644 index 0000000..3f3e373 --- /dev/null +++ b/bonsai-py/tests/test_status.py @@ -0,0 +1,75 @@ +"""Status enum: semantics, pickle, copy, hash, identity.""" +from __future__ import annotations + +import copy +import pickle + +import pytest + +import bonsai_py as bt + + +class TestStatusSemantics: + def test_three_variants(self) -> None: + """All three Status variants exist and form a non-empty set.""" + assert {bt.Status.Success, bt.Status.Failure, bt.Status.Running} + + @pytest.mark.parametrize( + "variant, expected_int", + [(bt.Status.Success, 0), (bt.Status.Failure, 1), (bt.Status.Running, 2)], + ) + def test_eq_int_discriminant(self, variant: bt.Status, expected_int: int) -> None: + """Discriminants 0/1/2 are locked; reordering is a breaking change.""" + assert variant == expected_int + assert int(variant) == expected_int + + def test_equality(self) -> None: + """Same variant compares equal; different variants compare unequal.""" + assert bt.Status.Success == bt.Status.Success + assert bt.Status.Success != bt.Status.Failure + assert bt.Status.Success != bt.Status.Running + + def test_identity_singleton(self) -> None: + """PyO3 simple enums are singletons; `is` comparison works.""" + assert bt.Status.Success is bt.Status.Success + + def test_hashable_as_dict_key(self) -> None: + """Status implements __hash__ and is usable as a dict key / set member.""" + d = {bt.Status.Success: "ok", bt.Status.Failure: "no"} + assert d[bt.Status.Success] == "ok" + assert d[bt.Status.Failure] == "no" + + def test_repr(self) -> None: + """repr() returns the dotted variant name (Status.Success / Failure / Running).""" + assert repr(bt.Status.Success) == "Status.Success" + assert repr(bt.Status.Failure) == "Status.Failure" + assert repr(bt.Status.Running) == "Status.Running" + + def test_module_attribution(self) -> None: + """`module = "bonsai_py"` is set on the pyclass — required for pickle.""" + assert bt.Status.__module__ == "bonsai_py" + + +class TestStatusPickle: + @pytest.mark.parametrize( + "variant", [bt.Status.Success, bt.Status.Failure, bt.Status.Running] + ) + def test_pickle_preserves_singleton_identity(self, variant: bt.Status) -> None: + """pickle round-trip returns the same singleton (not a copy).""" + roundtripped = pickle.loads(pickle.dumps(variant)) + assert roundtripped is variant + + @pytest.mark.parametrize( + "variant", [bt.Status.Success, bt.Status.Failure, bt.Status.Running] + ) + def test_copy_preserves_singleton_identity(self, variant: bt.Status) -> None: + """copy.copy and copy.deepcopy preserve singleton identity.""" + assert copy.copy(variant) is variant + assert copy.deepcopy(variant) is variant + + def test_dict_with_status_round_trips(self) -> None: + """multiprocessing-style pickle: a dict containing Status survives serialization.""" + data = pickle.dumps({"result": bt.Status.Success, "count": 3}) + out = pickle.loads(data) + assert out["result"] is bt.Status.Success + assert out["count"] == 3 diff --git a/bonsai-py/tests/test_telemetry.py b/bonsai-py/tests/test_telemetry.py new file mode 100644 index 0000000..94d539a --- /dev/null +++ b/bonsai-py/tests/test_telemetry.py @@ -0,0 +1,60 @@ +"""with_telemetry: chainable, bind failures, poisoned state, host parameter.""" +from __future__ import annotations + +from typing import Any + +import pytest + +import bonsai_py as bt + + +class TestWithTelemetry: + def test_chainable(self, free_port: int) -> None: + """with_telemetry returns the same BT instance (PyRefMut self), enabling fluent chaining.""" + b = bt.BT(bt.Action("x"), None) + b_after = b.with_telemetry(free_port) + assert b_after is b + result = b_after.tick(0.0, lambda _a, _bb: (bt.Status.Success, 0.0)) + assert result is not None + + def test_host_parameter(self, free_port: int) -> None: + """The optional `host` kwarg lets the listener bind to a non-loopback interface.""" + b = bt.BT(bt.Action("x"), None).with_telemetry(free_port, host="127.0.0.1") + assert b is not None + + def test_explicit_loopback(self, free_port: int) -> None: + """All-keyword form (port=..., host=...) works for callers that prefer kwargs.""" + b = bt.BT(bt.Action("x"), None).with_telemetry(port=free_port, host="127.0.0.1") + assert b is not None + + def test_bound_port_raises_os_error(self, free_port: int) -> None: + """A second bind on a port held by another BT raises OSError with the bind message.""" + holder = bt.BT(bt.Action("x"), None).with_telemetry(free_port) + assert holder is not None + with pytest.raises(OSError, match="failed"): + bt.BT(bt.Action("y"), None).with_telemetry(free_port) + + def test_unbindable_host_raises_os_error(self) -> None: + """Binding to an RFC-reserved address (240.0.0.0/4) raises OSError without hitting DNS.""" + with pytest.raises(OSError): + bt.BT(bt.Action("x"), None).with_telemetry(0, host="240.0.0.1") + + +class TestPoisonedBT: + def test_failed_with_telemetry_poisons_bt(self, free_port: int) -> None: + """A failed with_telemetry poisons the BT; every subsequent method raises RuntimeError.""" + holder = bt.BT(bt.Action("x"), None).with_telemetry(free_port) + assert holder is not None + + victim = bt.BT(bt.Action("y"), None) + with pytest.raises(OSError): + victim.with_telemetry(free_port) + + with pytest.raises(RuntimeError, match="invalidated"): + victim.tick(0.0, lambda _a, _b: (bt.Status.Success, 0.0)) + + with pytest.raises(RuntimeError, match="invalidated"): + victim.blackboard() + + with pytest.raises(RuntimeError, match="invalidated"): + victim.tick_count() diff --git a/bonsai-py/tests/test_threading_and_pickle.py b/bonsai-py/tests/test_threading_and_pickle.py new file mode 100644 index 0000000..ba46fed --- /dev/null +++ b/bonsai-py/tests/test_threading_and_pickle.py @@ -0,0 +1,61 @@ +"""Cross-process and cross-thread semantics: BT is unsendable + unpicklable.""" +from __future__ import annotations + +import pickle +import threading + +import pytest + +import bonsai_py as bt + + +class TestBTUnsendable: + def test_thread_send_raises_panic(self) -> None: + """PyBT is unsendable; touching it from a different thread raises PyO3's PanicException.""" + b = bt.BT(bt.Action("x"), None) + captured: list[BaseException] = [] + + def worker() -> None: + try: + b.tick(0.0, lambda _a, _b: (bt.Status.Success, 0.0)) + except BaseException as e: # PanicException is BaseException-subclass + captured.append(e) + + t = threading.Thread(target=worker) + t.start() + t.join() + + assert len(captured) == 1 + msg = str(captured[0]) + assert "unsendable" in msg or "thread" in msg.lower() + + +class TestBTUnpicklable: + def test_bt_not_picklable(self) -> None: + """BT instances have no __reduce__ — pickling raises TypeError/PicklingError.""" + b = bt.BT(bt.Action("x"), None) + with pytest.raises((TypeError, pickle.PicklingError)): + pickle.dumps(b) + + +class TestBehaviorUnpicklable: + def test_behavior_not_picklable(self) -> None: + """Behavior nodes are not picklable (no __reduce__ implemented).""" + with pytest.raises((TypeError, pickle.PicklingError)): + pickle.dumps(bt.Action("x")) + + +class TestActionArgsUnpicklable: + def test_action_args_not_picklable(self) -> None: + """ActionArgs instances are not picklable (no __reduce__ implemented).""" + with pytest.raises((TypeError, pickle.PicklingError)): + pickle.dumps(bt.ActionArgs(0.5, "x")) + + +class TestStatusPicklableAcrossProcesses: + """Status IS picklable — the one multiprocessing-friendly type in the binding.""" + + def test_status_round_trip_through_pickle(self) -> None: + """Each Status variant round-trips through pickle and returns the same singleton.""" + for s in (bt.Status.Success, bt.Status.Failure, bt.Status.Running): + assert pickle.loads(pickle.dumps(s)) is s From 38c5322bff4b1dd460701af9cc3e834405265949 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sun, 17 May 2026 01:21:04 -0500 Subject: [PATCH 14/25] IT TOOK ME SO MANY HOURS, BUT IT FINALY WORKS --- bonsai-py/examples/README.md | 71 +++++++++ bonsai-py/examples/__init__.py | 0 bonsai-py/examples/async_drone.py | 202 ++++++++++++++++++++++++ bonsai-py/examples/boids_console.py | 181 +++++++++++++++++++++ bonsai-py/examples/graphviz_demo.py | 68 ++++++++ bonsai-py/examples/race_timeout.py | 112 +++++++++++++ bonsai-py/examples/simple_npc_ai.py | 139 ++++++++++++++++ bonsai-py/examples/visualizer_smoke.py | 118 ++++++++++++++ bonsai-py/python/bonsai_py/__init__.pyi | 1 + bonsai-py/scripts/README.md | 6 +- bonsai-py/src/bt.rs | 4 + bonsai-py/tests/README.md | 6 +- bonsai-py/tests/test_bt.py | 27 ++++ 13 files changed, 933 insertions(+), 2 deletions(-) create mode 100644 bonsai-py/examples/README.md create mode 100644 bonsai-py/examples/__init__.py create mode 100644 bonsai-py/examples/async_drone.py create mode 100644 bonsai-py/examples/boids_console.py create mode 100644 bonsai-py/examples/graphviz_demo.py create mode 100644 bonsai-py/examples/race_timeout.py create mode 100644 bonsai-py/examples/simple_npc_ai.py create mode 100644 bonsai-py/examples/visualizer_smoke.py diff --git a/bonsai-py/examples/README.md b/bonsai-py/examples/README.md new file mode 100644 index 0000000..eae676d --- /dev/null +++ b/bonsai-py/examples/README.md @@ -0,0 +1,71 @@ +# bonsai-py examples + +Pure-Python examples mirroring `examples/` in the Rust workspace. Each example is a single self-contained `.py` file. + +## Prerequisites + +Create and activate a Python venv (one-time), then build & install the extension: + +```bash +# 1. Create a venv (only needed the first time) +python3 -m venv .venv + +# 2. Activate it (every new shell) +source .venv/bin/activate # macOS / Linux / WSL +# .\.venv\Scripts\Activate.ps1 # Windows PowerShell + +# 3. Install build deps + build the extension into the venv +pip install maturin +cd bonsai-py && maturin develop --release && cd .. +``` + +After that, just `source .venv/bin/activate` + `python bonsai-py/examples/.py` in any new shell. + +## Examples (6) + +### [simple_npc_ai.py](simple_npc_ai.py) — console NPC +NPC runs and shoots until action points are exhausted, then rests and dies. Demonstrates `WhileAll`, blackboard mutation via `@dataclass`, structural-`match` callback. + +```bash +python bonsai-py/examples/simple_npc_ai.py +``` + +### [race_timeout.py](race_timeout.py) — `Race` between work and timeout +A simulated long-running job (random 200–1200 ms on a `threading.Thread`) races a 600 ms timeout. The callback polls the work's `queue.Queue` non-blockingly. Demonstrates `Race`, asyncio main loop + threading worker, the unsendable-BT constraint. + +```bash +python bonsai-py/examples/race_timeout.py +``` + +### [graphviz_demo.py](graphviz_demo.py) — tree visualization +Builds an attack-drone tree (mix of plain-string and `@dataclass(frozen=True)` payload actions) and prints the graphviz DOT representation. Paste the output into to render it. + +```bash +python bonsai-py/examples/graphviz_demo.py +python bonsai-py/examples/graphviz_demo.py > tree.dot +``` + +### [visualizer_smoke.py](visualizer_smoke.py) — live web visualizer +Drives a deliberately rich 27-node tree at ~400 ms/tick with a 5-step status rotation and per-leaf phase offset; the browser shows continuous color animation. Demonstrates `BT.with_telemetry(port)`, `reset_bt()`, and every major factory. + +```bash +python bonsai-py/examples/visualizer_smoke.py +``` + +Then open in a browser. `Ctrl-C` to stop. + +### [boids_console.py](boids_console.py) — shared BT across N agents +Builds **one** `Behavior` tree and binds it to 10 independent `BT` instances (each with its own `Boid` dataclass blackboard). Updates positions every tick for 30 frames. Demonstrates the shared-subtree pattern, real-time-loop dt, `WhenAll` for parallel updates. + +```bash +python bonsai-py/examples/boids_console.py +``` + +### [async_drone.py](async_drone.py) — multi-job mission +Drone mission: takeoff → check battery → fly (or fall back to land) → land → repeat. Each long-running step runs on a background thread; the BT polls per-job queues. Prints the tree's `graphviz()` at the start, then runs the mission for ~8 seconds. + +```bash +python bonsai-py/examples/async_drone.py +``` + +Demonstrates `Select` for prioritized fallback, multi-job orchestration via per-job channels, asyncio + threading. \ No newline at end of file diff --git a/bonsai-py/examples/__init__.py b/bonsai-py/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bonsai-py/examples/async_drone.py b/bonsai-py/examples/async_drone.py new file mode 100644 index 0000000..0bce1c6 --- /dev/null +++ b/bonsai-py/examples/async_drone.py @@ -0,0 +1,202 @@ +""" +Async drone mission with multi-job orchestration. + +A drone takes off, checks battery, flies to a mission point (or falls +back to landing at the dock if the battery is low), then lands — +repeating while a background collision-avoidance task runs: + + While( + cond = AvoidOthers, + body = [ + TakeOff, + Select([ + Sequence([CheckBattery, FlyToPoint(10, 10, 10)]), + FlyToPoint(0, 0, 0), # dock fallback + ]), + Land, + ], + ) + +Each long-running action runs on its own `threading.Thread` and reports +status through a per-job `queue.Queue`. The BT polls non-blockingly with +`get_nowait()`. The BT itself stays on the main asyncio loop. The script +prints the tree's `graphviz()` at the start, then runs the mission until +a wall-clock cap. + +Demonstrates `Select` for prioritized fallback, multi-job orchestration +via per-job channels, an asyncio main loop alongside per-job threads, +and `BT.graphviz()` for static tree visualization at startup. + +Run: + python bonsai-py/examples/async_drone.py +""" +from __future__ import annotations + +import asyncio +import enum +import queue +import random +import threading +import time +from dataclasses import dataclass, field +from typing import Any, Optional + +import bonsai_py as bt + +MAX_WALL_SECONDS = 8.0 # Demo cap; the BT itself would loop forever. + + +class DroneAction(enum.Enum): + AVOID_OTHERS = enum.auto() + TAKE_OFF = enum.auto() + LAND = enum.auto() + CHECK_BATTERY = enum.auto() + + +@dataclass(frozen=True) +class FlyToPoint: + x: float + y: float + z: float + + +@dataclass +class DroneState: + avoid_others: Optional[queue.Queue[bt.Status]] = None + takeoff: Optional[queue.Queue[bt.Status]] = None + land: Optional[queue.Queue[bt.Status]] = None + fly_to_point: Optional[queue.Queue[bt.Status]] = None + + +# ---- Background "jobs": one thread per long-running action ----------------- +# Each pushes Status.Running every step, then Status.Success when done. + +def collision_avoidance_task(q: queue.Queue[bt.Status]) -> None: + print("collision avoidance task started") + while True: + try: + q.put(bt.Status.Running, timeout=1.0) + except queue.Full: + return + time.sleep(0.1) + + +def takeoff_task(q: queue.Queue[bt.Status]) -> None: + print("takeoff task started") + for i in range(3): + q.put(bt.Status.Running) + time.sleep(0.3) + print(f"takeoff task running for {(i + 1) * 300} ms") + print("takeoff task finished") + q.put(bt.Status.Success) + + +def landing_task(q: queue.Queue[bt.Status]) -> None: + print("landing task started") + for i in range(3): + q.put(bt.Status.Running) + time.sleep(0.3) + print(f"landing task running for {(i + 1) * 300} ms") + print("landing task finished") + q.put(bt.Status.Success) + + +def fly_to_point_task(point: FlyToPoint, q: queue.Queue[bt.Status]) -> None: + print(f"flying task started: target=({point.x}, {point.y}, {point.z})") + for i in range(3): + q.put(bt.Status.Running) + time.sleep(0.5) + print(f"flying task running for {(i + 1) * 500} ms") + print("flying task finished") + q.put(bt.Status.Success) + + +# ---- Polling helpers ------------------------------------------------------- + +def poll_job( + q_attr: str, + state: DroneState, + spawn: callable, # type: ignore[type-arg] + dt: float, +) -> tuple[bt.Status, float]: + """Generic 'spawn on first call, poll thereafter' pattern for any threaded job.""" + q = getattr(state, q_attr) + if q is None: + q = queue.Queue() + setattr(state, q_attr, q) + threading.Thread(target=spawn, args=(q,), daemon=True).start() + try: + status = q.get_nowait() + except queue.Empty: + return bt.RUNNING + if status == bt.Status.Running: + return bt.RUNNING + setattr(state, q_attr, None) + return (status, dt) + + +def make_callback(state: DroneState, rng: random.Random): + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + action = args.action + if action == DroneAction.AVOID_OTHERS: + return poll_job("avoid_others", state, collision_avoidance_task, args.dt) + if action == DroneAction.TAKE_OFF: + return poll_job("takeoff", state, takeoff_task, args.dt) + if action == DroneAction.LAND: + return poll_job("land", state, landing_task, args.dt) + if action == DroneAction.CHECK_BATTERY: + # Fast sync action: 80% chance OK, 20% chance low -> Select fallback fires. + ok = rng.random() < 0.8 + print(f"check battery: {'OK' if ok else 'LOW'}") + return (bt.Status.Success if ok else bt.Status.Failure, args.dt) + if isinstance(action, FlyToPoint): + spawn = lambda q, p=action: fly_to_point_task(p, q) + return poll_job("fly_to_point", state, spawn, args.dt) + raise ValueError(f"unknown action: {action!r}") + + return cb + + +def build_tree() -> bt.Behavior: + fly_if_healthy = bt.Sequence([ + bt.Action(DroneAction.CHECK_BATTERY), + bt.Action(FlyToPoint(10.0, 10.0, 10.0)), + ]) + fly_to_dock = bt.Action(FlyToPoint(0.0, 0.0, 0.0)) + mission_with_fallback = bt.Select([fly_if_healthy, fly_to_dock]) + return bt.While( + bt.Action(DroneAction.AVOID_OTHERS), + [ + bt.Action(DroneAction.TAKE_OFF), + mission_with_fallback, + bt.Action(DroneAction.LAND), + ], + ) + + +async def main() -> None: + tree_bt = bt.BT(build_tree(), None) + state = DroneState() + rng = random.Random(0) + callback = make_callback(state, rng) + print("=== drone tree ===") + print(tree_bt.graphviz()) + print("=== mission start ===") + + start = time.perf_counter() + last = start + while True: + await asyncio.sleep(0.5) + now = time.perf_counter() + dt = now - last + last = now + result = tree_bt.tick(dt, callback) + if result is None: + break + if now - start > MAX_WALL_SECONDS: + print(f"=== demo cap reached ({MAX_WALL_SECONDS}s) — exiting ===") + break + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/bonsai-py/examples/boids_console.py b/bonsai-py/examples/boids_console.py new file mode 100644 index 0000000..56592cd --- /dev/null +++ b/bonsai-py/examples/boids_console.py @@ -0,0 +1,181 @@ +""" +Boids flocking with one Behavior shared across many agents. + +Build the `Behavior` ONCE, then construct `bt.BT(shared_tree, boid)` for +each of N agents — every BT instance has its own blackboard (a `Boid` +dataclass with `x, y, dx, dy`) but they all share the same tree +definition. Each tick runs all 5 flocking rules: + + While( + cond = WhenAll([FlyTowardsCenter, AvoidOthers]), + body = [MatchVelocity, LimitSpeed, KeepWithinBounds], + ) + +Both cond actions return `Running`, so the BT stays in the body each +tick and all 5 actions fire — updating each boid's velocity and +position. Output is text-only (no graphics window); first and last +boids are logged each tick. + +Demonstrates a shared `Behavior` across many `BT` instances, +real-time-loop `dt` integration, `WhenAll` for parallel cond updates, +and `While`-body re-execution per tick. + +Run: + python bonsai-py/examples/boids_console.py +""" +from __future__ import annotations + +import enum +import math +import random +import time +from dataclasses import dataclass +from typing import Any + +import bonsai_py as bt + +NUM_BOIDS = 10 +WIDTH = 1280.0 +HEIGHT = 720.0 +SPEED_LIMIT = 400.0 +VISUAL_RANGE = 32.0 +MIN_DISTANCE = 16.0 +TICKS = 30 +DT_SECONDS = 0.1 + + +class Action(enum.Enum): + AVOID_OTHERS = enum.auto() + FLY_TOWARDS_CENTER = enum.auto() + MATCH_VELOCITY = enum.auto() + LIMIT_SPEED = enum.auto() + KEEP_WITHIN_BOUNDS = enum.auto() + + +@dataclass +class Boid: + x: float + y: float + dx: float + dy: float + + def distance(self, other: Boid) -> float: + return math.hypot(self.x - other.x, self.y - other.y) + + +def build_tree() -> bt.Behavior: + """One Behavior, shared across all N boid BTs (matches the Rust pattern).""" + avoid_and_fly = bt.WhenAll([ + bt.Action(Action.FLY_TOWARDS_CENTER), + bt.Action(Action.AVOID_OTHERS), + ]) + return bt.While( + avoid_and_fly, + [ + bt.Action(Action.MATCH_VELOCITY), + bt.Action(Action.LIMIT_SPEED), + bt.Action(Action.KEEP_WITHIN_BOUNDS), + ], + ) + + +def make_callback(idx: int, all_boids: list[Boid]): + """Build a callback closed over this boid's neighbors (via `all_boids`).""" + + def cb(args: Any, boid: Boid) -> tuple[bt.Status, float]: + others = [b for j, b in enumerate(all_boids) if j != idx] + match args.action: + case Action.AVOID_OTHERS: + move_x = move_y = 0.0 + for other in others: + dist = boid.distance(other) + if 0.0 < dist < MIN_DISTANCE: + move_x += boid.x - other.x + move_y += boid.y - other.y + boid.dx += move_x * 0.5 + boid.dy += move_y * 0.5 + return bt.RUNNING + case Action.FLY_TOWARDS_CENTER: + cx = cy = 0.0 + n = 0 + for other in others: + if boid.distance(other) < VISUAL_RANGE: + cx += other.x + cy += other.y + n += 1 + if n > 0: + boid.dx += (cx / n - boid.x) * 0.05 + boid.dy += (cy / n - boid.y) * 0.05 + return bt.RUNNING + case Action.MATCH_VELOCITY: + avg_dx = avg_dy = 0.0 + n = 0 + for other in others: + if boid.distance(other) < VISUAL_RANGE: + avg_dx += other.dx + avg_dy += other.dy + n += 1 + if n > 0: + boid.dx += (avg_dx / n - boid.dx) * 0.1 + boid.dy += (avg_dy / n - boid.dy) * 0.1 + return (bt.Status.Success, args.dt) + case Action.LIMIT_SPEED: + speed = math.hypot(boid.dx, boid.dy) + if speed > SPEED_LIMIT: + boid.dx = boid.dx / speed * SPEED_LIMIT + boid.dy = boid.dy / speed * SPEED_LIMIT + return (bt.Status.Success, args.dt) + case Action.KEEP_WITHIN_BOUNDS: + edge = 40.0 + turn = 16.0 + if boid.x < edge: + boid.dx += turn + if boid.x > WIDTH - edge: + boid.dx -= turn + if boid.y < edge: + boid.dy += turn + if boid.y > HEIGHT - edge: + boid.dy -= turn + return bt.RUNNING + case _: + raise ValueError(f"unknown action: {args.action!r}") + + return cb + + +def main() -> None: + rng = random.Random(0) # deterministic for reproducible console output + boids = [ + Boid( + x=rng.uniform(WIDTH / 4, 3 * WIDTH / 4), + y=rng.uniform(HEIGHT / 4, 3 * HEIGHT / 4), + dx=(rng.random() - 0.5) * SPEED_LIMIT, + dy=(rng.random() - 0.5) * SPEED_LIMIT, + ) + for _ in range(NUM_BOIDS) + ] + + shared_tree = build_tree() + bts = [bt.BT(shared_tree, boids[i]) for i in range(NUM_BOIDS)] + + print(f"Boids console demo: {NUM_BOIDS} agents sharing one Behavior tree.") + for step in range(TICKS): + for i, tree_bt in enumerate(bts): + tree_bt.tick(DT_SECONDS, make_callback(i, boids)) + boid = boids[i] + boid.x += boid.dx * DT_SECONDS + boid.y += boid.dy * DT_SECONDS + if i == 0 or i == NUM_BOIDS - 1: + # Log first + last boid only; full log would be 300 lines. + print( + f"[boid {i:2d}] step {step:2d}" + f" pos=({boid.x:7.1f}, {boid.y:7.1f})" + f" vel=({boid.dx:7.1f}, {boid.dy:7.1f})" + ) + time.sleep(DT_SECONDS / 10.0) # tiny pause so output is readable + + print(f"Done after {TICKS} ticks. Each BT instance ticked {bts[0].tick_count()} times.") + + +if __name__ == "__main__": + main() diff --git a/bonsai-py/examples/graphviz_demo.py b/bonsai-py/examples/graphviz_demo.py new file mode 100644 index 0000000..f7fa828 --- /dev/null +++ b/bonsai-py/examples/graphviz_demo.py @@ -0,0 +1,68 @@ +""" +Print the graphviz DOT representation of an attack-drone behavior tree. + +Builds an attack-drone tree (circle the target, attack when in range, give +up when too far). Calls `BT.graphviz()` to emit +a DOT string. Paste the output into +to render the tree visually. + +Demonstrates `BT.graphviz()`, and composition with `While` / `Sequence` / `WhenAny` / +`Wait` / `WaitForever` / `Action`. + +Run: + python bonsai-py/examples/graphviz_demo.py +""" +from __future__ import annotations + +from dataclasses import dataclass + +import bonsai_py as bt + +# Payload-less actions are plain strings; payload variants are frozen +# dataclasses (hashable, immutable, work as bt.Action(...) values). +CIRCLING = "Circling" +FLY_TOWARD_PLAYER = "FlyTowardPlayer" + + +@dataclass(frozen=True) +class PlayerWithinDistance: + distance: float + + +@dataclass(frozen=True) +class PlayerFarAwayFromTarget: + distance: float + + +@dataclass(frozen=True) +class AttackPlayer: + damage: float + + +def build_tree() -> bt.Behavior: + circling = bt.Action(CIRCLING) + circle_until_player_within_distance = bt.Sequence([ + bt.While(bt.Wait(5.0), [circling]), + bt.While(bt.Action(PlayerWithinDistance(50.0)), [circling]), + ]) + give_up_or_attack = bt.WhenAny([ + bt.Action(PlayerFarAwayFromTarget(100.0)), + bt.Sequence([ + bt.Action(PlayerWithinDistance(10.0)), + bt.Action(AttackPlayer(0.1)), + ]), + ]) + attack_attempt = bt.While(give_up_or_attack, [bt.Action(FLY_TOWARD_PLAYER)]) + return bt.While( + bt.WaitForever(), + [circle_until_player_within_distance, attack_attempt], + ) + + +def main() -> None: + tree_bt = bt.BT(build_tree(), {}) + print(tree_bt.graphviz()) + + +if __name__ == "__main__": + main() diff --git a/bonsai-py/examples/race_timeout.py b/bonsai-py/examples/race_timeout.py new file mode 100644 index 0000000..c3ed775 --- /dev/null +++ b/bonsai-py/examples/race_timeout.py @@ -0,0 +1,112 @@ +""" +Race a simulated long-running job against a hard timeout. + +A `Race` runs two arms in parallel; whichever finishes first wins: + + Race([ + DO_WORK, # random 200-1200 ms + Sequence([Wait(0.6), ON_TIMEOUT]), # 600 ms hard deadline + ]) + +`DO_WORK` runs on a background `threading.Thread` and reports its status +through a `queue.Queue`. The BT polls non-blockingly with `get_nowait()`; +an empty queue means "still running, try again next tick." + +Demonstrates `Race` for timeouts, an asyncio main loop alongside a +threading worker (the `BT` itself stays on the main thread — it is +unsendable), the `bt.RUNNING` shorthand, and `time.perf_counter()` for +monotonic dt. + +Run: + python bonsai-py/examples/race_timeout.py +""" +from __future__ import annotations + +import asyncio +import enum +import queue +import random +import threading +import time +from dataclasses import dataclass, field +from typing import Any + +import bonsai_py as bt + + +class MissionAction(enum.Enum): + DO_WORK = enum.auto() + ON_TIMEOUT = enum.auto() + + +@dataclass +class MissionState: + work: queue.Queue[bt.Status] | None = field(default=None) + + +def do_work_task(q: queue.Queue[bt.Status]) -> None: + """Background thread: sleeps in 100ms steps, sends Status.Running each step, + then Status.Success after a random 200..=1200ms total.""" + work_ms = random.randint(200, 1200) + print(f"[do_work] started; planned duration {work_ms} ms") + elapsed = 0 + step_ms = 100 + while elapsed < work_ms: + q.put(bt.Status.Running) + time.sleep(step_ms / 1000.0) + elapsed += step_ms + print(f"[do_work] finished after {elapsed} ms") + q.put(bt.Status.Success) + + +def make_callback(state: MissionState): + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + match args.action: + case MissionAction.DO_WORK: + if state.work is None: + state.work = queue.Queue() + threading.Thread( + target=do_work_task, args=(state.work,), daemon=True + ).start() + try: + status = state.work.get_nowait() + except queue.Empty: + return bt.RUNNING + if status == bt.Status.Running: + return bt.RUNNING + state.work = None + return (status, args.dt) + case MissionAction.ON_TIMEOUT: + print("do_work timed out!") + return (bt.Status.Failure, args.dt) + case _: + raise ValueError(f"unknown action: {args.action!r}") + + return cb + + +async def main() -> None: + timeout_s = 0.6 + tree = bt.Sequence([ + bt.Race([ + bt.Action(MissionAction.DO_WORK), + bt.Sequence([bt.Wait(timeout_s), bt.Action(MissionAction.ON_TIMEOUT)]), + ]), + ]) + tree_bt = bt.BT(tree, None) + state = MissionState() + callback = make_callback(state) + last = time.perf_counter() + + while True: + await asyncio.sleep(0.05) + now = time.perf_counter() + dt = now - last + last = now + result = tree_bt.tick(dt, callback) + if result is None: + break + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/bonsai-py/examples/simple_npc_ai.py b/bonsai-py/examples/simple_npc_ai.py new file mode 100644 index 0000000..fc0deab --- /dev/null +++ b/bonsai-py/examples/simple_npc_ai.py @@ -0,0 +1,139 @@ +""" +Console NPC behavior demo. + +An NPC runs and shoots while it has action points. When exhausted, it rests +until fully recovered, then dies. Built from a nested `WhileAll`: + + WhileAll(IsDead, [ + WhileAll(HasActionPointsLeft, [Run, Shoot]), + Rest, + Die, + ]) + +Demonstrates `WhileAll` looping, blackboard mutation through a `@dataclass`, +and an enum action dispatched via Python's structural-match callback. + +Run: + python bonsai-py/examples/simple_npc_ai.py +""" +from __future__ import annotations + +import enum +from dataclasses import dataclass +from typing import Any + +import bonsai_py as bt + + +class EnemyNPC(enum.Enum): + RUN = enum.auto() + SHOOT = enum.auto() + HAS_ACTION_POINTS_LEFT = enum.auto() + REST = enum.auto() + DIE = enum.auto() + IS_DEAD = enum.auto() + + +@dataclass +class BlackBoard: + times_shot: int = 0 + + +@dataclass +class NPCState: + action_points: int + max_action_points: int + alive: bool + + def consume_action_point(self) -> None: + self.action_points = max(0, self.action_points - 1) + + def rest(self) -> None: + self.action_points = min(self.action_points + 1, self.max_action_points) + print(f"Rested for a while... Action points: {self.action_points}") + + def die(self) -> None: + print("NPC died...") + self.alive = False + + def is_alive(self) -> bool: + print("NPC is alive..." if self.alive else "NPC is dead...") + return self.alive + + def fully_rested(self) -> bool: + return self.action_points == self.max_action_points + + def perform_action(self, action: str) -> None: + if self.action_points > 0: + self.consume_action_point() + print(f"Performing action: {action}. Action points: {self.action_points}") + else: + print(f"Cannot perform action: {action}. Not enough action points.") + + +def make_callback(state: NPCState): + def cb(args: Any, blackboard: BlackBoard) -> tuple[bt.Status, float]: + match args.action: + case EnemyNPC.RUN: + state.perform_action("run") + return (bt.Status.Success, 0.0) + case EnemyNPC.HAS_ACTION_POINTS_LEFT: + if state.action_points == 0: + print("NPC does not have action points left...") + return (bt.Status.Success, 0.0) + print(f"NPC has action points: {state.action_points}") + return (bt.Status.Running, 0.0) + case EnemyNPC.SHOOT: + state.perform_action("shoot") + blackboard.times_shot += 1 + return (bt.Status.Success, 0.0) + case EnemyNPC.REST: + if state.fully_rested(): + return (bt.Status.Success, 0.0) + state.rest() + return (bt.Status.Running, 0.0) + case EnemyNPC.DIE: + state.die() + return (bt.Status.Success, 0.0) + case EnemyNPC.IS_DEAD: + if state.is_alive(): + return (bt.Status.Running, 0.0) + return (bt.Status.Success, 0.0) + case _: + raise ValueError(f"unknown action: {args.action!r}") + + return cb + + +def build_tree() -> bt.Behavior: + run_and_shoot = bt.WhileAll( + bt.Action(EnemyNPC.HAS_ACTION_POINTS_LEFT), + [bt.Action(EnemyNPC.RUN), bt.Action(EnemyNPC.SHOOT)], + ) + return bt.WhileAll( + bt.Action(EnemyNPC.IS_DEAD), + [run_and_shoot, bt.Action(EnemyNPC.REST), bt.Action(EnemyNPC.DIE)], + ) + + +def main() -> None: + max_actions = 3 + blackboard = BlackBoard() + state = NPCState(action_points=max_actions, max_action_points=max_actions, alive=True) + tree_bt = bt.BT(build_tree(), blackboard) + callback = make_callback(state) + + while True: + print("reached main loop...") + result = tree_bt.tick(0.0, callback) + if result is None: + break + status, _ = result + if status != bt.Status.Running: + break + + print(f"NPC shot {blackboard.times_shot} times during the simulation.") + + +if __name__ == "__main__": + main() diff --git a/bonsai-py/examples/visualizer_smoke.py b/bonsai-py/examples/visualizer_smoke.py new file mode 100644 index 0000000..8b8ae32 --- /dev/null +++ b/bonsai-py/examples/visualizer_smoke.py @@ -0,0 +1,118 @@ +""" +End-to-end demo for the WebSocket visualizer. + +Drives a deliberately rich 27-node tree (covering 12 of the 14 Behavior +factories), attaches the visualizer via `BT.with_telemetry(8910)`, and +re-runs the tree every ~400 ms wall tick. Each leaf's status follows a +5-step rotation with a per-action phase offset, so a varied mix of +green / yellow / red is visible at any moment. After each complete run, +`reset_bt()` rewinds the cursor; `tick_count` and the telemetry +connection survive, so the browser sees a continuous TickTrace stream +with monotonic `tick_id`. + +Demonstrates `with_telemetry`, `reset_bt`, every major factory in one +tree, and a deterministic-cycle callback contract. + +Run: + python bonsai-py/examples/visualizer_smoke.py + +Then open in a browser. + 1. Tree renders within ~1 s; status bar reads `connected` and `27 nodes`. + 2. Every ~400 ms leaf colors shift across all subtrees. + 3. `Ctrl-C` and restart -> browser reconnects within <=1 s. + +Port 8910 must be free; if it is busy, the script raises OSError. +""" +from __future__ import annotations + +import time +from typing import Any + +import bonsai_py as bt + + +def build_tree() -> bt.Behavior: + return bt.Sequence([ + bt.If( + bt.Action("low_hp"), + bt.AlwaysSucceed(bt.Action("flee")), + bt.Action("regroup"), + ), + bt.Select([ + bt.Sequence([ + bt.Action("acquire_target"), + bt.WhenAll([bt.Action("aim"), bt.Action("track")]), + ]), + bt.Race([bt.Action("dodge"), bt.Wait(2.0)]), + bt.Invert(bt.Action("enemy_visible")), + ]), + bt.While(bt.Action("has_ammo"), [bt.Action("fire"), bt.Wait(0.3)]), + bt.After([bt.Action("cooldown"), bt.Action("ready_signal")]), + bt.WhenAny([bt.Action("victory_check"), bt.Action("retreat_signal")]), + ]) + + +# Five-step status cycle visible across all three colors. Each action has a +# unique phase offset so the same wall tick produces a varied mix of statuses +# across the tree (and yellow-Running shows up). +CYCLE = ( + bt.Status.Success, + bt.Status.Running, + bt.Status.Failure, + bt.Status.Success, + bt.Status.Running, +) + +PHASE_OFFSET = { + "low_hp": 0, + "flee": 1, + "regroup": 2, + "acquire_target": 3, + "aim": 4, + "track": 0, + "dodge": 1, + "enemy_visible": 2, + "has_ammo": 3, + "fire": 4, + "cooldown": 0, + "ready_signal": 1, + "victory_check": 2, + "retreat_signal": 3, +} + +# Four leaves whose Failure would short-circuit the root Sequence before +# downstream subtrees ever render. Substitute Running for Failure on these so +# the chain reaches the bottom branches. They still show Success and Running. +KEEP_ALIVE = {"regroup", "has_ammo", "cooldown", "ready_signal"} + + +def make_callback(tick_n_ref: list[int]): + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + phase = PHASE_OFFSET.get(args.action, 0) + idx = (tick_n_ref[0] + phase) % len(CYCLE) + status = CYCLE[idx] + if args.action in KEEP_ALIVE and status == bt.Status.Failure: + status = bt.Status.Running + return (status, 0.0) + + return cb + + +def main() -> None: + tree_bt = bt.BT(build_tree(), None).with_telemetry(8910) + tick_n_ref = [0] + callback = make_callback(tick_n_ref) + print("bonsai-bt visualizer: open http://127.0.0.1:8910/") + + while True: + tick_n_ref[0] += 1 + result = tree_bt.tick(1.0, callback) + if result is not None: + status, _ = result + if status in (bt.Status.Success, bt.Status.Failure): + tree_bt.reset_bt() + time.sleep(0.4) + + +if __name__ == "__main__": + main() diff --git a/bonsai-py/python/bonsai_py/__init__.pyi b/bonsai-py/python/bonsai_py/__init__.pyi index b5d1b63..8e0bc77 100644 --- a/bonsai-py/python/bonsai_py/__init__.pyi +++ b/bonsai-py/python/bonsai_py/__init__.pyi @@ -62,6 +62,7 @@ class BT: def reset_bt(self) -> None: ... def tick_count(self) -> builtins.int: ... def is_finished(self) -> builtins.bool: ... + def graphviz(self) -> builtins.str: ... def with_telemetry(self, port: builtins.int, host: builtins.str = '127.0.0.1') -> BT: ... @typing.final diff --git a/bonsai-py/scripts/README.md b/bonsai-py/scripts/README.md index d8135c9..bdd1135 100644 --- a/bonsai-py/scripts/README.md +++ b/bonsai-py/scripts/README.md @@ -9,9 +9,13 @@ Developer scripts for working on the `bonsai_py` extension module. | [regen-stubs.sh](regen-stubs.sh) | Regenerates `python/bonsai_py/__init__.pyi` from the `#[gen_stub_*]` annotations on the Rust side. Run after editing any annotated `#[pyclass]` / `#[pyfunction]` / `#[pymethods]`. Also runs automatically via the `regen-stubs` pre-commit hook and is enforced in CI. | | [verify.py](verify.py) | End-to-end smoke test of the installed wheel: imports `bonsai_py`, exercises every public API (Status, ActionArgs, all 14 factories, BT tick/telemetry, type stub presence, mypy `--strict` on a sample script). Standalone — not a pytest suite (the long-form behavior coverage lives in [../tests/](../tests/)). | +## Prerequisites + +A Python venv with the `bonsai_py` extension built in. See [../README.md](../README.md#installation-dev) for the one-time setup (`python -m venv .venv`, activate, `pip install maturin`, `maturin develop --release`). + ## Running -From the repository root, with a venv that has the wheel installed (`maturin develop --release` from `bonsai-py/`): +From the repository root, with the venv activated: ```bash # Regenerate stubs after editing Rust annotations diff --git a/bonsai-py/src/bt.rs b/bonsai-py/src/bt.rs index d5e29a5..3fa1e32 100644 --- a/bonsai-py/src/bt.rs +++ b/bonsai-py/src/bt.rs @@ -97,6 +97,10 @@ impl PyBT { Ok(self.require_inner()?.is_finished()) } + fn graphviz(&mut self) -> PyResult { + Ok(self.require_inner_mut()?.get_graphviz()) + } + #[pyo3(signature = (port, host = "127.0.0.1"))] fn with_telemetry<'py>( mut slf: PyRefMut<'py, Self>, diff --git a/bonsai-py/tests/README.md b/bonsai-py/tests/README.md index 61cfd15..bd87ba4 100644 --- a/bonsai-py/tests/README.md +++ b/bonsai-py/tests/README.md @@ -17,9 +17,13 @@ Pytest suite for the `bonsai_py` extension module. Primarily used to prevent any | [test_performance.py](test_performance.py) | `@pytest.mark.perf`: 100 ticks under 500 ms, 1000 constructions under 5 s. `@pytest.mark.bench` (opt-in): tick throughput microbenchmark. | | [test_drift.py](test_drift.py) | Checks that every Rust `#[pyo3(name=...)]` symbol appears in `__all__`; every `#[pyclass(name=..., module='bonsai_py')]` appears in `__all__`; every name in `__all__` is mentioned in at least one other test file. | +## Prerequisites + +A Python venv with the `bonsai_py` extension built in. See [../README.md](../README.md#installation-dev) for the one-time setup (`python -m venv .venv`, activate, `pip install maturin`, `maturin develop --release`). + ## Running -From the repository root, with the wheel installed in your venv (`maturin develop --release` from `bonsai-py/`): +From the repository root, with the venv activated: ```bash # Full suite (default — runs perf budget tests, skips benchmarks) diff --git a/bonsai-py/tests/test_bt.py b/bonsai-py/tests/test_bt.py index 5104a2a..beba26f 100644 --- a/bonsai-py/tests/test_bt.py +++ b/bonsai-py/tests/test_bt.py @@ -227,3 +227,30 @@ def keep_running(_a: Any, _b: Any) -> tuple[bt.Status, float]: status, _ = result assert status == bt.Status.Running assert not b.is_finished() + + +class TestGraphviz: + def test_returns_dot_string(self) -> None: + """BT.graphviz() returns a non-empty graphviz DOT string.""" + b = bt.BT(bt.Sequence([bt.Action("a"), bt.Action("b")]), None) + dot = b.graphviz() + assert isinstance(dot, str) + assert "digraph" in dot.lower() or "graph" in dot.lower() + + def test_graphviz_idempotent_around_tick(self) -> None: + """graphviz() can be called before or after ticks — tree shape is invariant.""" + b = bt.BT(bt.Action("x"), None) + before = b.graphviz() + b.tick(0.0, lambda _a, _bb: (bt.Status.Success, 0.0)) + after = b.graphviz() + assert before == after + + def test_graphviz_on_poisoned_bt_raises(self, free_port: int) -> None: + """graphviz() on a poisoned BT raises RuntimeError (consistent with other methods).""" + holder = bt.BT(bt.Action("x"), None).with_telemetry(free_port) + assert holder is not None + victim = bt.BT(bt.Action("y"), None) + with pytest.raises(OSError): + victim.with_telemetry(free_port) + with pytest.raises(RuntimeError, match="invalidated"): + victim.graphviz() From 2ddc0c36766d3192abe9ecc93a493759ff62ba5b Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sun, 17 May 2026 11:49:23 -0500 Subject: [PATCH 15/25] generate python wheels action --- .github/workflows/python-wheels.yml | 171 ++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 .github/workflows/python-wheels.yml diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml new file mode 100644 index 0000000..a263dfd --- /dev/null +++ b/.github/workflows/python-wheels.yml @@ -0,0 +1,171 @@ +name: Python wheels + +on: + push: + branches: [main] + tags: ["py-v*", "py-test-*"] + pull_request: + branches: [main] + workflow_dispatch: # manual ad-hoc builds from any branch + +concurrency: + # Tag pushes get their own group so publishes never get cancelled. + group: >- + ${{ github.workflow }}-${{ github.ref }}-${{ startsWith(github.ref, 'refs/tags/') && 'publish' || 'branch' }} + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: Build wheel (${{ matrix.target.label }}) + runs-on: ${{ matrix.target.runner }} + strategy: + fail-fast: false + matrix: + target: + - label: linux-x86_64 + runner: ubuntu-latest + args: --release --manylinux 2_28 + - label: macos-universal2 + runner: macos-latest + args: --release --target universal2-apple-darwin + - label: windows-x86_64 + runner: windows-latest + args: --release + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.10" # abi3 — any 3.10+ works for building + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target.label == 'macos-universal2' && 'x86_64-apple-darwin,aarch64-apple-darwin' || '' }} + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: bonsai-py + key: ${{ matrix.target.label }} + + - name: Tag/version guard (tag pushes only) + if: startsWith(github.ref, 'refs/tags/') + shell: bash + run: | + tag="${GITHUB_REF#refs/tags/}" + version="${tag#py-v}" + version="${version#py-test-}" + cargo_version=$(grep -m1 '^version' bonsai-py/Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/') + if [ "$version" != "$cargo_version" ]; then + echo "::error::Tag version '$version' does not match Cargo.toml version '$cargo_version'." + exit 1 + fi + + - uses: PyO3/maturin-action@v1 + with: + working-directory: bonsai-py + command: build + args: ${{ matrix.target.args }} --out dist --strip + + - name: Verify wheel (Linux/macOS only — Windows venv quirks) + if: matrix.target.runner != 'windows-latest' + shell: bash + run: | + python -m venv .venv-test + source .venv-test/bin/activate + pip install --upgrade pip + pip install pytest pytest-timeout mypy + pip install bonsai-py/dist/*.whl + pytest bonsai-py/tests/ + python bonsai-py/scripts/verify.py + + - name: Wheel size sanity (Linux/macOS only) + if: matrix.target.runner != 'windows-latest' + shell: bash + run: | + size=$(stat -c%s bonsai-py/dist/*.whl 2>/dev/null || stat -f%z bonsai-py/dist/*.whl) + ceiling=$((5 * 1024 * 1024)) + if [ "$size" -gt "$ceiling" ]; then + echo "::error::Wheel size $size exceeds 5MB ceiling." + exit 1 + fi + echo "Wheel size: $size bytes (under 5MB ceiling)." + + - uses: actions/upload-artifact@v4 + with: + name: wheel-${{ matrix.target.label }} + path: bonsai-py/dist/*.whl + retention-days: ${{ startsWith(github.ref, 'refs/tags/') && 90 || 14 }} + + sdist: + name: Build sdist + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - uses: PyO3/maturin-action@v1 + with: + working-directory: bonsai-py + command: sdist + args: --out dist + - uses: actions/upload-artifact@v4 + with: + name: sdist + path: bonsai-py/dist/*.tar.gz + retention-days: ${{ startsWith(github.ref, 'refs/tags/') && 90 || 14 }} + + verify-sdist: + name: Verify sdist builds from source + needs: sdist + runs-on: ubuntu-latest + steps: + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - uses: dtolnay/rust-toolchain@stable + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + - name: Install + smoke-test from sdist + run: | + python -m venv .venv-sdist + source .venv-sdist/bin/activate + pip install --upgrade pip + pip install --no-binary :all: dist/*.tar.gz + python -c "import bonsai_py; print(bonsai_py.__version__)" + + publish: + name: Publish to PyPI / TestPyPI + needs: [build, sdist, verify-sdist] + if: startsWith(github.ref, 'refs/tags/py-v') || startsWith(github.ref, 'refs/tags/py-test-') + runs-on: ubuntu-latest + environment: + name: ${{ startsWith(github.ref, 'refs/tags/py-test-') && 'testpypi' || 'pypi' }} + permissions: + id-token: write # OIDC for Trusted Publishing + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + pattern: wheel-* + merge-multiple: true + + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + + - name: List artifacts to publish + run: ls -la dist/ + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: >- + ${{ startsWith(github.ref, 'refs/tags/py-test-') && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }} + packages-dir: dist + skip-existing: true From 81e550535a412492d76d06070ab67e6c742c8ded Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sun, 17 May 2026 12:52:36 -0500 Subject: [PATCH 16/25] pipeline fixes --- .pre-commit-config.yaml | 1 + bonsai-py/examples/README.md | 2 +- bonsai-py/examples/async_drone.py | 5 ++- bonsai-py/examples/boids_console.py | 1 + bonsai-py/examples/graphviz_demo.py | 1 + bonsai-py/examples/race_timeout.py | 1 + bonsai-py/examples/simple_npc_ai.py | 1 + bonsai-py/examples/visualizer_smoke.py | 1 + bonsai-py/python/bonsai_py/__init__.py | 1 + bonsai-py/scripts/verify.py | 1 + bonsai-py/src/action_args.rs | 4 +- bonsai-py/src/behavior.rs | 12 ++---- bonsai-py/src/bt.rs | 45 +++++++------------- bonsai-py/src/lib.rs | 4 +- bonsai-py/src/status.rs | 9 ++-- bonsai-py/tests/conftest.py | 1 + bonsai-py/tests/test_action_args.py | 1 + bonsai-py/tests/test_behavior.py | 1 + bonsai-py/tests/test_bt.py | 1 + bonsai-py/tests/test_drift.py | 1 + bonsai-py/tests/test_module.py | 1 + bonsai-py/tests/test_performance.py | 1 + bonsai-py/tests/test_status.py | 1 + bonsai-py/tests/test_telemetry.py | 1 + bonsai-py/tests/test_threading_and_pickle.py | 1 + 25 files changed, 51 insertions(+), 48 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f53bb17..6d7aa46 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,6 +33,7 @@ repos: - id: forbid-new-submodules - id: mixed-line-ending - id: name-tests-test + args: [--pytest-test-first] - id: requirements-txt-fixer - id: sort-simple-yaml - id: trailing-whitespace diff --git a/bonsai-py/examples/README.md b/bonsai-py/examples/README.md index eae676d..67125fd 100644 --- a/bonsai-py/examples/README.md +++ b/bonsai-py/examples/README.md @@ -68,4 +68,4 @@ Drone mission: takeoff → check battery → fly (or fall back to land) → land python bonsai-py/examples/async_drone.py ``` -Demonstrates `Select` for prioritized fallback, multi-job orchestration via per-job channels, asyncio + threading. \ No newline at end of file +Demonstrates `Select` for prioritized fallback, multi-job orchestration via per-job channels, asyncio + threading. diff --git a/bonsai-py/examples/async_drone.py b/bonsai-py/examples/async_drone.py index 0bce1c6..d43825c 100644 --- a/bonsai-py/examples/async_drone.py +++ b/bonsai-py/examples/async_drone.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Async drone mission with multi-job orchestration. @@ -19,8 +20,8 @@ Each long-running action runs on its own `threading.Thread` and reports status through a per-job `queue.Queue`. The BT polls non-blockingly with -`get_nowait()`. The BT itself stays on the main asyncio loop. The script -prints the tree's `graphviz()` at the start, then runs the mission until +`get_nowait()`. The BT itself stays on the main asyncio loop. The script +prints the tree's `graphviz()` at the start, then runs the mission until a wall-clock cap. Demonstrates `Select` for prioritized fallback, multi-job orchestration diff --git a/bonsai-py/examples/boids_console.py b/bonsai-py/examples/boids_console.py index 56592cd..ab3adc0 100644 --- a/bonsai-py/examples/boids_console.py +++ b/bonsai-py/examples/boids_console.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Boids flocking with one Behavior shared across many agents. diff --git a/bonsai-py/examples/graphviz_demo.py b/bonsai-py/examples/graphviz_demo.py index f7fa828..ba6d9e5 100644 --- a/bonsai-py/examples/graphviz_demo.py +++ b/bonsai-py/examples/graphviz_demo.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Print the graphviz DOT representation of an attack-drone behavior tree. diff --git a/bonsai-py/examples/race_timeout.py b/bonsai-py/examples/race_timeout.py index c3ed775..ab156f9 100644 --- a/bonsai-py/examples/race_timeout.py +++ b/bonsai-py/examples/race_timeout.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Race a simulated long-running job against a hard timeout. diff --git a/bonsai-py/examples/simple_npc_ai.py b/bonsai-py/examples/simple_npc_ai.py index fc0deab..2cd13d9 100644 --- a/bonsai-py/examples/simple_npc_ai.py +++ b/bonsai-py/examples/simple_npc_ai.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Console NPC behavior demo. diff --git a/bonsai-py/examples/visualizer_smoke.py b/bonsai-py/examples/visualizer_smoke.py index 8b8ae32..827dbb0 100644 --- a/bonsai-py/examples/visualizer_smoke.py +++ b/bonsai-py/examples/visualizer_smoke.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ End-to-end demo for the WebSocket visualizer. diff --git a/bonsai-py/python/bonsai_py/__init__.py b/bonsai-py/python/bonsai_py/__init__.py index fe3d91d..1d2a29f 100644 --- a/bonsai-py/python/bonsai_py/__init__.py +++ b/bonsai-py/python/bonsai_py/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """bonsai-py - behavior trees in Python, powered by the Rust bonsai-bt crate.""" from importlib.metadata import PackageNotFoundError, version as _version diff --git a/bonsai-py/scripts/verify.py b/bonsai-py/scripts/verify.py index bd7a821..830b19d 100644 --- a/bonsai-py/scripts/verify.py +++ b/bonsai-py/scripts/verify.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- r""" Verification script for bonsai-py. diff --git a/bonsai-py/src/action_args.rs b/bonsai-py/src/action_args.rs index 60437df..57af8a0 100644 --- a/bonsai-py/src/action_args.rs +++ b/bonsai-py/src/action_args.rs @@ -39,7 +39,9 @@ impl PyActionArgs { /// callback receives. Hot path — one `clone_ref` plus an `f64` copy. pub(crate) fn from_rust(args: &ActionArgs, py: Python<'_>) -> Self { Self { - dt: args.dt, + // `args.dt` is `bonsai_bt::Float` (f32 or f64 per feature). Cast to + // f64 at the Python boundary — Python's `float` is always f64. + dt: args.dt as f64, action: args.action.0.clone_ref(py), } } diff --git a/bonsai-py/src/behavior.rs b/bonsai-py/src/behavior.rs index 83553d8..e8b1efd 100644 --- a/bonsai-py/src/behavior.rs +++ b/bonsai-py/src/behavior.rs @@ -90,7 +90,7 @@ pub fn wait_fn(seconds: f64) -> PyResult { if seconds.is_nan() { return Err(PyValueError::new_err("Wait: seconds must not be NaN")); } - Ok(PyBehavior::wrap(Behavior::Wait(seconds))) + Ok(PyBehavior::wrap(Behavior::Wait(seconds as bonsai_bt::Float))) } #[gen_stub_pyfunction] @@ -180,10 +180,7 @@ pub fn if_fn( #[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "While")] -pub fn while_fn( - cond: PyRef<'_, PyBehavior>, - body: Vec>, -) -> PyResult { +pub fn while_fn(cond: PyRef<'_, PyBehavior>, body: Vec>) -> PyResult { if body.is_empty() { return Err(PyValueError::new_err("While: body must not be empty")); } @@ -196,10 +193,7 @@ pub fn while_fn( #[gen_stub_pyfunction] #[pyfunction] #[pyo3(name = "WhileAll")] -pub fn while_all_fn( - cond: PyRef<'_, PyBehavior>, - body: Vec>, -) -> PyResult { +pub fn while_all_fn(cond: PyRef<'_, PyBehavior>, body: Vec>) -> PyResult { if body.is_empty() { return Err(PyValueError::new_err("WhileAll: body must not be empty")); } diff --git a/bonsai-py/src/bt.rs b/bonsai-py/src/bt.rs index 3fa1e32..1bf389f 100644 --- a/bonsai-py/src/bt.rs +++ b/bonsai-py/src/bt.rs @@ -7,8 +7,7 @@ use crate::action_args::PyActionArgs; use crate::behavior::{PyAction, PyBehavior}; use crate::status::PyStatus; -const POISONED_MSG: &str = - "BT was invalidated by a failed with_telemetry call; construct a new BT"; +const POISONED_MSG: &str = "BT was invalidated by a failed with_telemetry call; construct a new BT"; /// A behavior-tree executor wrapping `bonsai_bt::BT`. /// @@ -22,15 +21,11 @@ pub struct PyBT { impl PyBT { fn require_inner(&self) -> PyResult<&BT>> { - self.inner - .as_ref() - .ok_or_else(|| PyRuntimeError::new_err(POISONED_MSG)) + self.inner.as_ref().ok_or_else(|| PyRuntimeError::new_err(POISONED_MSG)) } fn require_inner_mut(&mut self) -> PyResult<&mut BT>> { - self.inner - .as_mut() - .ok_or_else(|| PyRuntimeError::new_err(POISONED_MSG)) + self.inner.as_mut().ok_or_else(|| PyRuntimeError::new_err(POISONED_MSG)) } } @@ -45,14 +40,13 @@ impl PyBT { } } - fn tick( - &mut self, - py: Python<'_>, - dt: f64, - callback: Py, - ) -> PyResult> { + fn tick(&mut self, py: Python<'_>, dt: f64, callback: Py) -> PyResult> { let inner = self.require_inner_mut()?; - let event: Event = UpdateArgs { dt }.into(); + // `UpdateArgs.dt` is `bonsai_bt::Float`; cast from the f64 Python input. + let event: Event = UpdateArgs { + dt: dt as bonsai_bt::Float, + } + .into(); let mut cb_err: Option = None; let result = inner.tick(&event, &mut |args, bb: &mut Py| { if cb_err.is_some() { @@ -61,8 +55,9 @@ impl PyBT { let py_args = PyActionArgs::from_rust(&args, py); let bb_ref = bb.clone_ref(py); match callback.call1(py, (py_args, bb_ref)) { + // Callback returns Python f64; cast back to `bonsai_bt::Float`. Ok(ret) => match ret.extract::<(PyStatus, f64)>(py) { - Ok((s, remaining)) => (s.into(), remaining), + Ok((s, remaining)) => (s.into(), remaining as bonsai_bt::Float), Err(e) => { cb_err = Some(e); (Status::Failure, 0.0) @@ -77,7 +72,8 @@ impl PyBT { if let Some(e) = cb_err { return Err(e); } - Ok(result.map(|(s, dt)| (s.into(), dt))) + // Tick result's `Float` -> f64 for the Python return tuple. + Ok(result.map(|(s, dt)| (s.into(), dt as f64))) } fn blackboard(&self, py: Python<'_>) -> PyResult> { @@ -102,23 +98,14 @@ impl PyBT { } #[pyo3(signature = (port, host = "127.0.0.1"))] - fn with_telemetry<'py>( - mut slf: PyRefMut<'py, Self>, - port: u16, - host: &str, - ) -> PyResult> { - let inner = slf - .inner - .take() - .ok_or_else(|| PyRuntimeError::new_err(POISONED_MSG))?; + fn with_telemetry<'py>(mut slf: PyRefMut<'py, Self>, port: u16, host: &str) -> PyResult> { + let inner = slf.inner.take().ok_or_else(|| PyRuntimeError::new_err(POISONED_MSG))?; match inner.with_telemetry_at(host, port) { Ok(new_inner) => { slf.inner = Some(new_inner); Ok(slf) } - Err(e) => Err(PyOSError::new_err(format!( - "with_telemetry({host}:{port}) failed: {e}" - ))), + Err(e) => Err(PyOSError::new_err(format!("with_telemetry({host}:{port}) failed: {e}"))), } } } diff --git a/bonsai-py/src/lib.rs b/bonsai-py/src/lib.rs index 37f8e57..e2459a9 100644 --- a/bonsai-py/src/lib.rs +++ b/bonsai-py/src/lib.rs @@ -8,8 +8,8 @@ mod status; use action_args::PyActionArgs; use behavior::{ - action_fn, after_fn, always_succeed_fn, if_fn, invert_fn, race_fn, select_fn, sequence_fn, - wait_fn, wait_forever_fn, when_all_fn, when_any_fn, while_all_fn, while_fn, PyBehavior, + action_fn, after_fn, always_succeed_fn, if_fn, invert_fn, race_fn, select_fn, sequence_fn, wait_fn, + wait_forever_fn, when_all_fn, when_any_fn, while_all_fn, while_fn, PyBehavior, }; use bt::PyBT; use status::PyStatus; diff --git a/bonsai-py/src/status.rs b/bonsai-py/src/status.rs index 045e634..deb80bb 100644 --- a/bonsai-py/src/status.rs +++ b/bonsai-py/src/status.rs @@ -21,10 +21,11 @@ pub enum PyStatus { impl PyStatus { /// Pickle support: name the singleton by class + variant name, since /// PyO3 simple enums refuse construction by call (`Status(0)` raises). - fn __reduce__<'py>( - &self, - py: Python<'py>, - ) -> PyResult<(Bound<'py, PyAny>, (Bound<'py, PyAny>, &'static str))> { + // The nested-tuple return shape is dictated by Python's pickle protocol + // (callable, args-tuple); factoring it into a type alias would obscure + // the contract more than it would clarify. + #[allow(clippy::type_complexity)] + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, (Bound<'py, PyAny>, &'static str))> { let getattr = py.import("builtins")?.getattr("getattr")?; let cls = py.get_type::().into_any(); let name = match self { diff --git a/bonsai-py/tests/conftest.py b/bonsai-py/tests/conftest.py index dd7cf54..76e7c5d 100644 --- a/bonsai-py/tests/conftest.py +++ b/bonsai-py/tests/conftest.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Shared fixtures for bonsai-py tests.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_action_args.py b/bonsai-py/tests/test_action_args.py index e23c269..4096967 100644 --- a/bonsai-py/tests/test_action_args.py +++ b/bonsai-py/tests/test_action_args.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ActionArgs: construction, identity, frozen, repr.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_behavior.py b/bonsai-py/tests/test_behavior.py index 347e9cb..1812eb3 100644 --- a/bonsai-py/tests/test_behavior.py +++ b/bonsai-py/tests/test_behavior.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Behavior class + 14 factory functions + ports of Rust behavior_tests.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_bt.py b/bonsai-py/tests/test_bt.py index beba26f..618fab5 100644 --- a/bonsai-py/tests/test_bt.py +++ b/bonsai-py/tests/test_bt.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """BT class: tick mechanics, callback errors, blackboard, reset, finished state.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_drift.py b/bonsai-py/tests/test_drift.py index b41bebf..83f2242 100644 --- a/bonsai-py/tests/test_drift.py +++ b/bonsai-py/tests/test_drift.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Drift gate: every Rust binding has at least one exercising Python test. Two parity checks: diff --git a/bonsai-py/tests/test_module.py b/bonsai-py/tests/test_module.py index eeab305..3204690 100644 --- a/bonsai-py/tests/test_module.py +++ b/bonsai-py/tests/test_module.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Module-level surface: __version__, __all__, __doc__, RUNNING.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_performance.py b/bonsai-py/tests/test_performance.py index 7f8e03c..88694c1 100644 --- a/bonsai-py/tests/test_performance.py +++ b/bonsai-py/tests/test_performance.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Performance budget -- generous bounds to avoid CI flakiness. Heavy benchmarks gated behind `@pytest.mark.bench` (run with `pytest -m bench`).""" from __future__ import annotations diff --git a/bonsai-py/tests/test_status.py b/bonsai-py/tests/test_status.py index 3f3e373..9887fee 100644 --- a/bonsai-py/tests/test_status.py +++ b/bonsai-py/tests/test_status.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Status enum: semantics, pickle, copy, hash, identity.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_telemetry.py b/bonsai-py/tests/test_telemetry.py index 94d539a..4aae115 100644 --- a/bonsai-py/tests/test_telemetry.py +++ b/bonsai-py/tests/test_telemetry.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """with_telemetry: chainable, bind failures, poisoned state, host parameter.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_threading_and_pickle.py b/bonsai-py/tests/test_threading_and_pickle.py index ba46fed..ac8223e 100644 --- a/bonsai-py/tests/test_threading_and_pickle.py +++ b/bonsai-py/tests/test_threading_and_pickle.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Cross-process and cross-thread semantics: BT is unsendable + unpicklable.""" from __future__ import annotations From 1fcca8f6805088b88711d2074c7770a57c42f5e7 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sun, 17 May 2026 13:08:02 -0500 Subject: [PATCH 17/25] more pipeline fixes --- .github/workflows/python-wheels.yml | 17 +++++++++++++---- .github/workflows/rust-pr.yml | 8 ++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index a263dfd..da83b61 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -25,15 +25,22 @@ jobs: fail-fast: false matrix: target: + # `manylinux: "2_28"` makes maturin-action run the build inside the + # official PyPA manylinux_2_28 container (Rocky Linux 8 / glibc 2.28). + # Without this, the build runs on the host (Ubuntu glibc 2.39) and + # produces a wheel that fails the auditwheel manylinux_2_28 check. - label: linux-x86_64 runner: ubuntu-latest - args: --release --manylinux 2_28 + target: x86_64-unknown-linux-gnu + manylinux: "2_28" - label: macos-universal2 runner: macos-latest - args: --release --target universal2-apple-darwin + target: universal2-apple-darwin + manylinux: "auto" - label: windows-x86_64 runner: windows-latest - args: --release + target: x86_64-pc-windows-msvc + manylinux: "auto" steps: - uses: actions/checkout@v4 @@ -67,7 +74,9 @@ jobs: with: working-directory: bonsai-py command: build - args: ${{ matrix.target.args }} --out dist --strip + target: ${{ matrix.target.target }} + manylinux: ${{ matrix.target.manylinux }} + args: --release --out dist --strip - name: Verify wheel (Linux/macOS only — Windows venv quirks) if: matrix.target.runner != 'windows-latest' diff --git a/.github/workflows/rust-pr.yml b/.github/workflows/rust-pr.yml index f1e41fa..076e1b1 100644 --- a/.github/workflows/rust-pr.yml +++ b/.github/workflows/rust-pr.yml @@ -41,6 +41,14 @@ jobs: with: python-version: ${{ matrix.python }} - uses: dtolnay/rust-toolchain@stable + - name: Create + activate venv (maturin develop requires one) + # $GITHUB_PATH prepends to PATH for every subsequent step; + # $GITHUB_ENV exports VIRTUAL_ENV (which maturin/pip detect) + # so we don't need to `source venv/bin/activate` in each step. + run: | + python -m venv $HOME/.venv + echo "$HOME/.venv/bin" >> $GITHUB_PATH + echo "VIRTUAL_ENV=$HOME/.venv" >> $GITHUB_ENV - name: Install maturin and test deps run: pip install "maturin>=1.7,<2.0" pytest pytest-timeout mypy - name: Build and install bonsai-py From 6f50ffefdb64e559ef69a810387fa9ac6e2bf5bb Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Fri, 22 May 2026 23:19:05 -0500 Subject: [PATCH 18/25] pr fixes --- .github/workflows/rust-pr.yml | 2 -- .pre-commit-config.yaml | 1 - bonsai-py/examples/async_drone.py | 1 - bonsai-py/examples/boids_console.py | 13 +++++-------- bonsai-py/examples/graphviz_demo.py | 1 - bonsai-py/examples/race_timeout.py | 1 - bonsai-py/examples/simple_npc_ai.py | 1 - bonsai-py/examples/visualizer_smoke.py | 1 - bonsai-py/python/bonsai_py/__init__.py | 1 - bonsai-py/scripts/verify.py | 1 - bonsai-py/tests/conftest.py | 1 - bonsai-py/tests/test_action_args.py | 1 - bonsai-py/tests/test_behavior.py | 1 - bonsai-py/tests/test_bt.py | 1 - bonsai-py/tests/test_drift.py | 1 - bonsai-py/tests/test_module.py | 1 - bonsai-py/tests/test_performance.py | 1 - bonsai-py/tests/test_status.py | 1 - bonsai-py/tests/test_telemetry.py | 1 - bonsai-py/tests/test_threading_and_pickle.py | 1 - 20 files changed, 5 insertions(+), 28 deletions(-) diff --git a/.github/workflows/rust-pr.yml b/.github/workflows/rust-pr.yml index 076e1b1..98dcd02 100644 --- a/.github/workflows/rust-pr.yml +++ b/.github/workflows/rust-pr.yml @@ -34,8 +34,6 @@ jobs: matrix: python: ["3.10", "3.13"] steps: - - name: Install libudev - run: sudo apt-get update && sudo apt-get install -y libudev-dev pkg-config librust-alsa-sys-dev - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6.2.0 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d7aa46..eecd0f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,6 @@ repos: - id: end-of-file-fixer - id: file-contents-sorter - id: fix-byte-order-marker - - id: fix-encoding-pragma - id: forbid-new-submodules - id: mixed-line-ending - id: name-tests-test diff --git a/bonsai-py/examples/async_drone.py b/bonsai-py/examples/async_drone.py index d43825c..e4d822a 100644 --- a/bonsai-py/examples/async_drone.py +++ b/bonsai-py/examples/async_drone.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Async drone mission with multi-job orchestration. diff --git a/bonsai-py/examples/boids_console.py b/bonsai-py/examples/boids_console.py index ab3adc0..2fbda88 100644 --- a/bonsai-py/examples/boids_console.py +++ b/bonsai-py/examples/boids_console.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Boids flocking with one Behavior shared across many agents. @@ -166,13 +165,11 @@ def main() -> None: boid = boids[i] boid.x += boid.dx * DT_SECONDS boid.y += boid.dy * DT_SECONDS - if i == 0 or i == NUM_BOIDS - 1: - # Log first + last boid only; full log would be 300 lines. - print( - f"[boid {i:2d}] step {step:2d}" - f" pos=({boid.x:7.1f}, {boid.y:7.1f})" - f" vel=({boid.dx:7.1f}, {boid.dy:7.1f})" - ) + print( + f"[boid {i:2d}] step {step:2d}" + f" pos=({boid.x:7.1f}, {boid.y:7.1f})" + f" vel=({boid.dx:7.1f}, {boid.dy:7.1f})" + ) time.sleep(DT_SECONDS / 10.0) # tiny pause so output is readable print(f"Done after {TICKS} ticks. Each BT instance ticked {bts[0].tick_count()} times.") diff --git a/bonsai-py/examples/graphviz_demo.py b/bonsai-py/examples/graphviz_demo.py index ba6d9e5..f7fa828 100644 --- a/bonsai-py/examples/graphviz_demo.py +++ b/bonsai-py/examples/graphviz_demo.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Print the graphviz DOT representation of an attack-drone behavior tree. diff --git a/bonsai-py/examples/race_timeout.py b/bonsai-py/examples/race_timeout.py index ab156f9..c3ed775 100644 --- a/bonsai-py/examples/race_timeout.py +++ b/bonsai-py/examples/race_timeout.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Race a simulated long-running job against a hard timeout. diff --git a/bonsai-py/examples/simple_npc_ai.py b/bonsai-py/examples/simple_npc_ai.py index 2cd13d9..fc0deab 100644 --- a/bonsai-py/examples/simple_npc_ai.py +++ b/bonsai-py/examples/simple_npc_ai.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Console NPC behavior demo. diff --git a/bonsai-py/examples/visualizer_smoke.py b/bonsai-py/examples/visualizer_smoke.py index 827dbb0..8b8ae32 100644 --- a/bonsai-py/examples/visualizer_smoke.py +++ b/bonsai-py/examples/visualizer_smoke.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ End-to-end demo for the WebSocket visualizer. diff --git a/bonsai-py/python/bonsai_py/__init__.py b/bonsai-py/python/bonsai_py/__init__.py index 1d2a29f..fe3d91d 100644 --- a/bonsai-py/python/bonsai_py/__init__.py +++ b/bonsai-py/python/bonsai_py/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """bonsai-py - behavior trees in Python, powered by the Rust bonsai-bt crate.""" from importlib.metadata import PackageNotFoundError, version as _version diff --git a/bonsai-py/scripts/verify.py b/bonsai-py/scripts/verify.py index 830b19d..bd7a821 100644 --- a/bonsai-py/scripts/verify.py +++ b/bonsai-py/scripts/verify.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- r""" Verification script for bonsai-py. diff --git a/bonsai-py/tests/conftest.py b/bonsai-py/tests/conftest.py index 76e7c5d..dd7cf54 100644 --- a/bonsai-py/tests/conftest.py +++ b/bonsai-py/tests/conftest.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Shared fixtures for bonsai-py tests.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_action_args.py b/bonsai-py/tests/test_action_args.py index 4096967..e23c269 100644 --- a/bonsai-py/tests/test_action_args.py +++ b/bonsai-py/tests/test_action_args.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ActionArgs: construction, identity, frozen, repr.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_behavior.py b/bonsai-py/tests/test_behavior.py index 1812eb3..347e9cb 100644 --- a/bonsai-py/tests/test_behavior.py +++ b/bonsai-py/tests/test_behavior.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Behavior class + 14 factory functions + ports of Rust behavior_tests.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_bt.py b/bonsai-py/tests/test_bt.py index 618fab5..beba26f 100644 --- a/bonsai-py/tests/test_bt.py +++ b/bonsai-py/tests/test_bt.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """BT class: tick mechanics, callback errors, blackboard, reset, finished state.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_drift.py b/bonsai-py/tests/test_drift.py index 83f2242..b41bebf 100644 --- a/bonsai-py/tests/test_drift.py +++ b/bonsai-py/tests/test_drift.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Drift gate: every Rust binding has at least one exercising Python test. Two parity checks: diff --git a/bonsai-py/tests/test_module.py b/bonsai-py/tests/test_module.py index 3204690..eeab305 100644 --- a/bonsai-py/tests/test_module.py +++ b/bonsai-py/tests/test_module.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Module-level surface: __version__, __all__, __doc__, RUNNING.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_performance.py b/bonsai-py/tests/test_performance.py index 88694c1..7f8e03c 100644 --- a/bonsai-py/tests/test_performance.py +++ b/bonsai-py/tests/test_performance.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Performance budget -- generous bounds to avoid CI flakiness. Heavy benchmarks gated behind `@pytest.mark.bench` (run with `pytest -m bench`).""" from __future__ import annotations diff --git a/bonsai-py/tests/test_status.py b/bonsai-py/tests/test_status.py index 9887fee..3f3e373 100644 --- a/bonsai-py/tests/test_status.py +++ b/bonsai-py/tests/test_status.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Status enum: semantics, pickle, copy, hash, identity.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_telemetry.py b/bonsai-py/tests/test_telemetry.py index 4aae115..94d539a 100644 --- a/bonsai-py/tests/test_telemetry.py +++ b/bonsai-py/tests/test_telemetry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """with_telemetry: chainable, bind failures, poisoned state, host parameter.""" from __future__ import annotations diff --git a/bonsai-py/tests/test_threading_and_pickle.py b/bonsai-py/tests/test_threading_and_pickle.py index ac8223e..ba46fed 100644 --- a/bonsai-py/tests/test_threading_and_pickle.py +++ b/bonsai-py/tests/test_threading_and_pickle.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Cross-process and cross-thread semantics: BT is unsendable + unpicklable.""" from __future__ import annotations From bf07d92c550eb8efedafb0a113741bccba4f1c17 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sat, 23 May 2026 00:03:35 -0500 Subject: [PATCH 19/25] more fixes --- bonsai-py/examples/visualizer_smoke.py | 52 +++++++++++++------------- bonsai-py/pyproject.toml | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/bonsai-py/examples/visualizer_smoke.py b/bonsai-py/examples/visualizer_smoke.py index 8b8ae32..4427bb3 100644 --- a/bonsai-py/examples/visualizer_smoke.py +++ b/bonsai-py/examples/visualizer_smoke.py @@ -28,27 +28,27 @@ import time from typing import Any -import bonsai_py as bt +from bonsai_py import * # noqa: F401,F403 -def build_tree() -> bt.Behavior: - return bt.Sequence([ - bt.If( - bt.Action("low_hp"), - bt.AlwaysSucceed(bt.Action("flee")), - bt.Action("regroup"), +def build_tree() -> Behavior: + return Sequence([ + If( + Action("low_hp"), + AlwaysSucceed(Action("flee")), + Action("regroup"), ), - bt.Select([ - bt.Sequence([ - bt.Action("acquire_target"), - bt.WhenAll([bt.Action("aim"), bt.Action("track")]), + Select([ + Sequence([ + Action("acquire_target"), + WhenAll([Action("aim"), Action("track")]), ]), - bt.Race([bt.Action("dodge"), bt.Wait(2.0)]), - bt.Invert(bt.Action("enemy_visible")), + Race([Action("dodge"), Wait(2.0)]), + Invert(Action("enemy_visible")), ]), - bt.While(bt.Action("has_ammo"), [bt.Action("fire"), bt.Wait(0.3)]), - bt.After([bt.Action("cooldown"), bt.Action("ready_signal")]), - bt.WhenAny([bt.Action("victory_check"), bt.Action("retreat_signal")]), + While(Action("has_ammo"), [Action("fire"), Wait(0.3)]), + After([Action("cooldown"), Action("ready_signal")]), + WhenAny([Action("victory_check"), Action("retreat_signal")]), ]) @@ -56,11 +56,11 @@ def build_tree() -> bt.Behavior: # unique phase offset so the same wall tick produces a varied mix of statuses # across the tree (and yellow-Running shows up). CYCLE = ( - bt.Status.Success, - bt.Status.Running, - bt.Status.Failure, - bt.Status.Success, - bt.Status.Running, + Status.Success, + Status.Running, + Status.Failure, + Status.Success, + Status.Running, ) PHASE_OFFSET = { @@ -87,19 +87,19 @@ def build_tree() -> bt.Behavior: def make_callback(tick_n_ref: list[int]): - def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + def cb(args: Any, _bb: Any) -> tuple[Status, float]: phase = PHASE_OFFSET.get(args.action, 0) idx = (tick_n_ref[0] + phase) % len(CYCLE) status = CYCLE[idx] - if args.action in KEEP_ALIVE and status == bt.Status.Failure: - status = bt.Status.Running + if args.action in KEEP_ALIVE and status == Status.Failure: + status = Status.Running return (status, 0.0) return cb def main() -> None: - tree_bt = bt.BT(build_tree(), None).with_telemetry(8910) + tree_bt = BT(build_tree(), None).with_telemetry(8910) tick_n_ref = [0] callback = make_callback(tick_n_ref) print("bonsai-bt visualizer: open http://127.0.0.1:8910/") @@ -109,7 +109,7 @@ def main() -> None: result = tree_bt.tick(1.0, callback) if result is not None: status, _ = result - if status in (bt.Status.Success, bt.Status.Failure): + if status in (Status.Success, Status.Failure): tree_bt.reset_bt() time.sleep(0.4) diff --git a/bonsai-py/pyproject.toml b/bonsai-py/pyproject.toml index 8a6560d..968877c 100644 --- a/bonsai-py/pyproject.toml +++ b/bonsai-py/pyproject.toml @@ -9,7 +9,7 @@ readme = "README.md" license = { text = "MIT" } requires-python = ">=3.10" authors = [ - { name = "Kristoffer Solberg Rakstad", email = "kristoffer.solberg@cognite.com" }, + { name = "Kristoffer Solberg Rakstad", email = "solkristoffer@gmail.com" }, { name = "Anmol Kathail", email = "anmolkathail@gmail.com" }, ] classifiers = [ From 7328a58420b8e65e0b9e9ad6ef4b13986a45e8cc Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sun, 24 May 2026 15:41:58 -0500 Subject: [PATCH 20/25] async drone --- bonsai-py/examples/README.md | 15 +- bonsai-py/examples/async_drone.py | 105 ++++++++------ bonsai-py/examples/threaded_drone.py | 210 +++++++++++++++++++++++++++ 3 files changed, 280 insertions(+), 50 deletions(-) create mode 100644 bonsai-py/examples/threaded_drone.py diff --git a/bonsai-py/examples/README.md b/bonsai-py/examples/README.md index 67125fd..bd7441d 100644 --- a/bonsai-py/examples/README.md +++ b/bonsai-py/examples/README.md @@ -21,7 +21,7 @@ cd bonsai-py && maturin develop --release && cd .. After that, just `source .venv/bin/activate` + `python bonsai-py/examples/.py` in any new shell. -## Examples (6) +## Examples (7) ### [simple_npc_ai.py](simple_npc_ai.py) — console NPC NPC runs and shoots until action points are exhausted, then rests and dies. Demonstrates `WhileAll`, blackboard mutation via `@dataclass`, structural-`match` callback. @@ -61,11 +61,16 @@ Builds **one** `Behavior` tree and binds it to 10 independent `BT` instances (ea python bonsai-py/examples/boids_console.py ``` -### [async_drone.py](async_drone.py) — multi-job mission -Drone mission: takeoff → check battery → fly (or fall back to land) → land → repeat. Each long-running step runs on a background thread; the BT polls per-job queues. Prints the tree's `graphviz()` at the start, then runs the mission for ~8 seconds. +### [threaded_drone.py](threaded_drone.py) — multi-job mission (threading) +Drone mission: takeoff → check battery → fly (or fall back to land) → land → repeat. Each long-running step runs on a background `threading.Thread`; the BT polls per-job `queue.Queue`s. Prints the tree's `graphviz()` at the start, then runs the mission for ~8 seconds. **Pick this variant when actions block on hardware or sync IO** — blocking syscalls, vendor SDKs without an async API, `subprocess.run`, etc. ```bash -python bonsai-py/examples/async_drone.py +python bonsai-py/examples/threaded_drone.py ``` -Demonstrates `Select` for prioritized fallback, multi-job orchestration via per-job channels, asyncio + threading. +### [async_drone.py](async_drone.py) — multi-job mission (asyncio) +Same mission and tree as `threaded_drone.py`, but background jobs are `async def` coroutines on a single asyncio event loop, communicating via `asyncio.Queue`. **Pick this variant when actions are awaitable** (`aiohttp`, async DB drivers, websockets) — N-way concurrency without OS thread overhead. + +```bash +python bonsai-py/examples/async_drone.py +``` diff --git a/bonsai-py/examples/async_drone.py b/bonsai-py/examples/async_drone.py index e4d822a..32bf3e6 100644 --- a/bonsai-py/examples/async_drone.py +++ b/bonsai-py/examples/async_drone.py @@ -1,9 +1,7 @@ """ -Async drone mission with multi-job orchestration. +Async drone mission with multi-job orchestration (asyncio-native). -A drone takes off, checks battery, flies to a mission point (or falls -back to landing at the dock if the battery is low), then lands — -repeating while a background collision-avoidance task runs: +Same mission as `threaded_drone.py`: While( cond = AvoidOthers, @@ -17,15 +15,27 @@ ], ) -Each long-running action runs on its own `threading.Thread` and reports -status through a per-job `queue.Queue`. The BT polls non-blockingly with -`get_nowait()`. The BT itself stays on the main asyncio loop. The script -prints the tree's `graphviz()` at the start, then runs the mission until -a wall-clock cap. +…but each long-running action is an `async def` coroutine scheduled on +the same asyncio event loop as the BT tick loop, communicating with the +BT via `asyncio.Queue`. No OS threads — every job is cooperatively +scheduled on a single thread. -Demonstrates `Select` for prioritized fallback, multi-job orchestration -via per-job channels, an asyncio main loop alongside per-job threads, -and `BT.graphviz()` for static tree visualization at startup. +**Pick this variant when actions are awaitable** — `aiohttp`, async DB +drivers (`asyncpg`, `motor`), websockets, async stream readers, async +subprocess. + +How the integration works: + +* `BT.tick()` is synchronous. The callback we pass to it is also + synchronous — it polls each per-job `asyncio.Queue` with the + non-blocking `get_nowait()`. The callback never awaits. +* On the first call for a given action, the callback spawns the job + coroutine via `asyncio.create_task(job(q))`. The task starts running + on the next loop iteration. +* The main loop is `async def` and uses `await asyncio.sleep(0.5)` to + yield control back to the event loop between BT ticks. While main + is sleeping, the job coroutines run and push status updates into + their queues. Run: python bonsai-py/examples/async_drone.py @@ -34,12 +44,10 @@ import asyncio import enum -import queue import random -import threading import time -from dataclasses import dataclass, field -from typing import Any, Optional +from dataclasses import dataclass +from typing import Any, Awaitable, Callable, Optional import bonsai_py as bt @@ -62,72 +70,78 @@ class FlyToPoint: @dataclass class DroneState: - avoid_others: Optional[queue.Queue[bt.Status]] = None - takeoff: Optional[queue.Queue[bt.Status]] = None - land: Optional[queue.Queue[bt.Status]] = None - fly_to_point: Optional[queue.Queue[bt.Status]] = None + avoid_others: Optional[asyncio.Queue[bt.Status]] = None + takeoff: Optional[asyncio.Queue[bt.Status]] = None + land: Optional[asyncio.Queue[bt.Status]] = None + fly_to_point: Optional[asyncio.Queue[bt.Status]] = None -# ---- Background "jobs": one thread per long-running action ----------------- +# ---- Background "jobs": one coroutine per long-running action -------------- # Each pushes Status.Running every step, then Status.Success when done. -def collision_avoidance_task(q: queue.Queue[bt.Status]) -> None: +async def collision_avoidance_task(q: asyncio.Queue[bt.Status]) -> None: print("collision avoidance task started") while True: - try: - q.put(bt.Status.Running, timeout=1.0) - except queue.Full: - return - time.sleep(0.1) + q.put_nowait(bt.Status.Running) + await asyncio.sleep(0.1) -def takeoff_task(q: queue.Queue[bt.Status]) -> None: +async def takeoff_task(q: asyncio.Queue[bt.Status]) -> None: print("takeoff task started") for i in range(3): - q.put(bt.Status.Running) - time.sleep(0.3) + q.put_nowait(bt.Status.Running) + await asyncio.sleep(0.3) print(f"takeoff task running for {(i + 1) * 300} ms") print("takeoff task finished") - q.put(bt.Status.Success) + q.put_nowait(bt.Status.Success) -def landing_task(q: queue.Queue[bt.Status]) -> None: +async def landing_task(q: asyncio.Queue[bt.Status]) -> None: print("landing task started") for i in range(3): - q.put(bt.Status.Running) - time.sleep(0.3) + q.put_nowait(bt.Status.Running) + await asyncio.sleep(0.3) print(f"landing task running for {(i + 1) * 300} ms") print("landing task finished") - q.put(bt.Status.Success) + q.put_nowait(bt.Status.Success) -def fly_to_point_task(point: FlyToPoint, q: queue.Queue[bt.Status]) -> None: +async def fly_to_point_task(point: FlyToPoint, q: asyncio.Queue[bt.Status]) -> None: print(f"flying task started: target=({point.x}, {point.y}, {point.z})") for i in range(3): - q.put(bt.Status.Running) - time.sleep(0.5) + q.put_nowait(bt.Status.Running) + await asyncio.sleep(0.5) print(f"flying task running for {(i + 1) * 500} ms") print("flying task finished") - q.put(bt.Status.Success) + q.put_nowait(bt.Status.Success) # ---- Polling helpers ------------------------------------------------------- +SpawnCoro = Callable[[asyncio.Queue[bt.Status]], Awaitable[None]] + + def poll_job( q_attr: str, state: DroneState, - spawn: callable, # type: ignore[type-arg] + spawn: SpawnCoro, dt: float, ) -> tuple[bt.Status, float]: - """Generic 'spawn on first call, poll thereafter' pattern for any threaded job.""" + """Generic 'schedule on first call, poll thereafter' pattern for any async job. + + Calls `asyncio.create_task(spawn(q))` on first invocation; subsequent + invocations drain the queue non-blockingly. Must be called from inside + a running event loop (the BT tick runs from inside `await asyncio.sleep` + in `main`, so this holds). + """ q = getattr(state, q_attr) if q is None: - q = queue.Queue() + q = asyncio.Queue() setattr(state, q_attr, q) - threading.Thread(target=spawn, args=(q,), daemon=True).start() + asyncio.create_task(spawn(q)) try: status = q.get_nowait() - except queue.Empty: + except asyncio.QueueEmpty: return bt.RUNNING if status == bt.Status.Running: return bt.RUNNING @@ -150,7 +164,8 @@ def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: print(f"check battery: {'OK' if ok else 'LOW'}") return (bt.Status.Success if ok else bt.Status.Failure, args.dt) if isinstance(action, FlyToPoint): - spawn = lambda q, p=action: fly_to_point_task(p, q) + async def spawn(q: asyncio.Queue[bt.Status], p: FlyToPoint = action) -> None: + await fly_to_point_task(p, q) return poll_job("fly_to_point", state, spawn, args.dt) raise ValueError(f"unknown action: {action!r}") diff --git a/bonsai-py/examples/threaded_drone.py b/bonsai-py/examples/threaded_drone.py new file mode 100644 index 0000000..b9f387e --- /dev/null +++ b/bonsai-py/examples/threaded_drone.py @@ -0,0 +1,210 @@ +""" +Threaded drone mission with multi-job orchestration. + +A drone takes off, checks battery, flies to a mission point (or falls +back to landing at the dock if the battery is low), then lands — +repeating while a background collision-avoidance task runs: + + While( + cond = AvoidOthers, + body = [ + TakeOff, + Select([ + Sequence([CheckBattery, FlyToPoint(10, 10, 10)]), + FlyToPoint(0, 0, 0), # dock fallback + ]), + Land, + ], + ) + +Each long-running action runs on its own `threading.Thread` and reports +status through a per-job `queue.Queue`. The BT polls non-blockingly with +`get_nowait()`. The script prints the tree's `graphviz()` at the start, +then runs the mission until a wall-clock cap. + +**Pick this variant when actions block on hardware or sync IO** — +blocking syscalls, blocking C extensions, vendor SDKs without an async +API, `subprocess.run`, file IO without `aiofiles`, etc. The OS scheduler +runs the threads in parallel, so a blocking call in one job doesn't +freeze the others. + +See `async_drone.py` for the asyncio-native variant of the same mission +(pick that when actions are awaitable — `aiohttp`, async DB drivers, +websockets, async stream readers). + +Demonstrates `Select` for prioritized fallback, multi-job orchestration +via per-job channels, threading + queue.Queue, and `BT.graphviz()` for +static tree visualization at startup. + +Run: + python bonsai-py/examples/threaded_drone.py +""" +from __future__ import annotations + +import enum +import queue +import random +import threading +import time +from dataclasses import dataclass +from typing import Any, Optional + +import bonsai_py as bt + +MAX_WALL_SECONDS = 8.0 # Demo cap; the BT itself would loop forever. + + +class DroneAction(enum.Enum): + AVOID_OTHERS = enum.auto() + TAKE_OFF = enum.auto() + LAND = enum.auto() + CHECK_BATTERY = enum.auto() + + +@dataclass(frozen=True) +class FlyToPoint: + x: float + y: float + z: float + + +@dataclass +class DroneState: + avoid_others: Optional[queue.Queue[bt.Status]] = None + takeoff: Optional[queue.Queue[bt.Status]] = None + land: Optional[queue.Queue[bt.Status]] = None + fly_to_point: Optional[queue.Queue[bt.Status]] = None + + +# ---- Background "jobs": one thread per long-running action ----------------- +# Each pushes Status.Running every step, then Status.Success when done. + +def collision_avoidance_task(q: queue.Queue[bt.Status]) -> None: + print("collision avoidance task started") + while True: + try: + q.put(bt.Status.Running, timeout=1.0) + except queue.Full: + return + time.sleep(0.1) + + +def takeoff_task(q: queue.Queue[bt.Status]) -> None: + print("takeoff task started") + for i in range(3): + q.put(bt.Status.Running) + time.sleep(0.3) + print(f"takeoff task running for {(i + 1) * 300} ms") + print("takeoff task finished") + q.put(bt.Status.Success) + + +def landing_task(q: queue.Queue[bt.Status]) -> None: + print("landing task started") + for i in range(3): + q.put(bt.Status.Running) + time.sleep(0.3) + print(f"landing task running for {(i + 1) * 300} ms") + print("landing task finished") + q.put(bt.Status.Success) + + +def fly_to_point_task(point: FlyToPoint, q: queue.Queue[bt.Status]) -> None: + print(f"flying task started: target=({point.x}, {point.y}, {point.z})") + for i in range(3): + q.put(bt.Status.Running) + time.sleep(0.5) + print(f"flying task running for {(i + 1) * 500} ms") + print("flying task finished") + q.put(bt.Status.Success) + + +# ---- Polling helpers ------------------------------------------------------- + +def poll_job( + q_attr: str, + state: DroneState, + spawn: callable, # type: ignore[type-arg] + dt: float, +) -> tuple[bt.Status, float]: + """Generic 'spawn on first call, poll thereafter' pattern for any threaded job.""" + q = getattr(state, q_attr) + if q is None: + q = queue.Queue() + setattr(state, q_attr, q) + threading.Thread(target=spawn, args=(q,), daemon=True).start() + try: + status = q.get_nowait() + except queue.Empty: + return bt.RUNNING + if status == bt.Status.Running: + return bt.RUNNING + setattr(state, q_attr, None) + return (status, dt) + + +def make_callback(state: DroneState, rng: random.Random): + def cb(args: Any, _bb: Any) -> tuple[bt.Status, float]: + action = args.action + if action == DroneAction.AVOID_OTHERS: + return poll_job("avoid_others", state, collision_avoidance_task, args.dt) + if action == DroneAction.TAKE_OFF: + return poll_job("takeoff", state, takeoff_task, args.dt) + if action == DroneAction.LAND: + return poll_job("land", state, landing_task, args.dt) + if action == DroneAction.CHECK_BATTERY: + # Fast sync action: 80% chance OK, 20% chance low -> Select fallback fires. + ok = rng.random() < 0.8 + print(f"check battery: {'OK' if ok else 'LOW'}") + return (bt.Status.Success if ok else bt.Status.Failure, args.dt) + if isinstance(action, FlyToPoint): + spawn = lambda q, p=action: fly_to_point_task(p, q) + return poll_job("fly_to_point", state, spawn, args.dt) + raise ValueError(f"unknown action: {action!r}") + + return cb + + +def build_tree() -> bt.Behavior: + fly_if_healthy = bt.Sequence([ + bt.Action(DroneAction.CHECK_BATTERY), + bt.Action(FlyToPoint(10.0, 10.0, 10.0)), + ]) + fly_to_dock = bt.Action(FlyToPoint(0.0, 0.0, 0.0)) + mission_with_fallback = bt.Select([fly_if_healthy, fly_to_dock]) + return bt.While( + bt.Action(DroneAction.AVOID_OTHERS), + [ + bt.Action(DroneAction.TAKE_OFF), + mission_with_fallback, + bt.Action(DroneAction.LAND), + ], + ) + + +def main() -> None: + tree_bt = bt.BT(build_tree(), None) + state = DroneState() + rng = random.Random(0) + callback = make_callback(state, rng) + print("=== drone tree ===") + print(tree_bt.graphviz()) + print("=== mission start ===") + + start = time.perf_counter() + last = start + while True: + time.sleep(0.5) + now = time.perf_counter() + dt = now - last + last = now + result = tree_bt.tick(dt, callback) + if result is None: + break + if now - start > MAX_WALL_SECONDS: + print(f"=== demo cap reached ({MAX_WALL_SECONDS}s) — exiting ===") + break + + +if __name__ == "__main__": + main() From 7d3ff0555aa4f2d71b512cd5acd0479d160494f8 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sun, 24 May 2026 15:42:45 -0500 Subject: [PATCH 21/25] update email --- bonsai-py/Cargo.toml | 2 +- bonsai/Cargo.toml | 2 +- examples/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bonsai-py/Cargo.toml b/bonsai-py/Cargo.toml index 21a2c16..c2009bf 100644 --- a/bonsai-py/Cargo.toml +++ b/bonsai-py/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" rust-version = "1.80.0" description = "Python bindings for the bonsai-bt behavior tree library" license = "MIT" -authors = ["Kristoffer Solberg Rakstad "] +authors = ["Kristoffer Solberg Rakstad "] repository = "https://github.com/sollimann/bonsai.git" homepage = "https://github.com/sollimann/bonsai" publish = false diff --git a/bonsai/Cargo.toml b/bonsai/Cargo.toml index a6adbfd..7fb657f 100644 --- a/bonsai/Cargo.toml +++ b/bonsai/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = ["Kristoffer Solberg Rakstad "] +authors = ["Kristoffer Solberg Rakstad "] autotests = false categories = [] description = "Behavior tree" diff --git a/examples/Cargo.toml b/examples/Cargo.toml index ecf3c01..7421791 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = ["Kristoffer Solberg Rakstad "] +authors = ["Kristoffer Solberg Rakstad "] description = "Behavior tree examples" edition = "2021" name = "examples" From c3deb8dfd9b72dd11a18f08cee7276d0428127cd Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sun, 24 May 2026 15:56:34 -0500 Subject: [PATCH 22/25] refactor tests to remove duplicates --- .github/workflows/python-wheels.yml | 1 - .github/workflows/rust-pr.yml | 2 - bonsai-py/scripts/README.md | 12 - bonsai-py/scripts/verify.py | 503 ---------------------------- bonsai-py/tests/README.md | 2 +- bonsai-py/tests/test_mypy_strict.py | 61 ++++ 6 files changed, 62 insertions(+), 519 deletions(-) delete mode 100644 bonsai-py/scripts/verify.py create mode 100644 bonsai-py/tests/test_mypy_strict.py diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index da83b61..b94ac24 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -88,7 +88,6 @@ jobs: pip install pytest pytest-timeout mypy pip install bonsai-py/dist/*.whl pytest bonsai-py/tests/ - python bonsai-py/scripts/verify.py - name: Wheel size sanity (Linux/macOS only) if: matrix.target.runner != 'windows-latest' diff --git a/.github/workflows/rust-pr.yml b/.github/workflows/rust-pr.yml index 98dcd02..0c600a5 100644 --- a/.github/workflows/rust-pr.yml +++ b/.github/workflows/rust-pr.yml @@ -54,5 +54,3 @@ jobs: run: maturin develop --release - name: Run pytest run: pytest -v bonsai-py/tests/ - - name: Run verify.py (sanity) - run: python bonsai-py/scripts/verify.py diff --git a/bonsai-py/scripts/README.md b/bonsai-py/scripts/README.md index bdd1135..66b1cb1 100644 --- a/bonsai-py/scripts/README.md +++ b/bonsai-py/scripts/README.md @@ -7,7 +7,6 @@ Developer scripts for working on the `bonsai_py` extension module. | Script | Purpose | |---|---| | [regen-stubs.sh](regen-stubs.sh) | Regenerates `python/bonsai_py/__init__.pyi` from the `#[gen_stub_*]` annotations on the Rust side. Run after editing any annotated `#[pyclass]` / `#[pyfunction]` / `#[pymethods]`. Also runs automatically via the `regen-stubs` pre-commit hook and is enforced in CI. | -| [verify.py](verify.py) | End-to-end smoke test of the installed wheel: imports `bonsai_py`, exercises every public API (Status, ActionArgs, all 14 factories, BT tick/telemetry, type stub presence, mypy `--strict` on a sample script). Standalone — not a pytest suite (the long-form behavior coverage lives in [../tests/](../tests/)). | ## Prerequisites @@ -20,15 +19,4 @@ From the repository root, with the venv activated: ```bash # Regenerate stubs after editing Rust annotations bash bonsai-py/scripts/regen-stubs.sh - -# Run the verification smoke test (every test always runs; exits 0 on success) -python bonsai-py/scripts/verify.py -``` - -## Dependencies - -`verify.py` uses `mypy` for the strict-typing check (skipped with a warning if `mypy` is not installed): - -```bash -pip install mypy ``` diff --git a/bonsai-py/scripts/verify.py b/bonsai-py/scripts/verify.py deleted file mode 100644 index bd7a821..0000000 --- a/bonsai-py/scripts/verify.py +++ /dev/null @@ -1,503 +0,0 @@ -r""" -Verification script for bonsai-py. - -WHAT THIS IS - Python-side assertions for the installed `bonsai_py` extension module. - Every test runs on every invocation; the script exits 0 on full pass, - 1 on the first failure, 2 on a usage error. - -USAGE - From the repository root, with a venv that has the wheel installed: - - source .venv/bin/activate # macOS / Linux / WSL - .\.venv\Scripts\Activate.ps1 # Windows PowerShell - - Then: - - python bonsai-py/scripts/verify.py - -PREREQUISITES - cargo check -p bonsai-py - cd bonsai-py && maturin develop - - `maturin develop` produces the abi3 wheel and installs it editable into - the active venv. This script imports the installed `bonsai_py` module — - if it fails with `ImportError`, you forgot to `maturin develop`. - -WHAT THIS IS NOT - Not a pytest suite. Long-term behavior coverage lives under - `bonsai-py/tests/` and is run via pytest separately. -""" - -from __future__ import annotations - -import sys - - -def test_module_scaffolding() -> None: - """Module imports cleanly; version metadata and docstring present.""" - import bonsai_py - - assert bonsai_py.__version__ == "0.12.0", bonsai_py.__version__ - print(" OK: __version__ == 0.12.0") - - assert bonsai_py.__doc__, "missing module docstring" - print(" OK: module docstring present") - - -def test_status_and_action_args() -> None: - """Status enum + ActionArgs class: semantics, pickle, copy, identity.""" - import copy - import pickle - - import bonsai_py as bt - - assert hasattr(bt, "Status") and hasattr(bt, "ActionArgs") - print(" OK: Status and ActionArgs exported") - - s = bt.Status - assert s.Success == s.Success - assert s.Success != s.Failure - assert s.Success == 0 and s.Failure == 1 and s.Running == 2 - assert int(s.Success) == 0 - assert hash(s.Success) == hash(s.Success) - assert {s.Success: "ok"}[s.Success] == "ok" - assert s.Success is s.Success - assert repr(s.Success) == "Status.Success" - print(" OK: Status semantics (eq, eq_int, hash, identity, repr)") - - assert bt.Status.__module__ == "bonsai_py", bt.Status.__module__ - assert bt.ActionArgs.__module__ == "bonsai_py", bt.ActionArgs.__module__ - print(" OK: __module__ == 'bonsai_py' on both classes") - - for v in (s.Success, s.Failure, s.Running): - assert pickle.loads(pickle.dumps(v)) is v - for v in (s.Success, s.Failure, s.Running): - assert copy.copy(v) is v - assert copy.deepcopy(v) is v - print(" OK: pickle / copy.copy / copy.deepcopy preserve Status singleton identity") - - a = bt.ActionArgs(0.5, "inc") - assert a.dt == 0.5 - assert a.action == "inc" - assert repr(a) == "ActionArgs(dt=0.5, action='inc')" - try: - a.dt = 0.7 - except AttributeError: - pass - else: - raise AssertionError("ActionArgs should be frozen (got mutation)") - print(" OK: ActionArgs construct + readonly + repr") - - bb = {"k": 1} - a2 = bt.ActionArgs(0.0, bb) - assert a2.action is bb, "action identity not preserved through FFI" - a2.action["k"] = 2 - assert bb["k"] == 2, "mutation via args.action did not persist" - print(" OK: action identity preserved across FFI boundary") - - -def test_behavior_factories() -> None: - """14 tree-construction factories: exported, build trees, validate inputs.""" - import bonsai_py as bt - - factories = ( - "Action", "Wait", "WaitForever", - "Invert", "AlwaysSucceed", - "Sequence", "Select", "WhenAll", "WhenAny", "After", "Race", - "If", "While", "WhileAll", - ) - for name in factories: - assert hasattr(bt, name), f"missing factory: {name}" - assert callable(getattr(bt, name)), f"not callable: {name}" - print(f" OK: all {len(factories)} factories exported") - - # 27-node tree mirroring the Rust visualizer-smoke example. - tree = bt.Sequence([ - bt.If( - bt.Action("low_hp"), - bt.AlwaysSucceed(bt.Action("flee")), - bt.Action("regroup"), - ), - bt.Select([ - bt.Sequence([ - bt.Action("acquire_target"), - bt.WhenAll([bt.Action("aim"), bt.Action("track")]), - ]), - bt.Race([bt.Action("dodge"), bt.Wait(2.0)]), - bt.Invert(bt.Action("enemy_visible")), - ]), - bt.While(bt.Action("has_ammo"), [bt.Action("fire"), bt.Wait(0.3)]), - bt.After([bt.Action("cooldown"), bt.Action("ready_signal")]), - bt.WhenAny([bt.Action("victory_check"), bt.Action("retreat_signal")]), - ]) - assert repr(tree) == "Sequence(5)" - print(" OK: 27-node nested tree constructs (5 top-level children)") - - wait = bt.Wait(1.0) - reuse = bt.Sequence([wait, wait, wait]) - assert repr(reuse) == "Sequence(3)" - assert repr(wait) == "Wait(1)" - print(" OK: subtree reuse (same Behavior referenced 3x)") - - for label, call in ( - ("While(cond, [])", lambda: bt.While(bt.Action("x"), [])), - ("WhileAll(cond, [])", lambda: bt.WhileAll(bt.Action("x"), [])), - ("Wait(NaN)", lambda: bt.Wait(float("nan"))), - ): - try: - call() - except ValueError: - pass - else: - raise AssertionError(f"{label} should raise ValueError") - print(" OK: validation guards (empty While/WhileAll body, NaN Wait)") - - # Pass-through cases — must NOT raise. - bt.Sequence([]); bt.Select([]) - bt.WhenAll([]); bt.WhenAny([]); bt.After([]); bt.Race([]) - bt.Wait(-1.0); bt.Wait(float("inf")); bt.Wait(1); bt.Wait(0.0) - print(" OK: empty composites + negative/inf/int/zero Wait pass through") - - tree2 = bt.If( - cond=bt.Action("c"), - on_success=bt.Action("s"), - on_failure=bt.Action("f"), - ) - assert repr(tree2) == "If(...)" - print(" OK: keyword arguments accepted on If") - - assert repr(bt.Action({"k": 1})) == "Action(...)" - assert repr(bt.Invert(bt.Action("x"))) == "Invert(...)" - assert repr(bt.WaitForever()) == "WaitForever" - print(" OK: __repr__ is one-line bounded summary") - - assert bt.Behavior.__module__ == "bonsai_py", bt.Behavior.__module__ - print(" OK: Behavior.__module__ == 'bonsai_py'") - - assert repr(bt.Action(None)) == "Action(...)" - print(" OK: Action(None) is a legal leaf") - - # Identity-based equality holds for every variant. Two distinct - # constructions of the same shape must compare as distinct objects. - builders = [ - ("Action", lambda: bt.Action("x")), - ("Wait", lambda: bt.Wait(1.0)), - ("WaitForever", lambda: bt.WaitForever()), - ("Invert", lambda: bt.Invert(bt.Action("x"))), - ("AlwaysSucceed", lambda: bt.AlwaysSucceed(bt.Action("x"))), - ("Sequence", lambda: bt.Sequence([bt.Action("x")])), - ("Select", lambda: bt.Select([bt.Action("x")])), - ("WhenAll", lambda: bt.WhenAll([bt.Action("x")])), - ("WhenAny", lambda: bt.WhenAny([bt.Action("x")])), - ("After", lambda: bt.After([bt.Action("x")])), - ("Race", lambda: bt.Race([bt.Action("x")])), - ("If", lambda: bt.If(bt.Action("c"), bt.Action("s"), bt.Action("f"))), - ("While", lambda: bt.While(bt.Action("c"), [bt.Action("b")])), - ("WhileAll", lambda: bt.WhileAll(bt.Action("c"), [bt.Action("b")])), - ] - for label, make in builders: - a, b = make(), make() - assert a is not b, f"{label}: distinct constructions returned same object" - assert (a == b) is False, f"{label}: structural == returned True" - assert a != b, f"{label}: != returned False" - print(f" OK: identity-based equality across all {len(builders)} variants") - - -def test_bt_tick_and_telemetry() -> None: - """BT class: tick callback semantics, blackboard, reset, telemetry.""" - import socket - - import bonsai_py as bt - - # Doctest equivalent: 5 ticks of 0.5s, expect count == 1. - tree = bt.Sequence([ - bt.Wait(1.0), bt.Action("inc"), - bt.Wait(1.0), bt.Action("inc"), - bt.Wait(0.5), bt.Action("dec"), - ]) - bb = {"count": 0} - tree_bt = bt.BT(tree, bb) - acc = 0 - - def cb(args, _blackboard): - nonlocal acc - if args.action == "inc": - acc += 1 - return (bt.Status.Success, args.dt) - if args.action == "dec": - acc -= 1 - return (bt.Status.Success, args.dt) - return bt.RUNNING - - for _ in range(5): - tree_bt.tick(0.5, cb) - blackboard = tree_bt.blackboard() - blackboard["count"] = acc - assert bb["count"] == 1, bb["count"] - assert tree_bt.tick_count() == 5 - print(" OK: doctest equivalent — count == 1 after 5 ticks") - - assert bt.RUNNING == (bt.Status.Running, 0.0) - print(" OK: bt.RUNNING == (Status.Running, 0.0)") - - # Callback exception propagates with traceback / message intact. - bad_bt = bt.BT(bt.Sequence([bt.Action("boom")]), None) - - def raising_cb(_args, _bb): - raise ValueError("boom") - - try: - bad_bt.tick(0.5, raising_cb) - except ValueError as e: - assert str(e) == "boom", str(e) - print(" OK: callback ValueError propagates with message intact") - else: - raise AssertionError("expected ValueError from tick") - - # Callback returning a non-tuple is rejected by extract. - bad_shape = bt.BT(bt.Sequence([bt.Action("x")]), None) - - def bad_return_cb(_args, _bb): - return "not a tuple" - - try: - bad_shape.tick(0.5, bad_return_cb) - except Exception as e: - msg = str(e).lower() - assert "extract" in msg or "tuple" in msg or isinstance(e, TypeError), e - print(f" OK: bad return shape rejected ({type(e).__name__})") - else: - raise AssertionError("expected error from malformed return") - - # is_finished + reset_bt cycle. - quick = bt.BT(bt.Action("done"), None) - - def done_cb(_args, _bb): - return (bt.Status.Success, 0.0) - - assert not quick.is_finished() - quick.tick(0.0, done_cb) - assert quick.is_finished() - assert quick.tick(0.0, done_cb) is None - quick.reset_bt() - assert not quick.is_finished() - print(" OK: is_finished + reset_bt cycle") - - # with_telemetry: chainable + bind-failure raises OSError. - with socket.socket() as s: - s.bind(("127.0.0.1", 0)) - port = s.getsockname()[1] - chained = bt.BT(bt.Action("x"), None).with_telemetry(port) - assert chained is not None, "with_telemetry should return self for chaining" - print(f" OK: with_telemetry({port}) chainable") - - try: - bt.BT(bt.Action("x"), None).with_telemetry(port) - except OSError as e: - print(f" OK: with_telemetry on bound port raises OSError ({type(e).__name__})") - else: - raise AssertionError("expected OSError on already-bound port") - - assert bt.BT.__module__ == "bonsai_py", bt.BT.__module__ - print(" OK: BT.__module__ == 'bonsai_py'") - - bb_in = {"x": 1} - identity_bt = bt.BT(bt.Action("noop"), bb_in) - assert identity_bt.blackboard() is bb_in - print(" OK: blackboard() preserves Python object identity") - - sentinel_action = {"k": 42} - sentinel_bb = {"y": 1} - seen = [] - - def identity_cb(args, bbref): - seen.append((args.action is sentinel_action, bbref is sentinel_bb)) - return (bt.Status.Success, 0.0) - - bt.BT(bt.Action(sentinel_action), sentinel_bb).tick(0.0, identity_cb) - assert seen == [(True, True)], seen - print(" OK: action + bb identity preserved through tick callback") - - # WhenAll short-circuit: callback raise on child[1] must NOT invoke child[2]. - order = [] - - def short_circuit_cb(args, _bb): - order.append(args.action) - if args.action == "b": - raise ValueError("stop") - return (bt.Status.Success, 0.0) - - parallel = bt.BT( - bt.WhenAll([bt.Action("a"), bt.Action("b"), bt.Action("c")]), - None, - ) - try: - parallel.tick(0.0, short_circuit_cb) - except ValueError: - pass - assert order == ["a", "b"], f"expected ['a','b'] (short-circuit), got {order}" - print(" OK: WhenAll short-circuits sibling callbacks after a raise") - - # Poisoned BT (after with_telemetry bind failure) refuses subsequent calls. - with socket.socket() as s: - s.bind(("127.0.0.1", 0)) - port2 = s.getsockname()[1] - _holder = bt.BT(bt.Action("x"), None).with_telemetry(port2) # keep alive - victim = bt.BT(bt.Action("y"), None) - try: - victim.with_telemetry(port2) - except OSError: - pass - try: - victim.tick(0.0, lambda _a, _b: (bt.Status.Success, 0.0)) - except RuntimeError as e: - assert "invalidated" in str(e), str(e) - print(" OK: poisoned BT raises RuntimeError on subsequent tick") - else: - raise AssertionError("expected RuntimeError on poisoned BT") - - -def test_type_stubs() -> None: - """Type stub is present, valid syntax, declares every public symbol.""" - import ast - from pathlib import Path - - import bonsai_py - - stub_path = Path(bonsai_py.__file__).parent / "__init__.pyi" - assert stub_path.exists(), f"missing stub: {stub_path}" - print(f" OK: stub present at {stub_path.name}") - - tree = ast.parse(stub_path.read_text()) - print(" OK: stub parses as valid Python syntax") - - declared: set[str] = set() - for node in ast.walk(tree): - if isinstance(node, (ast.ClassDef, ast.FunctionDef)): - declared.add(node.name) - elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): - declared.add(node.target.id) - - expected = set(bonsai_py.__all__) - missing = expected - declared - assert not missing, f"stub missing names: {sorted(missing)}" - print(f" OK: all {len(expected)} __all__ names present in stub") - - # The stub-side __all__ must agree with the package __all__ on the - # full public surface. Drift between the two is the most common cause - # of "mypy sees Status but `from bonsai_py import *` doesn't". - stub_all: set[str] = set() - for node in ast.walk(tree): - if isinstance(node, ast.Assign) and any( - isinstance(t, ast.Name) and t.id == "__all__" for t in node.targets - ): - if isinstance(node.value, ast.List): - stub_all = { - elt.value for elt in node.value.elts if isinstance(elt, ast.Constant) - } - if stub_all: - diff = expected.symmetric_difference(stub_all) - assert not diff, f"package and stub __all__ disagree: {sorted(diff)}" - print(" OK: package and stub __all__ agree") - - -def test_mypy_strict() -> None: - """mypy --strict accepts a sample script using the typed surface.""" - import importlib.util - import os - import subprocess - import sys - import tempfile - - if importlib.util.find_spec("mypy") is None: - print(" SKIP: mypy not installed (pip install mypy to enable)") - return - - sample = ( - "import bonsai_py as bt\n" - "\n" - "def cb(args: bt.ActionArgs, bb: object) -> tuple[bt.Status, float]:\n" - ' if args.action == "inc":\n' - " return (bt.Status.Success, args.dt)\n" - " return bt.RUNNING\n" - "\n" - 'tree = bt.Sequence([bt.Action("inc"), bt.Wait(1)]) # int coerces to float\n' - 'tree_bt = bt.BT(tree, {"count": 0})\n' - "for _ in range(3):\n" - " res: tuple[bt.Status, float] | None = tree_bt.tick(0.5, cb)\n" - " if res is None:\n" - " tree_bt.reset_bt()\n" - "\n" - 'chained: bt.BT = bt.BT(bt.Action("x"), None).with_telemetry(0, host="0.0.0.0")\n' - ) - - with tempfile.NamedTemporaryFile( - mode="w", suffix=".py", delete=False, encoding="utf-8" - ) as f: - f.write(sample) - sample_path = f.name - - try: - result = subprocess.run( - [sys.executable, "-m", "mypy", "--strict", sample_path], - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - raise AssertionError( - f"mypy --strict failed (exit {result.returncode}):\n" - f"--- stdout ---\n{result.stdout}\n" - f"--- stderr ---\n{result.stderr}" - ) - print(" OK: mypy --strict accepts the sample script") - finally: - os.unlink(sample_path) - - -# --------------------------------------------------------------------------- -# Registry — append new test functions above and add them to this list. -# Do not reorder or remove existing entries. -# --------------------------------------------------------------------------- -TESTS = [ - test_module_scaffolding, - test_status_and_action_args, - test_behavior_factories, - test_bt_tick_and_telemetry, - test_type_stubs, - test_mypy_strict, -] - - -def main() -> int: - if len(sys.argv) > 1: - print( - f"usage: {sys.argv[0]} (no arguments; runs every test)", - file=sys.stderr, - ) - return 2 - - for fn in TESTS: - print(f"=== {fn.__name__} ===") - try: - fn() - except AssertionError as e: - print(f" FAIL: {e}", file=sys.stderr) - return 1 - except ImportError as e: - print( - f" FAIL: import error ({e}) — did you run `maturin develop`?", - file=sys.stderr, - ) - return 1 - except Exception as e: # noqa: BLE001 — surface anything unexpected - print(f" FAIL: {type(e).__name__}: {e}", file=sys.stderr) - return 1 - - print(f"\nAll {len(TESTS)} tests passed.") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/bonsai-py/tests/README.md b/bonsai-py/tests/README.md index bd87ba4..3a1bddc 100644 --- a/bonsai-py/tests/README.md +++ b/bonsai-py/tests/README.md @@ -56,7 +56,7 @@ A `pytest-timeout` of 10 seconds per test is configured in [pyproject.toml](../p pip install pytest pytest-timeout ``` -`mypy` is used by [../scripts/verify.py](../scripts/verify.py), not by this suite directly. +`mypy` is used by [test_mypy_strict.py](test_mypy_strict.py) — install with `pip install mypy`. The test is skipped if `mypy` isn't installed. ## CI diff --git a/bonsai-py/tests/test_mypy_strict.py b/bonsai-py/tests/test_mypy_strict.py new file mode 100644 index 0000000..e486425 --- /dev/null +++ b/bonsai-py/tests/test_mypy_strict.py @@ -0,0 +1,61 @@ +"""mypy --strict acceptance test for the typed public surface.""" +from __future__ import annotations + +import importlib.util +import os +import subprocess +import sys +import tempfile + +import pytest + + +def test_mypy_strict_accepts_sample_script() -> None: + """mypy --strict accepts a sample script using bonsai_py's typed surface. + + Catches stub regressions that pytest's runtime tests wouldn't notice — + e.g. a removed annotation, a wrong type in __init__.pyi, or a missing + overload. Runs `mypy --strict` on a generated sample exercising + Status, ActionArgs, Behavior factories, BT.tick, with_telemetry. + """ + if importlib.util.find_spec("mypy") is None: + pytest.skip("mypy not installed (pip install mypy to enable)") + + sample = ( + "import bonsai_py as bt\n" + "\n" + "def cb(args: bt.ActionArgs, bb: object) -> tuple[bt.Status, float]:\n" + ' if args.action == "inc":\n' + " return (bt.Status.Success, args.dt)\n" + " return bt.RUNNING\n" + "\n" + 'tree = bt.Sequence([bt.Action("inc"), bt.Wait(1)]) # int coerces to float\n' + 'tree_bt = bt.BT(tree, {"count": 0})\n' + "for _ in range(3):\n" + " res: tuple[bt.Status, float] | None = tree_bt.tick(0.5, cb)\n" + " if res is None:\n" + " tree_bt.reset_bt()\n" + "\n" + 'chained: bt.BT = bt.BT(bt.Action("x"), None).with_telemetry(0, host="0.0.0.0")\n' + ) + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as f: + f.write(sample) + sample_path = f.name + + try: + result = subprocess.run( + [sys.executable, "-m", "mypy", "--strict", sample_path], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, ( + f"mypy --strict failed (exit {result.returncode}):\n" + f"--- stdout ---\n{result.stdout}\n" + f"--- stderr ---\n{result.stderr}" + ) + finally: + os.unlink(sample_path) From 41259b1b99d0405ccfae09adaed7f68df0e59542 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sun, 24 May 2026 16:10:14 -0500 Subject: [PATCH 23/25] update readme --- README.md | 7 +++++ bonsai-py/README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/README.md b/README.md index 7bea51c..42b6d6b 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ * [Honorable Mentions](#similar-crates) ## Using Bonsai + +### Rust + Bonsai is available on crates.io. The recommended way to use it is to add a line into your Cargo.toml such as: ```toml @@ -36,6 +39,10 @@ Bonsai is available on crates.io. The recommended way to use it is to add a line bonsai-bt = "*" ``` +### Python + +Python bindings are available — see [`bonsai-py/`](bonsai-py/) for installation, examples, and a side-by-side comparison of the same BT in Rust and Python. The package wraps the same Rust crate, so the BT semantics are identical; only the API surface differs. + ## What is a Behavior Tree? A _Behavior Tree_ (BT) is a data structure in which we can set the rules of how certain _behavior's_ can occur, and the order in which they would execute. BTs are a very efficient way of creating complex systems that are both modular and reactive. These properties are crucial in many applications, which has led to the spread of BT from computer game programming to many branches of AI and Robotics. diff --git a/bonsai-py/README.md b/bonsai-py/README.md index c3cc03c..04ec826 100644 --- a/bonsai-py/README.md +++ b/bonsai-py/README.md @@ -14,6 +14,68 @@ maturin develop python -c "import bonsai_py; print(bonsai_py.__version__)" ``` +## Same BT in Rust and Python + +A minimal three-node tree (`Hello → Wait(1.0) → Goodbye`) implemented in both languages. Semantics are identical because the Python package is a thin wrapper around the Rust crate; only the API surface differs (Rust requires an `enum` + explicit types; Python uses any hashable object as the action payload). + +### Rust + +```rust +use bonsai_bt::{Behavior, Event, Status, UpdateArgs, BT}; + +#[derive(Clone, Debug)] +enum Greet { Hello, Goodbye } + +fn main() { + let tree = Behavior::Sequence(vec![ + Behavior::Action(Greet::Hello), + Behavior::Wait(1.0), + Behavior::Action(Greet::Goodbye), + ]); + + let mut bt: BT = BT::new(tree, ()); + + for _ in 0..5 { + let e: Event = UpdateArgs { dt: 0.5 }.into(); + bt.tick(&e, &mut |args, _bb| { + match *args.action { + Greet::Hello => println!("hello"), + Greet::Goodbye => println!("goodbye"), + } + (Status::Success, args.dt) + }); + } +} +``` + +### Python + +```python +import bonsai_py as bt + +tree = bt.Sequence([ + bt.Action("hello"), + bt.Wait(1.0), + bt.Action("goodbye"), +]) + +tree_bt = bt.BT(tree, None) + +def cb(args, _bb): + print(args.action) + return (bt.Status.Success, args.dt) + +for _ in range(5): + tree_bt.tick(0.5, cb) +``` + +Output (both): + + hello + goodbye + +For richer examples — multi-job orchestration, visualizer integration, parallel agents — see [examples/](examples/). + ## License MIT - see [LICENSE](../LICENSE). From 596ec769f6f273d5e3dd7d4cfac4524a2399a41f Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sun, 24 May 2026 17:00:24 -0500 Subject: [PATCH 24/25] rename to bonsai-bt --- bonsai-py/Cargo.toml | 4 ++++ bonsai-py/README.md | 6 +++--- bonsai-py/examples/README.md | 2 +- bonsai-py/examples/async_drone.py | 2 +- bonsai-py/examples/boids_console.py | 2 +- bonsai-py/examples/graphviz_demo.py | 2 +- bonsai-py/examples/race_timeout.py | 2 +- bonsai-py/examples/simple_npc_ai.py | 2 +- bonsai-py/examples/threaded_drone.py | 2 +- bonsai-py/examples/visualizer_smoke.py | 2 +- bonsai-py/pyproject.toml | 4 ++-- .../python/{bonsai_py => bonsai_bt}/__init__.py | 6 +++--- .../python/{bonsai_py => bonsai_bt}/__init__.pyi | 0 bonsai-py/python/{bonsai_py => bonsai_bt}/py.typed | 0 bonsai-py/scripts/README.md | 6 +++--- bonsai-py/scripts/regen-stubs.sh | 4 ++-- bonsai-py/src/action_args.rs | 2 +- bonsai-py/src/behavior.rs | 2 +- bonsai-py/src/bin/stub_gen.rs | 2 +- bonsai-py/src/bt.rs | 2 +- bonsai-py/src/lib.rs | 2 +- bonsai-py/src/status.rs | 2 +- bonsai-py/tests/README.md | 6 +++--- bonsai-py/tests/conftest.py | 2 +- bonsai-py/tests/test_action_args.py | 6 +++--- bonsai-py/tests/test_behavior.py | 6 +++--- bonsai-py/tests/test_bt.py | 6 +++--- bonsai-py/tests/test_drift.py | 14 +++++++------- bonsai-py/tests/test_module.py | 2 +- bonsai-py/tests/test_mypy_strict.py | 4 ++-- bonsai-py/tests/test_performance.py | 2 +- bonsai-py/tests/test_status.py | 6 +++--- bonsai-py/tests/test_telemetry.py | 2 +- bonsai-py/tests/test_threading_and_pickle.py | 2 +- 34 files changed, 60 insertions(+), 56 deletions(-) rename bonsai-py/python/{bonsai_py => bonsai_bt}/__init__.py (76%) rename bonsai-py/python/{bonsai_py => bonsai_bt}/__init__.pyi (100%) rename bonsai-py/python/{bonsai_py => bonsai_bt}/py.typed (100%) mode change 100755 => 100644 bonsai-py/scripts/regen-stubs.sh diff --git a/bonsai-py/Cargo.toml b/bonsai-py/Cargo.toml index c2009bf..844f5be 100644 --- a/bonsai-py/Cargo.toml +++ b/bonsai-py/Cargo.toml @@ -11,6 +11,10 @@ homepage = "https://github.com/sollimann/bonsai" publish = false [lib] +# Internal Rust crate name — kept as `bonsai_py` to avoid colliding with the +# workspace's `bonsai-bt` crate at `bonsai/`, which also produces `libbonsai_bt.rlib`. +# The Python-facing module name is controlled separately by +# `[tool.maturin] module-name = "bonsai_bt"` in pyproject.toml. name = "bonsai_py" crate-type = ["cdylib", "rlib"] diff --git a/bonsai-py/README.md b/bonsai-py/README.md index 04ec826..d935c9a 100644 --- a/bonsai-py/README.md +++ b/bonsai-py/README.md @@ -1,4 +1,4 @@ -# bonsai-py - Python bindings +# bonsai-bt - Python bindings Python bindings for the [bonsai-bt](https://github.com/sollimann/bonsai) behavior-tree library. @@ -11,7 +11,7 @@ source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1 pip install maturin cd bonsai-py maturin develop -python -c "import bonsai_py; print(bonsai_py.__version__)" +python -c "import bonsai_bt; print(bonsai_bt.__version__)" ``` ## Same BT in Rust and Python @@ -51,7 +51,7 @@ fn main() { ### Python ```python -import bonsai_py as bt +import bonsai_bt as bt tree = bt.Sequence([ bt.Action("hello"), diff --git a/bonsai-py/examples/README.md b/bonsai-py/examples/README.md index bd7441d..74ec2ae 100644 --- a/bonsai-py/examples/README.md +++ b/bonsai-py/examples/README.md @@ -69,7 +69,7 @@ python bonsai-py/examples/threaded_drone.py ``` ### [async_drone.py](async_drone.py) — multi-job mission (asyncio) -Same mission and tree as `threaded_drone.py`, but background jobs are `async def` coroutines on a single asyncio event loop, communicating via `asyncio.Queue`. **Pick this variant when actions are awaitable** (`aiohttp`, async DB drivers, websockets) — N-way concurrency without OS thread overhead. +Same mission and tree as `threaded_drone.py`, but background jobs are `async def` coroutines on a single asyncio event loop, communicating via `asyncio.Queue`. **Pick this variant when actions are awaitable** (`aiohttp`, async DB drivers, websockets) — N-way concurrency without OS thread overhead. ```bash python bonsai-py/examples/async_drone.py diff --git a/bonsai-py/examples/async_drone.py b/bonsai-py/examples/async_drone.py index 32bf3e6..c63ddde 100644 --- a/bonsai-py/examples/async_drone.py +++ b/bonsai-py/examples/async_drone.py @@ -49,7 +49,7 @@ from dataclasses import dataclass from typing import Any, Awaitable, Callable, Optional -import bonsai_py as bt +import bonsai_bt as bt MAX_WALL_SECONDS = 8.0 # Demo cap; the BT itself would loop forever. diff --git a/bonsai-py/examples/boids_console.py b/bonsai-py/examples/boids_console.py index 2fbda88..0d8370c 100644 --- a/bonsai-py/examples/boids_console.py +++ b/bonsai-py/examples/boids_console.py @@ -32,7 +32,7 @@ from dataclasses import dataclass from typing import Any -import bonsai_py as bt +import bonsai_bt as bt NUM_BOIDS = 10 WIDTH = 1280.0 diff --git a/bonsai-py/examples/graphviz_demo.py b/bonsai-py/examples/graphviz_demo.py index f7fa828..f9745fb 100644 --- a/bonsai-py/examples/graphviz_demo.py +++ b/bonsai-py/examples/graphviz_demo.py @@ -16,7 +16,7 @@ from dataclasses import dataclass -import bonsai_py as bt +import bonsai_bt as bt # Payload-less actions are plain strings; payload variants are frozen # dataclasses (hashable, immutable, work as bt.Action(...) values). diff --git a/bonsai-py/examples/race_timeout.py b/bonsai-py/examples/race_timeout.py index c3ed775..f1682c0 100644 --- a/bonsai-py/examples/race_timeout.py +++ b/bonsai-py/examples/race_timeout.py @@ -31,7 +31,7 @@ from dataclasses import dataclass, field from typing import Any -import bonsai_py as bt +import bonsai_bt as bt class MissionAction(enum.Enum): diff --git a/bonsai-py/examples/simple_npc_ai.py b/bonsai-py/examples/simple_npc_ai.py index fc0deab..1c858e4 100644 --- a/bonsai-py/examples/simple_npc_ai.py +++ b/bonsai-py/examples/simple_npc_ai.py @@ -22,7 +22,7 @@ from dataclasses import dataclass from typing import Any -import bonsai_py as bt +import bonsai_bt as bt class EnemyNPC(enum.Enum): diff --git a/bonsai-py/examples/threaded_drone.py b/bonsai-py/examples/threaded_drone.py index b9f387e..21040a8 100644 --- a/bonsai-py/examples/threaded_drone.py +++ b/bonsai-py/examples/threaded_drone.py @@ -49,7 +49,7 @@ from dataclasses import dataclass from typing import Any, Optional -import bonsai_py as bt +import bonsai_bt as bt MAX_WALL_SECONDS = 8.0 # Demo cap; the BT itself would loop forever. diff --git a/bonsai-py/examples/visualizer_smoke.py b/bonsai-py/examples/visualizer_smoke.py index 4427bb3..be1de5a 100644 --- a/bonsai-py/examples/visualizer_smoke.py +++ b/bonsai-py/examples/visualizer_smoke.py @@ -28,7 +28,7 @@ import time from typing import Any -from bonsai_py import * # noqa: F401,F403 +from bonsai_bt import * # noqa: F401,F403 def build_tree() -> Behavior: diff --git a/bonsai-py/pyproject.toml b/bonsai-py/pyproject.toml index 968877c..2886f74 100644 --- a/bonsai-py/pyproject.toml +++ b/bonsai-py/pyproject.toml @@ -3,7 +3,7 @@ requires = ["maturin>=1.7,<2.0"] build-backend = "maturin" [project] -name = "bonsai-py" +name = "bonsai-bt" description = "Behavior trees in Python, powered by the bonsai-bt Rust crate." readme = "README.md" license = { text = "MIT" } @@ -35,7 +35,7 @@ Repository = "https://github.com/sollimann/bonsai" Issues = "https://github.com/sollimann/bonsai/issues" [tool.maturin] -module-name = "bonsai_py" +module-name = "bonsai_bt" python-source = "python" features = ["pyo3/extension-module"] strip = true diff --git a/bonsai-py/python/bonsai_py/__init__.py b/bonsai-py/python/bonsai_bt/__init__.py similarity index 76% rename from bonsai-py/python/bonsai_py/__init__.py rename to bonsai-py/python/bonsai_bt/__init__.py index fe3d91d..f7b06ed 100644 --- a/bonsai-py/python/bonsai_py/__init__.py +++ b/bonsai-py/python/bonsai_bt/__init__.py @@ -1,13 +1,13 @@ -"""bonsai-py - behavior trees in Python, powered by the Rust bonsai-bt crate.""" +"""bonsai-bt - behavior trees in Python, powered by the Rust bonsai-bt crate.""" from importlib.metadata import PackageNotFoundError, version as _version try: - __version__ = _version("bonsai-py") + __version__ = _version("bonsai-bt") except PackageNotFoundError: # editable install before metadata is in place __version__ = "0.0.0+unknown" -from .bonsai_py import * # noqa: F401,F403 (re-export the compiled module) +from .bonsai_bt import * # noqa: F401,F403 (re-export the compiled module) __all__ = [ # types diff --git a/bonsai-py/python/bonsai_py/__init__.pyi b/bonsai-py/python/bonsai_bt/__init__.pyi similarity index 100% rename from bonsai-py/python/bonsai_py/__init__.pyi rename to bonsai-py/python/bonsai_bt/__init__.pyi diff --git a/bonsai-py/python/bonsai_py/py.typed b/bonsai-py/python/bonsai_bt/py.typed similarity index 100% rename from bonsai-py/python/bonsai_py/py.typed rename to bonsai-py/python/bonsai_bt/py.typed diff --git a/bonsai-py/scripts/README.md b/bonsai-py/scripts/README.md index 66b1cb1..fde355a 100644 --- a/bonsai-py/scripts/README.md +++ b/bonsai-py/scripts/README.md @@ -1,16 +1,16 @@ # bonsai-py scripts -Developer scripts for working on the `bonsai_py` extension module. +Developer scripts for working on the `bonsai_bt` extension module. ## Scripts | Script | Purpose | |---|---| -| [regen-stubs.sh](regen-stubs.sh) | Regenerates `python/bonsai_py/__init__.pyi` from the `#[gen_stub_*]` annotations on the Rust side. Run after editing any annotated `#[pyclass]` / `#[pyfunction]` / `#[pymethods]`. Also runs automatically via the `regen-stubs` pre-commit hook and is enforced in CI. | +| [regen-stubs.sh](regen-stubs.sh) | Regenerates `python/bonsai_bt/__init__.pyi` from the `#[gen_stub_*]` annotations on the Rust side. Run after editing any annotated `#[pyclass]` / `#[pyfunction]` / `#[pymethods]`. Also runs automatically via the `regen-stubs` pre-commit hook and is enforced in CI. | ## Prerequisites -A Python venv with the `bonsai_py` extension built in. See [../README.md](../README.md#installation-dev) for the one-time setup (`python -m venv .venv`, activate, `pip install maturin`, `maturin develop --release`). +A Python venv with the `bonsai_bt` extension built in. See [../README.md](../README.md#installation-dev) for the one-time setup (`python -m venv .venv`, activate, `pip install maturin`, `maturin develop --release`). ## Running diff --git a/bonsai-py/scripts/regen-stubs.sh b/bonsai-py/scripts/regen-stubs.sh old mode 100755 new mode 100644 index a1dce0d..019efaf --- a/bonsai-py/scripts/regen-stubs.sh +++ b/bonsai-py/scripts/regen-stubs.sh @@ -9,8 +9,8 @@ cd "$(dirname "$0")/.." # cd into bonsai-py/ cargo run --quiet --bin stub_gen -p bonsai-py # pyo3-stub-gen 0.22 reads pyproject.toml and writes the stub to -# python/bonsai_py/__init__.pyi automatically. -STUB=python/bonsai_py/__init__.pyi +# python/bonsai_bt/__init__.pyi automatically. +STUB=python/bonsai_bt/__init__.pyi if [ ! -f "$STUB" ]; then echo "ERROR: $STUB was not generated. Did the binary fail silently?" >&2 exit 1 diff --git a/bonsai-py/src/action_args.rs b/bonsai-py/src/action_args.rs index 57af8a0..63c9619 100644 --- a/bonsai-py/src/action_args.rs +++ b/bonsai-py/src/action_args.rs @@ -10,7 +10,7 @@ use crate::behavior::PyAction; /// The Rust `ActionArgs::event` field is intentionally not exposed — /// Python users only see `dt` and `action`. #[gen_stub_pyclass] -#[pyclass(frozen, module = "bonsai_py", name = "ActionArgs")] +#[pyclass(frozen, module = "bonsai_bt", name = "ActionArgs")] pub struct PyActionArgs { /// Remaining delta time in seconds. #[pyo3(get)] diff --git a/bonsai-py/src/behavior.rs b/bonsai-py/src/behavior.rs index e8b1efd..bf38740 100644 --- a/bonsai-py/src/behavior.rs +++ b/bonsai-py/src/behavior.rs @@ -36,7 +36,7 @@ impl std::fmt::Debug for PyAction { /// at the module level. Subtrees are reusable - the same `Behavior` /// can appear as a child of multiple parents. #[gen_stub_pyclass] -#[pyclass(unsendable, frozen, module = "bonsai_py", name = "Behavior")] +#[pyclass(unsendable, frozen, module = "bonsai_bt", name = "Behavior")] pub struct PyBehavior { pub(crate) inner: Behavior, } diff --git a/bonsai-py/src/bin/stub_gen.rs b/bonsai-py/src/bin/stub_gen.rs index 773fff8..96f2274 100644 --- a/bonsai-py/src/bin/stub_gen.rs +++ b/bonsai-py/src/bin/stub_gen.rs @@ -3,7 +3,7 @@ //! //! Run with `cargo run --bin stub_gen -p bonsai-py`. pyo3-stub-gen reads //! `pyproject.toml` to determine the package layout and writes the stub -//! to `python/bonsai_py/__init__.pyi`. The companion `scripts/regen-stubs.sh` +//! to `python/bonsai_bt/__init__.pyi`. The companion `scripts/regen-stubs.sh` //! appends the manual `RUNNING` constant declaration afterwards. use pyo3_stub_gen::Result; diff --git a/bonsai-py/src/bt.rs b/bonsai-py/src/bt.rs index 1bf389f..7baa048 100644 --- a/bonsai-py/src/bt.rs +++ b/bonsai-py/src/bt.rs @@ -14,7 +14,7 @@ const POISONED_MSG: &str = "BT was invalidated by a failed with_telemetry call; /// Construct from a tree and a blackboard, then drive with `.tick(dt, callback)`. /// The callback receives `(args, blackboard)` and must return `(Status, float)`. #[gen_stub_pyclass] -#[pyclass(unsendable, module = "bonsai_py", name = "BT")] +#[pyclass(unsendable, module = "bonsai_bt", name = "BT")] pub struct PyBT { inner: Option>>, } diff --git a/bonsai-py/src/lib.rs b/bonsai-py/src/lib.rs index e2459a9..4390fca 100644 --- a/bonsai-py/src/lib.rs +++ b/bonsai-py/src/lib.rs @@ -19,7 +19,7 @@ use status::PyStatus; /// Construct trees with the factory functions (Sequence, Action, Wait, ...), /// wrap one in `BT(tree, blackboard)`, and drive it with `bt.tick(dt, callback)`. #[pymodule] -fn bonsai_py(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { +fn bonsai_bt(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/bonsai-py/src/status.rs b/bonsai-py/src/status.rs index deb80bb..cf0c8da 100644 --- a/bonsai-py/src/status.rs +++ b/bonsai-py/src/status.rs @@ -8,7 +8,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass_enum, gen_stub_pymethods}; /// (`Status.Success == 0`, `Failure == 1`, `Running == 2`) and usable /// as a `dict` key or `set` member. #[gen_stub_pyclass_enum] -#[pyclass(eq, eq_int, hash, frozen, from_py_object, module = "bonsai_py", name = "Status")] +#[pyclass(eq, eq_int, hash, frozen, from_py_object, module = "bonsai_bt", name = "Status")] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub enum PyStatus { Success, diff --git a/bonsai-py/tests/README.md b/bonsai-py/tests/README.md index 3a1bddc..a6ca0a0 100644 --- a/bonsai-py/tests/README.md +++ b/bonsai-py/tests/README.md @@ -1,6 +1,6 @@ # bonsai-py tests -Pytest suite for the `bonsai_py` extension module. Primarily used to prevent any drift between the Rust bindings (`bonsai-py/src/*.rs`) and the Python surface: every `#[pyclass]`, `#[pymethods]`, and `#[pyfunction]` has at least one Python test exercising it. +Pytest suite for the `bonsai_bt` extension module. Primarily used to prevent any drift between the Rust bindings (`bonsai-py/src/*.rs`) and the Python surface: every `#[pyclass]`, `#[pymethods]`, and `#[pyfunction]` has at least one Python test exercising it. ## Test files @@ -15,11 +15,11 @@ Pytest suite for the `bonsai_py` extension module. Primarily used to prevent any | [test_module.py](test_module.py) | `__version__ == "0.12.0"`; module docstring present; `__all__` lists exactly the 19 expected names; all names accessible; `RUNNING` is `(Status.Running, 0.0)`; `.pyi` stub ships with the wheel. | | [test_threading_and_pickle.py](test_threading_and_pickle.py) | `BT` is unsendable across threads (PyO3 `PanicException`); `BT`, `Behavior`, `ActionArgs` are unpicklable; `Status` IS picklable (multiprocessing-friendly). | | [test_performance.py](test_performance.py) | `@pytest.mark.perf`: 100 ticks under 500 ms, 1000 constructions under 5 s. `@pytest.mark.bench` (opt-in): tick throughput microbenchmark. | -| [test_drift.py](test_drift.py) | Checks that every Rust `#[pyo3(name=...)]` symbol appears in `__all__`; every `#[pyclass(name=..., module='bonsai_py')]` appears in `__all__`; every name in `__all__` is mentioned in at least one other test file. | +| [test_drift.py](test_drift.py) | Checks that every Rust `#[pyo3(name=...)]` symbol appears in `__all__`; every `#[pyclass(name=..., module='bonsai_bt')]` appears in `__all__`; every name in `__all__` is mentioned in at least one other test file. | ## Prerequisites -A Python venv with the `bonsai_py` extension built in. See [../README.md](../README.md#installation-dev) for the one-time setup (`python -m venv .venv`, activate, `pip install maturin`, `maturin develop --release`). +A Python venv with the `bonsai_bt` extension built in. See [../README.md](../README.md#installation-dev) for the one-time setup (`python -m venv .venv`, activate, `pip install maturin`, `maturin develop --release`). ## Running diff --git a/bonsai-py/tests/conftest.py b/bonsai-py/tests/conftest.py index dd7cf54..e828138 100644 --- a/bonsai-py/tests/conftest.py +++ b/bonsai-py/tests/conftest.py @@ -6,7 +6,7 @@ import pytest -import bonsai_py as bt +import bonsai_bt as bt @pytest.fixture diff --git a/bonsai-py/tests/test_action_args.py b/bonsai-py/tests/test_action_args.py index e23c269..33bfb48 100644 --- a/bonsai-py/tests/test_action_args.py +++ b/bonsai-py/tests/test_action_args.py @@ -5,7 +5,7 @@ import pytest -import bonsai_py as bt +import bonsai_bt as bt class TestActionArgsConstruction: @@ -78,5 +78,5 @@ def test_repr_format(self, dt: float, action: object, expected: str) -> None: class TestActionArgsModuleAttribution: def test_module(self) -> None: - """ActionArgs.__module__ is `bonsai_py` (required for pickle / introspection).""" - assert bt.ActionArgs.__module__ == "bonsai_py" + """ActionArgs.__module__ is `bonsai_bt` (required for pickle / introspection).""" + assert bt.ActionArgs.__module__ == "bonsai_bt" diff --git a/bonsai-py/tests/test_behavior.py b/bonsai-py/tests/test_behavior.py index 347e9cb..d2bd32e 100644 --- a/bonsai-py/tests/test_behavior.py +++ b/bonsai-py/tests/test_behavior.py @@ -5,7 +5,7 @@ import pytest -import bonsai_py as bt +import bonsai_bt as bt FACTORY_NAMES = ( "Action", "Wait", "WaitForever", @@ -184,8 +184,8 @@ def test_identity_based_eq_per_variant( class TestBehaviorAttribution: def test_module(self) -> None: - """Behavior.__module__ is `bonsai_py` (required for pickle / introspection).""" - assert bt.Behavior.__module__ == "bonsai_py" + """Behavior.__module__ is `bonsai_bt` (required for pickle / introspection).""" + assert bt.Behavior.__module__ == "bonsai_bt" # ---------- Ports of Rust behavior_tests.rs (golden-truth equivalence) ---------- diff --git a/bonsai-py/tests/test_bt.py b/bonsai-py/tests/test_bt.py index beba26f..dd72140 100644 --- a/bonsai-py/tests/test_bt.py +++ b/bonsai-py/tests/test_bt.py @@ -5,7 +5,7 @@ import pytest -import bonsai_py as bt +import bonsai_bt as bt class TestBTConstruction: @@ -30,8 +30,8 @@ def __init__(self) -> None: assert b.blackboard() is s def test_module(self) -> None: - """BT.__module__ is `bonsai_py` (required for pickle / introspection).""" - assert bt.BT.__module__ == "bonsai_py" + """BT.__module__ is `bonsai_bt` (required for pickle / introspection).""" + assert bt.BT.__module__ == "bonsai_bt" class TestTick: diff --git a/bonsai-py/tests/test_drift.py b/bonsai-py/tests/test_drift.py index b41bebf..911de4f 100644 --- a/bonsai-py/tests/test_drift.py +++ b/bonsai-py/tests/test_drift.py @@ -2,9 +2,9 @@ Two parity checks: 1. Every `#[pyfunction]` in `bonsai-py/src/*.rs` (with a `#[pyo3(name = "X")]` - override) appears in `bonsai_py.__all__`. -2. Every public `#[pyclass]` (with `module = "bonsai_py"` set) appears in - `bonsai_py.__all__`. + override) appears in `bonsai_bt.__all__`. +2. Every public `#[pyclass]` (with `module = "bonsai_bt"` set) appears in + `bonsai_bt.__all__`. If a Rust contributor adds a binding and forgets to: - add it to __all__, @@ -16,7 +16,7 @@ import re from pathlib import Path -import bonsai_py as bt +import bonsai_bt as bt SRC_DIR = Path(__file__).resolve().parent.parent / "src" @@ -34,18 +34,18 @@ def _names_from_rust(pattern: re.Pattern[str]) -> set[str]: def test_every_rust_pyfunction_in_all() -> None: - """Every Rust #[pyo3(name=...)] symbol scanned from src/*.rs must appear in bonsai_py.__all__.""" + """Every Rust #[pyo3(name=...)] symbol scanned from src/*.rs must appear in bonsai_bt.__all__.""" rust_names = _names_from_rust(RUST_NAME_RE) rust_names = {n for n in rust_names if not n.startswith("_")} missing = rust_names - set(bt.__all__) assert not missing, ( f"Rust declares these #[pyo3(name=...)] symbols but they're missing " - f"from bonsai_py.__all__: {sorted(missing)}." + f"from bonsai_bt.__all__: {sorted(missing)}." ) def test_every_rust_pyclass_in_all() -> None: - """Every Rust #[pyclass(name=...)] must appear in bonsai_py.__all__.""" + """Every Rust #[pyclass(name=...)] must appear in bonsai_bt.__all__.""" rust_classes = _names_from_rust(PYCLASS_NAME_RE) missing = rust_classes - set(bt.__all__) assert not missing, ( diff --git a/bonsai-py/tests/test_module.py b/bonsai-py/tests/test_module.py index eeab305..b03fcda 100644 --- a/bonsai-py/tests/test_module.py +++ b/bonsai-py/tests/test_module.py @@ -1,7 +1,7 @@ """Module-level surface: __version__, __all__, __doc__, RUNNING.""" from __future__ import annotations -import bonsai_py as bt +import bonsai_bt as bt def test_version_present() -> None: diff --git a/bonsai-py/tests/test_mypy_strict.py b/bonsai-py/tests/test_mypy_strict.py index e486425..438da50 100644 --- a/bonsai-py/tests/test_mypy_strict.py +++ b/bonsai-py/tests/test_mypy_strict.py @@ -11,7 +11,7 @@ def test_mypy_strict_accepts_sample_script() -> None: - """mypy --strict accepts a sample script using bonsai_py's typed surface. + """mypy --strict accepts a sample script using bonsai_bt's typed surface. Catches stub regressions that pytest's runtime tests wouldn't notice — e.g. a removed annotation, a wrong type in __init__.pyi, or a missing @@ -22,7 +22,7 @@ def test_mypy_strict_accepts_sample_script() -> None: pytest.skip("mypy not installed (pip install mypy to enable)") sample = ( - "import bonsai_py as bt\n" + "import bonsai_bt as bt\n" "\n" "def cb(args: bt.ActionArgs, bb: object) -> tuple[bt.Status, float]:\n" ' if args.action == "inc":\n' diff --git a/bonsai-py/tests/test_performance.py b/bonsai-py/tests/test_performance.py index 7f8e03c..69c689a 100644 --- a/bonsai-py/tests/test_performance.py +++ b/bonsai-py/tests/test_performance.py @@ -7,7 +7,7 @@ import pytest -import bonsai_py as bt +import bonsai_bt as bt @pytest.mark.perf diff --git a/bonsai-py/tests/test_status.py b/bonsai-py/tests/test_status.py index 3f3e373..56a13dc 100644 --- a/bonsai-py/tests/test_status.py +++ b/bonsai-py/tests/test_status.py @@ -6,7 +6,7 @@ import pytest -import bonsai_py as bt +import bonsai_bt as bt class TestStatusSemantics: @@ -46,8 +46,8 @@ def test_repr(self) -> None: assert repr(bt.Status.Running) == "Status.Running" def test_module_attribution(self) -> None: - """`module = "bonsai_py"` is set on the pyclass — required for pickle.""" - assert bt.Status.__module__ == "bonsai_py" + """`module = "bonsai_bt"` is set on the pyclass — required for pickle.""" + assert bt.Status.__module__ == "bonsai_bt" class TestStatusPickle: diff --git a/bonsai-py/tests/test_telemetry.py b/bonsai-py/tests/test_telemetry.py index 94d539a..5fda7fe 100644 --- a/bonsai-py/tests/test_telemetry.py +++ b/bonsai-py/tests/test_telemetry.py @@ -5,7 +5,7 @@ import pytest -import bonsai_py as bt +import bonsai_bt as bt class TestWithTelemetry: diff --git a/bonsai-py/tests/test_threading_and_pickle.py b/bonsai-py/tests/test_threading_and_pickle.py index ba46fed..19c2a33 100644 --- a/bonsai-py/tests/test_threading_and_pickle.py +++ b/bonsai-py/tests/test_threading_and_pickle.py @@ -6,7 +6,7 @@ import pytest -import bonsai_py as bt +import bonsai_bt as bt class TestBTUnsendable: From 0baad2834b7a307ff0d2c2ccdb6bfab9bb4d6f03 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Sun, 24 May 2026 17:09:58 -0500 Subject: [PATCH 25/25] fix error --- .github/workflows/python-wheels.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index b94ac24..f2fa2db 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -87,6 +87,7 @@ jobs: pip install --upgrade pip pip install pytest pytest-timeout mypy pip install bonsai-py/dist/*.whl + python -c "import bonsai_bt; print(bonsai_bt.__version__)" pytest bonsai-py/tests/ - name: Wheel size sanity (Linux/macOS only) @@ -145,7 +146,7 @@ jobs: source .venv-sdist/bin/activate pip install --upgrade pip pip install --no-binary :all: dist/*.tar.gz - python -c "import bonsai_py; print(bonsai_py.__version__)" + python -c "import bonsai_bt; print(bonsai_bt.__version__)" publish: name: Publish to PyPI / TestPyPI

ZJO}SCct}|Io9Ue*FFnRYcFY+ZQ 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 literal 0 HcmV?d00001 diff --git a/bonsai-py/python/bonsai_py/py.typed b/bonsai-py/python/bonsai_py/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/bonsai-py/src/lib.rs b/bonsai-py/src/lib.rs new file mode 100644 index 0000000..34d6cec --- /dev/null +++ b/bonsai-py/src/lib.rs @@ -0,0 +1,10 @@ +use pyo3::prelude::*; + +/// Python bindings for the bonsai-bt behavior-tree library. +/// +/// Construct trees with the factory functions (Sequence, Action, Wait, ...), +/// wrap one in `BT(tree, blackboard)`, and drive it with `bt.tick(dt, callback)`. +#[pymodule] +fn bonsai_py(_py: Python<'_>, _m: &Bound<'_, PyModule>) -> PyResult<()> { + Ok(()) +} From 12566369071418000f91d62d100ff8b75f0e9f74 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Fri, 15 May 2026 19:27:18 -0500 Subject: [PATCH 02/25] add missing items in gitignore --- .claude/claude_plan.md | 716 ++++++++++++++++++ .claude/settings.local.json | 7 + .gitignore | 9 + .../__pycache__/__init__.cpython-312.pyc | Bin 522 -> 0 bytes bonsai-py/python/bonsai_py/bonsai_py.abi3.so | Bin 512888 -> 0 bytes 5 files changed, 732 insertions(+) create mode 100644 .claude/claude_plan.md create mode 100644 .claude/settings.local.json delete mode 100644 bonsai-py/python/bonsai_py/__pycache__/__init__.cpython-312.pyc delete mode 100755 bonsai-py/python/bonsai_py/bonsai_py.abi3.so diff --git a/.claude/claude_plan.md b/.claude/claude_plan.md new file mode 100644 index 0000000..e249a30 --- /dev/null +++ b/.claude/claude_plan.md @@ -0,0 +1,716 @@ +# Plan: Python bindings for `bonsai-bt` via PyO3 + +## Context + +`bonsai-bt` is a pure-Rust behavior-tree library ([bonsai/src/lib.rs](bonsai/src/lib.rs)). Its core surface is small — a recursive `Behavior` enum, a `BT` executor with a `tick()` method that takes a user closure, plus an optional WebSocket visualizer attached via `BT::with_telemetry(port)` (see [bonsai/src/bt_telemetry.rs:94](bonsai/src/bt_telemetry.rs#L94)). There are no existing bindings, no C deps, and `BT` carries no lifetimes — a clean fit for PyO3. + +Goal: expose the full library to Python so users can build behavior trees, tick them with a Python callback, and open the same browser visualizer. Action and blackboard types will be arbitrary `PyObject`s so Python users aren't constrained to a fixed shape. The crate will be a new workspace member and ship as a published wheel on PyPI. + +## Resolved decisions + +| Decision | Choice | +|---|---| +| Action type in Python | Arbitrary `PyObject` (strings, dicts, dataclasses — anything) | +| Blackboard type | Arbitrary `PyObject` (live reference, mutations persist) | +| Visualizer (`with_telemetry`) | Included via `visualize` feature | +| Crate location | New workspace member `bonsai-py/` | +| Distribution | Full PyPI pipeline (wheels + sdist on tag) | +| PyPI dist name | `bonsai-py` (Python import is `bonsai_py` — PyPI's automatic hyphen→underscore mapping) | +| `BT.with_telemetry(port)` | Returns `self` (chainable); two-line usage also documented | +| `ActionArgs` fields | `dt: float`, `action: Any` only (no `event`) | +| Execution mode | Sequential — each section completes & is reviewed before the next | +| Python version floor | 3.10+ via `abi3-py310` | + +## Crate layout + +Add a third workspace member `bonsai-py/` (sibling of `bonsai/` and `examples/`). Update [Cargo.toml](Cargo.toml) workspace `members` to `["bonsai", "examples", "bonsai-py"]`. + +``` +bonsai-py/ +├── Cargo.toml # cdylib, pyo3 + bonsai-bt deps +├── pyproject.toml # maturin build backend, package metadata +├── README.md +├── src/ +│ ├── lib.rs # #[pymodule] root, registers classes/functions +│ ├── behavior.rs # PyBehavior + factory functions +│ ├── bt.rs # PyBT class wrapping BT +│ ├── status.rs # PyStatus enum +│ └── action_args.rs # PyActionArgs passed to user callback +├── python/bonsai_py/ +│ ├── __init__.py # re-export from the compiled module +│ └── py.typed # PEP 561 marker +├── stubs/bonsai_py.pyi # type stubs for IDE/mypy +├── tests/test_bt.py # pytest suite +└── examples/visualizer_smoke.py +``` + +Key dependency choices in `bonsai-py/Cargo.toml`: +- `pyo3 = { version = "0.28", features = ["extension-module", "abi3-py310"] }` +- `bonsai-bt = { path = "../bonsai", version = "0.12", features = ["visualize"] }` +- `[lib] crate-type = ["cdylib"]` + +## Rust → Python type mapping + +| Rust type | Python exposure | +|--------------------------------------|-------------------------------------------------------------------| +| `Behavior` | `Behavior` opaque `#[pyclass]`; built via factory funcs | +| `BT` | `BT` `#[pyclass(unsendable)]` — PyObject is not `Send` | +| `Status` (Success/Failure/Running) | `Status` `#[pyclass(eq, eq_int)]` enum | +| `ActionArgs<'_, Event, PyObject>` | `ActionArgs` `#[pyclass]` with `.dt`, `.action` (no `.event`) | +| `with_telemetry(port) -> io::Result` | `BT.with_telemetry(port)` chainable, raises `OSError` on bind fail | + +**Why `unsendable`:** `PyObject` isn't `Send`. `#[pyclass(unsendable)]` tells PyO3 to panic if `BT` crosses thread boundaries — safe because all calls happen under the GIL. + +## Python API surface (target shape) + +```python +import bonsai_py as bt + +tree = bt.Sequence([ + bt.Wait(1.0), + bt.Action("inc"), + bt.If(bt.Action("low_hp"), + bt.AlwaysSucceed(bt.Action("flee")), + bt.Action("regroup")), + bt.While(bt.Action("has_ammo"), + [bt.Action("fire"), bt.Wait(0.3)]), +]) + +# Both styles work — with_telemetry returns self: +tree_bt = bt.BT(tree, {"count": 0}).with_telemetry(8910) +# or: +tree_bt = bt.BT(tree, {"count": 0}) +tree_bt.with_telemetry(8910) + +def on_action(args, bb): + if args.action == "inc": + bb["count"] += 1 + return (bt.Status.Success, args.dt) + return (bt.Status.Running, 0.0) + +status, remaining_dt = tree_bt.tick(dt=0.5, callback=on_action) +print(tree_bt.blackboard()["count"], tree_bt.is_finished()) +tree_bt.reset_bt() +``` + +--- + +# Agent-ready sections + +Sections are designed for sequential execution. Each is self-contained: a downstream agent reading only its own section plus the **shared overview above** has everything needed. Acceptance criteria define the handoff to the next section. + +--- + +## Section 1 — Scaffolding & build setup + +**Goal.** Stand up `bonsai-py/` so that `maturin develop` produces an importable (empty) Python module. + +**Depends on:** nothing. + +### Resolved decisions (Section-1 scope) + +| Decision | Choice | Rationale | +|---|---|---| +| Python version floor | `abi3-py310` | Python 3.8/3.9 EOL by 2026; py310 lets us use modern typing in stubs in §5. | +| PyO3 version | `0.28` (caret pin, latest stable confirmed via `cargo search pyo3` on 2026-05-15) | Mature `Bound<>` API; matches Section 4's tick-bridge sketch verbatim. | +| bonsai-bt dep style | Dual `path = "../bonsai", version = "0.12"` | Local builds use path; sdist consumers resolve from crates.io. **Requires `bonsai-bt 0.12.x` published to crates.io before any sdist upload.** | +| `f32` feature | Not exposed | Python floats are 64-bit; enabling f32 would silently lose precision. | +| `visualize` feature | Always enabled in `bonsai-py/Cargo.toml` | Resolved upstream; keeps the Python wheel self-contained. | +| Single name across layers | Cargo package = `bonsai-py`; PyPI distribution = `bonsai-py`; Python import = `bonsai_py` | The PyPI↔import hyphen↔underscore mapping is automatic across the Python ecosystem; users still type one name. | +| `[lib].name` | `bonsai_py` | Controls the produced `.so`/`.pyd` filename; must match the `#[pymodule]` function name and pyproject `module-name`. | +| License | MIT | Mirrors the parent crate. | +| Publish to crates.io? | No (`publish = false`) | The binding crate is built into wheels, not distributed via crates.io. | + +### Files to create + +Exact file contents follow — paste as-is. + +#### `bonsai-py/Cargo.toml` + +```toml +[package] +name = "bonsai-py" +version = "0.12.0" +edition = "2021" +rust-version = "1.80.0" +description = "Python bindings for the bonsai-bt behavior tree library" +license = "MIT" +authors = [ + "Kristoffer Solberg Rakstad ", + "Anmol Kathail ", +] +repository = "https://github.com/sollimann/bonsai.git" +homepage = "https://github.com/sollimann/bonsai" +publish = false + +[lib] +name = "bonsai_py" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.28", features = ["extension-module", "abi3-py310"] } +bonsai-bt = { path = "../bonsai", version = "0.12", features = ["visualize"] } +``` + +Notes on each line worth understanding: +- `name = "bonsai-py"` is the **Cargo package name** and also the **PyPI distribution name** (set in `pyproject.toml` below). Python imports replace the hyphen with an underscore, giving `import bonsai_py`. This is the universal PyPI↔import convention — users only learn one name. +- `[lib].name = "bonsai_py"` controls the compiled artifact filename. PyO3 requires this to match the `#[pymodule]` function name; maturin requires it to match `tool.maturin.module-name`. All three must be **`bonsai_py`** (underscored) for `import bonsai_py` to find `PyInit_bonsai_py`. +- `crate-type = ["cdylib"]` is what produces the dynamic library Python loads. Not `rlib`, not `dylib`. +- `pyo3` features: + - `extension-module` tells PyO3 we're building a Python C extension (skips linking libpython, needed on Linux for portable wheels). + - `abi3-py310` enables the stable ABI floor at Python 3.10. One built wheel works for 3.10, 3.11, 3.12, 3.13, and future 3.x versions. +- `bonsai-bt = { path = ..., version = "0.12" }` — the **dual** form is load-bearing. With path only, Cargo refuses to package (sdist build fails). With version only, local dev wouldn't pick up uncommitted changes in `../bonsai/`. Both together: local cargo uses path; published sdist falls back to crates.io. + +#### `bonsai-py/pyproject.toml` + +```toml +[build-system] +requires = ["maturin>=1.7,<2.0"] +build-backend = "maturin" + +[project] +name = "bonsai-py" +description = "Behavior trees in Python, powered by the bonsai-bt Rust crate." +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.10" +authors = [ + { name = "Kristoffer Solberg Rakstad", email = "kristoffer.solberg@cognite.com" }, + { name = "Anmol Kathail", email = "anmolkathail@gmail.com" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Rust", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Games/Entertainment", + "Typing :: Typed", +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/sollimann/bonsai" +Repository = "https://github.com/sollimann/bonsai" +Issues = "https://github.com/sollimann/bonsai/issues" + +[tool.maturin] +module-name = "bonsai_py" +python-source = "python" +features = ["pyo3/extension-module"] +strip = true +``` + +Notes: +- `dynamic = ["version"]` — maturin auto-populates the project version from `Cargo.toml`'s `package.version`. Don't duplicate it here, or you risk drift between the wheel filename and the package metadata. +- `python-source = "python"` — required so maturin recognizes `python/bonsai_py/` as the package layout and places the compiled `.so` *inside* the `bonsai_py/` Python package (not at the top level). Without this, `from .bonsai_py import *` in `__init__.py` fails. +- `strip = true` — strips debug symbols from the built `cdylib`, shrinking the wheel by ~5-10 MB. +- No `include = [...]` for `../bonsai/**` — superseded by the dual-version dep. Adding it would bundle bonsai's source into the sdist redundantly. + +#### `bonsai-py/src/lib.rs` + +```rust +use pyo3::prelude::*; + +/// Python bindings for the bonsai-bt behavior-tree library. +/// +/// Construct trees with the factory functions (Sequence, Action, Wait, …), +/// wrap one in `BT(tree, blackboard)`, and drive it with `bt.tick(dt, callback)`. +#[pymodule] +fn bonsai_py(_py: Python<'_>, _m: &Bound<'_, PyModule>) -> PyResult<()> { + Ok(()) +} +``` + +Notes: +- Function name `bonsai_py` **must** match `[lib].name` and `tool.maturin.module-name`. PyO3 emits `PyInit_bonsai_py`; the dynamic loader looks for that symbol in `bonsai_py.so`. +- The `&Bound<'_, PyModule>` parameter shape is the PyO3 ≥0.22 signature. If pinning to an older PyO3, switch to `&PyModule`. +- The module docstring shows up under `help(bonsai_py)` and on PyPI — keep it short but useful even at the scaffolding stage. + +#### `bonsai-py/python/bonsai_py/__init__.py` + +```python +"""bonsai-py — behavior trees in Python, powered by the Rust bonsai-bt crate.""" + +from importlib.metadata import PackageNotFoundError, version as _version + +try: + __version__ = _version("bonsai-py") +except PackageNotFoundError: # editable install before metadata is in place + __version__ = "0.0.0+unknown" + +from .bonsai_py import * # noqa: F401,F403 (re-export the compiled module) +``` + +Notes: +- `importlib.metadata.version("bonsai-py")` reads from the installed wheel's METADATA. This keeps `__version__` in lockstep with the PyPI name without hard-coding. +- The `from .bonsai_py import *` line is what makes `bonsai_py.BT`, `bonsai_py.Status`, etc. resolve at top level once Sections 2–4 land. `# noqa: F401,F403` is for linters; remove if the project doesn't use ruff/flake8. + +#### `bonsai-py/python/bonsai_py/py.typed` + +Empty file. PEP 561 marker — type checkers (`mypy`, `pyright`) only honor inline types or stub files if this marker is present. + +#### `bonsai-py/README.md` + +````markdown +# bonsai-py — Python bindings + +Python bindings for the [bonsai-bt](https://github.com/sollimann/bonsai) +behavior-tree library. + +> Scaffolding only — public API arrives in subsequent commits. See the +> repository for the full design plan. + +## Installation (dev) + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1 +pip install maturin +cd bonsai-py +maturin develop +python -c "import bonsai_py; print(bonsai_py.__version__)" +``` + +## License + +MIT — see [LICENSE](../LICENSE). +```` + +Real placeholder (not blank), so a PyPI render won't break if a release is cut before Section 9 fills it in. + +### Files to modify + +#### Root [Cargo.toml](Cargo.toml) + +```toml +[workspace] +resolver = "2" +members = ["bonsai", "examples", "bonsai-py"] +``` + +Append `"bonsai-py"` to the existing two-entry list. Keep `resolver = "2"` (already there per commit `341b7a7`). No other change. + +### Edge cases & pitfalls + +The implementer should read this list **before** typing anything — most pitfalls produce confusing errors hours into debugging. + +1. **Hyphen vs underscore drift.** One conceptual name (`bonsai-py`) flows through every layer, but Python module identifiers can't contain hyphens — so the *import* name is `bonsai_py` (underscore). Concretely: PyPI distribution `bonsai-py` ↔ Cargo package `bonsai-py` ↔ `[lib].name` `bonsai_py` ↔ `#[pymodule]` fn `bonsai_py` ↔ `tool.maturin.module-name` `bonsai_py` ↔ `import bonsai_py`. If the underscored form drifts (e.g. `[lib].name = "bonsai_bt"`), the import fails at runtime with `ImportError: dynamic module does not define module export function (PyInit_*)`. Quick check: grep for `bonsai_py` and `bonsai-py` and confirm those are the only two forms in the binding crate. + +2. **`python-source` layout is exact.** `python/bonsai_py/__init__.py` — both directory names matter. If you name the inner dir `python/bonsai/`, maturin will install the package as `bonsai`, not `bonsai_py`, and the C extension import inside `__init__.py` will mismatch. + +3. **Path dependency without version.** `bonsai-bt = { path = "../bonsai" }` (no `version`) works for `cargo build` but causes `cargo package` (and hence `maturin sdist`) to fail with `cannot package a workspace member with a path dependency without a version`. The dual form solves this. + +4. **bonsai-bt not on crates.io yet?** Check `https://crates.io/crates/bonsai-bt`. If version 0.12.0 isn't there, the dual dep still works for local builds but `maturin sdist` will refuse to package because the version isn't resolvable from a registry. **Action:** before publishing any Python sdist (Section 8/9), confirm `cargo search bonsai-bt` shows ≥0.12.0. If it doesn't, run `cargo publish -p bonsai-bt` from the workspace root first. Wheels are unaffected — only sdist needs registry resolution. + +5. **abi3 feature drift.** Some PyO3 features (custom `__hash__`, certain magic methods) need conditional `#[cfg(...)]` to compile under abi3. Section 1 won't hit this since the module is empty, but document it: when Section 2 adds `#[pyclass(eq, eq_int)]`, verify it compiles with `abi3-py310` (it should — `eq_int` is abi3-safe). + +6. **`f32` feature must NOT be enabled.** Python's `float` is always 64-bit (IEEE-754 double). Compiling bonsai-bt with `f32` and passing `f64` from Python down to a `f32`-typed `dt` would either fail to compile or silently truncate. Keep `features = ["visualize"]` only. + +7. **`cargo build` from workspace root pulls Python headers.** Building `bonsai-py` requires the C Python development headers. On Linux CI without them, `cargo build` (no `-p`) will fail. For Section 1's verification this isn't a blocker (we test `-p bonsai-py` explicitly and `-p bonsai-bt`), but Section 8 will need to either (a) configure CI to install Python dev headers before `cargo build`, or (b) avoid workspace-wide `cargo build` in Rust-only CI. Flag this in the section so the implementer mentions it in the PR description. + +8. **Windows wheel build needs MSVC.** Not Section 1's job, but document: developers on Windows must have the MSVC build tools installed (`rustup default stable-msvc` and Visual Studio Build Tools). `maturin develop` will fail with a linker error otherwise. + +9. **`maturin develop` requires an active venv.** It refuses to install into the system Python. The verification steps below activate one explicitly. + +10. **README path resolution.** `readme = "README.md"` in `pyproject.toml` is relative to `pyproject.toml`, not to the workspace root. The placeholder README **must exist** at `bonsai-py/README.md` before `maturin develop` runs, or the build fails before reaching Rust compilation. + +11. **`publish = false` on the Cargo package** prevents accidental `cargo publish` from pushing the binding crate to crates.io (where it would never resolve correctly). The Python wheel is the only published artifact. + +12. **`Cargo.lock` churn.** Adding `bonsai-py` will materially expand the workspace `Cargo.lock` (pyo3 pulls a sizeable dep graph). Expect the lockfile diff in the PR to be large. This is normal and not a regression. + +### Python-usage considerations baked into the scaffold + +Even though Section 1 ships no API, several scaffolding choices were made specifically to make later sections' Python ergonomics work: + +- **`py.typed` from day one** — once Section 5 lands stubs, IDE autocomplete works in editable installs without any additional config. +- **`python-source = "python"`** — lets us add pure-Python helpers later (e.g. context managers, dataclass adapters) without rebuilding the wheel. +- **`__version__` via `importlib.metadata`** — common Python convention; matches what users expect from `pip show bonsai-py`. +- **`abi3-py310`** — users on 3.10/3.11/3.12/3.13 install the *same* wheel; no per-Python-minor fanout. Saves CI time and disk for users. +- **Module docstring in `lib.rs`** — `help(bonsai_py)` will print something meaningful from day one, which sets expectations for the API doc style in later sections. + +### Verification (sequential — each step must pass before continuing) + +Run from the repository root unless noted. **Each step is a complete command the implementer can copy-paste.** + +1. **Rust crate compiles in isolation:** + ```bash + cargo check -p bonsai-py + ``` + Expect: exit 0, warnings allowed but no errors. First run will download pyo3 and friends — may take 1–2 min. + +2. **Workspace still builds end-to-end (core crate unaffected):** + ```bash + cargo build -p bonsai-bt + cargo test -p bonsai-bt + ``` + Expect: existing tests still pass. If they don't, Section 1 has corrupted workspace settings — investigate before continuing. + +3. **Create a fresh venv:** + ```bash + python -m venv .venv + source .venv/bin/activate # macOS/Linux + .\.venv\Scripts\Activate.ps1 # Windows PowerShell + ``` + +4. **Install maturin:** + ```bash + pip install -U "maturin>=1.7,<2.0" + ``` + +5. **Build & install the extension into the venv:** + ```bash + cd bonsai-py + maturin develop + ``` + Expect a line like `📦 Installed bonsai-py-0.12.0`. The compiled artifact name should contain `abi3` — confirm by inspecting `maturin develop -v` output or: + ```bash + python -c "import bonsai_py, os; print(os.path.basename(bonsai_py.__file__))" + ``` + Expect a filename like `bonsai_py.abi3.so` (Linux/macOS) or `bonsai_py.pyd` (Windows — note Windows abi3 wheels embed the tag in the wheel filename, not the `.pyd`). + +6. **Import works and version metadata is present:** + ```bash + python -c "import bonsai_py; print(bonsai_py.__version__); print(bonsai_py.__doc__)" + ``` + Expect: + ``` + 0.12.0 + bonsai-py — behavior trees in Python, powered by the Rust bonsai-bt crate. + ``` + +7. **Build a release wheel (no install) and verify the filename:** + ```bash + maturin build --release + ls target/wheels/ + ``` + Expect a single file matching `bonsai_py-0.12.0-cp310-abi3-.whl`. The `cp310-abi3` substring confirms abi3-py310 is active. (PyPI normalizes the distribution name `bonsai-py` to `bonsai_py` in wheel filenames — this is expected.) + +If all seven steps pass, Section 1 is complete and Section 2 may begin. + +### Acceptance criteria (for the reviewer) + +- All files listed in **Files to create** exist with the specified content (auto-verifiable: `git diff main --stat` shows the expected set). +- Root `Cargo.toml` `members` includes `"bonsai-py"`. +- Verification steps 1–7 all pass on a clean checkout. +- The PR description confirms `abi3-py310` resolves cleanly on `pyo3 = "0.28"` (see Open Question #1). +- No changes to `bonsai/` or `examples/` source — Section 1 is additive only. (`git diff main -- bonsai/ examples/` should be empty.) + +### Open questions for the implementer (resolve before merging) + +1. **`abi3-py310` feature on `pyo3 = "0.28"` — sanity check.** PyO3 has been pruning older `abi3-py3X` features over time. Confirm `0.28` still exposes `abi3-py310` before pinning by running `cargo add pyo3 --features extension-module,abi3-py310` in a scratch directory. If it doesn't resolve, drop one minor version of PyO3 at a time until it does, and note the resolved pair in the PR description. +2. **`bonsai-bt 0.12.0` on crates.io.** Not blocking for Section 1's verification (which uses path), but the implementer should confirm `cargo search bonsai-bt` returns a `0.12.x` entry. If not, file a follow-up ticket on Section 9 to publish bonsai-bt first. + +### Handoff checklist + +The implementer should treat this section as complete only after: + +- [ ] All 6 files created with content matching this spec. +- [ ] Root `Cargo.toml` updated. +- [ ] Verification steps 1–7 captured in the PR description (screenshot or pasted output). +- [ ] Open questions 1–2 above resolved or filed as follow-up tickets. +- [ ] CI passes on the PR (note: existing `rust-ci.yml` may need a small tweak from item #7 in pitfalls — coordinate with the user if it fails). + +--- + +## Section 2 — `PyStatus` and `PyActionArgs` + +**Goal.** Wrap the two leaf value types that the user-facing API exposes. + +**Depends on:** Section 1. + +**Files to create:** +- [bonsai-py/src/status.rs](bonsai-py/src/status.rs) +- [bonsai-py/src/action_args.rs](bonsai-py/src/action_args.rs) + +**Files to modify:** +- [bonsai-py/src/lib.rs](bonsai-py/src/lib.rs) — add `mod status; mod action_args;` and register `PyStatus`, `PyActionArgs` on the module. + +**Key references:** +- Rust `Status` enum: [bonsai/src/status.rs](bonsai/src/status.rs). +- Rust `ActionArgs<'_, E, A>`: [bonsai/src/state.rs:18-27](bonsai/src/state.rs#L18-L27). + +**Implementation notes:** +- `PyStatus` is a `#[pyclass(eq, eq_int, name = "Status")]` enum with three variants: `Success`, `Failure`, `Running`. Add `From` + `From` conversions in this file. +- `PyActionArgs` is `#[pyclass(name = "ActionArgs")]` with two `#[pyo3(get)]` fields: `dt: f64`, `action: PyObject`. **No `event` field** (resolved decision). +- Provide a Rust-side helper `PyActionArgs::from_rust(args: &ActionArgs, py: Python) -> Self` that clones the action `PyObject` for the Python user. This is what the tick bridge in Section 4 will call. + +**Acceptance criteria:** +- `cargo build -p bonsai-py` passes. +- `maturin develop` succeeds; from Python: `bonsai_py.Status.Success == bonsai_py.Status.Success` and `bonsai_py.Status.Success != bonsai_py.Status.Failure`. +- An ad-hoc Rust unit test constructs a `PyActionArgs` and reads `dt`, `action`. + +--- + +## Section 3 — `PyBehavior` and factory functions + +**Goal.** Let Python construct a `Behavior` tree. + +**Depends on:** Section 1. + +**Files to create:** +- [bonsai-py/src/behavior.rs](bonsai-py/src/behavior.rs). + +**Files to modify:** +- [bonsai-py/src/lib.rs](bonsai-py/src/lib.rs) — register `PyBehavior` and add `m.add_function(...)` for each factory. + +**Key references:** +- Rust `Behavior` enum (14 variants): [bonsai/src/behavior.rs:12](bonsai/src/behavior.rs#L12). +- The exact factory list is in [bonsai/src/lib.rs:121-124](bonsai/src/lib.rs#L121-L124). + +**Implementation notes:** +- `PyBehavior` is opaque to Python: `#[pyclass(name = "Behavior")] struct PyBehavior { inner: Behavior }`. No fields exposed. (Power users can introspect via repr in a later iteration.) +- Implement 14 `#[pyfunction]` factories that build a `Behavior` and wrap it in `PyBehavior`: + - **Leaves:** `Action(action: PyObject) -> PyBehavior`, `Wait(seconds: f64)`, `WaitForever()` + - **Decorators:** `Invert(child: PyBehavior)`, `AlwaysSucceed(child: PyBehavior)` + - **Composites:** `Sequence(children: Vec)`, `Select(children: Vec)`, `WhenAll(...)`, `WhenAny(...)`, `After(...)`, `Race(...)` + - **Control flow:** `If(cond: PyBehavior, ok: PyBehavior, ko: PyBehavior)`, `While(cond: PyBehavior, body: Vec)`, `WhileAll(cond: PyBehavior, body: Vec)` +- Factories take `PyBehavior` by value (the user's variable is consumed). Internally we move `inner` out. +- Add `__repr__` on `PyBehavior` that prints the variant name + arity. Helps debugging. +- Watch the `While` / `WhileAll` panic-on-empty-body documented in [bonsai/src/behavior.rs:54-55](bonsai/src/behavior.rs#L54-L55): raise `ValueError` from Python *before* calling the Rust constructor. Don't let a Rust panic cross the FFI boundary. + +**Acceptance criteria:** +- All 14 factories importable from Python. +- Construct the full tree from [examples/src/visualizer_smoke/main.rs:75-94](examples/src/visualizer_smoke/main.rs#L75-L94) from Python without panics. +- `bt.While(bt.Action("x"), [])` raises `ValueError`. + +--- + +## Section 4 — `PyBT` and the callback bridge + +**Goal.** The hot path: `BT(tree, blackboard).tick(dt, callback)` works end-to-end. + +**Depends on:** Sections 2 and 3. + +**Files to create:** +- [bonsai-py/src/bt.rs](bonsai-py/src/bt.rs). + +**Files to modify:** +- [bonsai-py/src/lib.rs](bonsai-py/src/lib.rs) — register `PyBT`. + +**Key references:** +- Rust `BT` struct + `tick`: [bonsai/src/bt.rs](bonsai/src/bt.rs). +- `with_telemetry`: [bonsai/src/bt_telemetry.rs:94](bonsai/src/bt_telemetry.rs#L94). +- Doctest example (target behavior to match): [bonsai/src/lib.rs:39-118](bonsai/src/lib.rs#L39-L118). + +**API to expose** (all `#[pymethods]`): +- `__init__(behavior: PyBehavior, blackboard: PyObject)` +- `tick(dt: float, callback: Callable[[ActionArgs, Any], tuple[Status, float]]) -> Optional[tuple[Status, float]]` +- `blackboard() -> PyObject` (live reference — mutations to it from Python persist into the BT) +- `reset_bt() -> None` +- `tick_count() -> int` +- `is_finished() -> bool` +- `with_telemetry(port: int, host: str = "127.0.0.1") -> PyBT` (chainable; raises `OSError` if bind fails) + +**The tick bridge.** This is the only non-trivial code in the binding. Sketch: + +```rust +#[pymethods] +impl PyBT { + fn tick(&mut self, py: Python<'_>, dt: f64, callback: PyObject) + -> PyResult> + { + let event: Event = UpdateArgs { dt }.into(); + let mut cb_err: Option = None; + let result = self.inner.tick(&event, &mut |args, bb: &mut PyObject| { + if cb_err.is_some() { return (Status::Failure, 0.0); } // short-circuit after error + let py_args = PyActionArgs::from_rust(&args, py); + match callback.call1(py, (py_args, bb.clone_ref(py))) { + Ok(ret) => match ret.extract::<(PyStatus, f64)>(py) { + Ok((s, dt)) => (s.into(), dt), + Err(e) => { cb_err = Some(e); (Status::Failure, 0.0) } + }, + Err(e) => { cb_err = Some(e); (Status::Failure, 0.0) } + } + }); + if let Some(e) = cb_err { return Err(e); } + Ok(result.map(|(s, dt)| (s.into(), dt))) + } +} +``` + +**Implementation notes:** +- **GIL strategy:** hold the GIL for the whole tick. The closure runs synchronously and is always Python-bound, so `allow_threads` + `with_gil` adds overhead without benefit. (We can revisit if a future async path appears.) +- **Error propagation:** Python exceptions from the callback can't unwind through Rust. Capture in `cb_err` and propagate. Short-circuit subsequent action calls in the same tick to avoid spurious sibling callbacks after an error. +- **Blackboard.** `inner.bb` is the actual `PyObject` — `blackboard()` returns `self.inner.blackboard().clone_ref(py)`. Same Python object, refcount bumped. +- **`with_telemetry`:** wrap the Rust `io::Result`. Use `pyo3::exceptions::PyOSError` for bind failures. Returning `Self` from a `#[pymethods]` consuming method requires careful PyO3 ownership — easier to take `&mut self`, call `with_telemetry_at` semantically (port/host stored, server started), and return a clone or `Py`. Look at [bonsai/src/bt_telemetry.rs:162](bonsai/src/bt_telemetry.rs#L162) — the underlying call takes `mut self` by value. Workaround: pop `inner` into a temporary, run `with_telemetry_at`, put it back. Or refactor to use `&mut self`. Document the approach in code. + +**Acceptance criteria:** +- Python can run the doctest equivalent and observe `count == 1` after 5 ticks of 0.5s (matching [bonsai/src/lib.rs:99-113](bonsai/src/lib.rs#L99-L113)). +- A callback that `raise ValueError("boom")` causes `bt.tick()` to raise `ValueError("boom")` with traceback intact. +- `bt.with_telemetry(8910)` returns the same `BT` (chainable) and starts a listening socket on `127.0.0.1:8910`. +- `bt.with_telemetry(8910)` while the port is already bound raises `OSError`. + +--- + +## Section 5 — Type stubs + +**Goal.** Editor autocomplete + `mypy`-friendly types. + +**Depends on:** Section 4 (final method signatures must be locked in). + +### Approach decision (evaluate before writing code) + +Two viable approaches — **the implementer should pick one at the start of Section 5 and document the choice in the PR description.** + +**Option A — Hand-written stubs (default if B fails compatibility check).** +Write `bonsai-py/stubs/bonsai_py.pyi` by hand, mirroring the API from Section 4. Simple, no extra build-time dep, full control over the wording — but the stub file and the Rust signatures drift independently, so every API change in Sections 2–4 requires a parallel `.pyi` edit. Maintenance cost compounds. + +**Option B — Auto-generate stubs via `pyo3-stub-gen` (preferred if compatible).** +[`pyo3-stub-gen`](https://crates.io/crates/pyo3-stub-gen) (`0.22.2` on crates.io as of 2026-05-15) reads `#[pyo3(...)]` annotations on `#[pyclass]`/`#[pyfunction]` items and emits a `.pyi` file. The stub generation runs as a small binary built into the crate and invoked by `cargo run --bin stub_gen` (typically wired into CI). Trade-offs: +- ✅ Stubs stay in lockstep with the Rust source automatically. +- ✅ Annotations live next to the functions they document. +- ⚠ Adds a build-time dep (`pyo3-stub-gen` + `pyo3-stub-gen-derive`). +- ⚠ **Compatibility check required:** `pyo3-stub-gen 0.22.x` was published before PyO3 `0.28`; the implementer must verify `pyo3-stub-gen` supports PyO3 `0.28` (check the crate's README and Cargo.toml's `pyo3 = "..."` line). If it lags, either wait for an update or fall back to Option A. + +**Recommendation:** start by running `cargo info pyo3-stub-gen` and inspecting its current PyO3 compatibility. If it supports `0.28` (or a newer matching version exists), use Option B. Otherwise Option A. + +### Files to create + +- Option A: [bonsai-py/stubs/bonsai_py.pyi](bonsai-py/stubs/bonsai_py.pyi) (hand-written). +- Option B: [bonsai-py/src/bin/stub_gen.rs](bonsai-py/src/bin/stub_gen.rs) (small binary that calls the generator) + add `#[gen_stub_pyclass]` / `#[gen_stub_pyfunction]` macros throughout the existing Rust files from Sections 2–4. The generated `bonsai_py.pyi` lives at the same path under `stubs/` (or wherever maturin's `include` glob picks it up). + +### Implementation notes (apply to either option) + +- Use `Any` for `action` and the blackboard. Use `Callable[[ActionArgs, Any], tuple[Status, float]]` for the tick callback. +- `Status` is an `IntEnum`-like class — expose `Success`, `Failure`, `Running` as class attributes. +- Factory functions are stubbed at module level: `def Sequence(children: list[Behavior]) -> Behavior: ...` etc. +- In `bonsai-py/pyproject.toml` `[tool.maturin]` add an `include` glob so the `.pyi` and `py.typed` ship in the wheel. +- If Option B is chosen, wire `cargo run --bin stub_gen` into CI (Section 8) so generated stubs are regenerated and diffed on every PR — drift between annotations and committed `.pyi` should fail the build. + +### Acceptance criteria + +- `mypy --strict` on a small example script that imports `bonsai_py` passes with no errors. +- VS Code/PyCharm shows autocomplete on `bt.Status.` and `bt.BT(...).`. +- The PR description states which option was chosen and (for Option B) the exact `pyo3-stub-gen` version pinned alongside the PyO3 0.28 compatibility result. + +--- + +## Section 6 — Test suite + +**Goal.** Behavioral parity with the Rust test suite. + +**Depends on:** Section 4. + +**Files to create:** +- [bonsai-py/tests/test_bt.py](bonsai-py/tests/test_bt.py). +- [bonsai-py/tests/conftest.py](bonsai-py/tests/conftest.py) (optional, fixtures). + +**Tests to write** (one pytest each): +1. **Doctest port** — translate [bonsai/src/lib.rs:39-118](bonsai/src/lib.rs#L39-L118) literally; assert counter values at each step. Covers `Sequence`, `Wait`, `Action`, blackboard mutation, `reset_bt`, `tick_count`, `is_finished`. +2. **Variant coverage** — one test per non-trivial variant. Mirror cases in [bonsai/tests/bt_tests.rs](bonsai/tests/bt_tests.rs): + - `Select` first-success short-circuit + - `If` true/false branches + - `Invert` flips Success↔Failure + - `AlwaysSucceed` swallows Failure + - `While` repeats until cond fails + - `WhileAll` checks cond only between iterations + - `WhenAll` AND-parallel, `WhenAny` OR-parallel + - `Race` first-to-complete (success OR failure) + - `After` sequential success in parallel +3. **Callback error propagation** — callback raises `ValueError`; assert `tick()` propagates it. +4. **Empty `While` body validation** — `bt.While(bt.Action("x"), [])` raises `ValueError`. +5. **`with_telemetry` bind failure** — bind port 8910 twice; second call raises `OSError`. + +**Acceptance criteria:** +- `cd bonsai-py && maturin develop && pytest -v` is green. +- Tests run in <5s total. + +--- + +## Section 7 — Visualizer smoke example + +**Goal.** Confirm the WebSocket visualizer works through Python end-to-end. + +**Depends on:** Section 4. + +**Files to create:** +- [bonsai-py/examples/visualizer_smoke.py](bonsai-py/examples/visualizer_smoke.py). + +**Key references:** +- Rust original: [examples/src/visualizer_smoke/main.rs](examples/src/visualizer_smoke/main.rs). + +**Implementation notes:** +- Direct line-for-line port. Same 27-node tree, same phase-offset color rotation, same 400 ms wall sleep. +- The Rust example uses string slices for actions; in Python use plain `str` — matches naturally. +- `bt.with_telemetry(8910)` chains: `bt = BT(build_tree(), {}).with_telemetry(8910)`. + +**Manual verification protocol** (document in the script's module docstring): +1. `python examples/visualizer_smoke.py` +2. Open +3. Confirm: 27 nodes render, leaves cycle through green/yellow/red every ~400 ms. +4. `Ctrl-C` and restart → browser reconnects within 1 s. + +**Acceptance criteria:** +- Script runs without exceptions for at least 30 s. +- All manual checks above pass. + +--- + +## Section 8 — CI wheel build matrix + +**Goal.** Multi-platform wheels build on push and on tag. + +**Depends on:** Sections 1, 6 (tests must exist to run in CI). + +**Files to create:** +- [.github/workflows/python-wheels.yml](.github/workflows/python-wheels.yml). + +**Implementation notes:** +- Use [`PyO3/maturin-action`](https://github.com/PyO3/maturin-action) (de-facto standard). +- Matrix: + - Linux: `manylinux_2_17_x86_64`, `manylinux_2_17_aarch64` (cross-build via docker) + - macOS: `universal2` (single wheel covers x86_64 + arm64) + - Windows: `x86_64` +- One abi3 wheel per platform covers Python 3.8+ — don't fan out across Python minor versions. +- Triggers: push to `main` (test build, don't publish), tag matching `py-v*` (publish to PyPI). +- Add a step that runs `pytest` on the built wheel inside the matrix (install wheel into a fresh venv, then test). +- PyPI publish step uses `pypa/gh-action-pypi-publish` with `PYPI_API_TOKEN` secret. **Recommend a TestPyPI dry-run job triggered on `py-test-*` tags first.** + +**Acceptance criteria:** +- A push to a feature branch produces green builds for all three platforms. +- Wheels are uploaded as GitHub Actions artifacts. +- A test tag pushes a wheel to TestPyPI; `pip install -i https://test.pypi.org/simple/ bonsai-py` works in a fresh venv. + +--- + +## Section 9 — Docs & release process + +**Goal.** Users can find and install the package. + +**Depends on:** Sections 4, 7, 8. + +**Files to modify:** +- [bonsai-py/README.md](bonsai-py/README.md) — quickstart, install, link to visualizer example, link back to the core Rust crate. +- [README.md](README.md) (root) — short Python section pointing at the wheel: `pip install bonsai-py`. + +**Files to create:** +- [bonsai-py/CHANGELOG.md](bonsai-py/CHANGELOG.md) — initial entry: `0.1.0 — first PyO3 binding, parity with bonsai-bt 0.12.0`. + +**Versioning policy** (document in `bonsai-py/README.md`): +- Python package version tracks the Rust crate version it wraps (e.g. Rust `bonsai-bt 0.12.0` → `bonsai-py 0.12.0` on PyPI). +- Hot-fix releases on the Python side only bump a `postN` suffix (PEP 440), e.g. `0.12.0.post1`. + +**Acceptance criteria:** +- README renders correctly on PyPI (no broken Markdown after `maturin upload`). +- A tagged release pushes both a sdist and wheels for all platforms. + +--- + +## Integration verification (final, after all sections) + +Run once everything is in place: + +1. Fresh venv: `python -m venv .venv && source .venv/bin/activate` +2. `pip install bonsai-py` (from PyPI, not the local path). +3. Run [bonsai-py/examples/visualizer_smoke.py](bonsai-py/examples/visualizer_smoke.py) and walk through Section 7's manual checks. +4. `pytest bonsai-py/tests/` against the installed wheel. +5. Verify `import bonsai_py; help(bonsai_py.BT)` shows the documented surface. + +If all five pass, the binding is ready to announce. diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..e90285d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(git rm *)" + ] + } +} diff --git a/.gitignore b/.gitignore index c7deb0c..d028cad 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,12 @@ Cargo.lock # Python virtual environments .venv/ venv/ + +# Python build artifacts (maturin develop output, bytecode caches) +__pycache__/ +*.pyc +*.pyo +*.so +*.abi3.so +*.pyd +*.dylib diff --git a/bonsai-py/python/bonsai_py/__pycache__/__init__.cpython-312.pyc b/bonsai-py/python/bonsai_py/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index b183b502f92ec908ebca3b7a48f7eba815fa192d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 522 zcmX|7F>ljA6n@WkY8QwqMJ%ltjt~h2XxxQ~imiaC6h&#K&z7Ex-Sf`Yn@3+ecaQgf1%R8smp%lyegb0mTj%!4eogPLiW^pW O8f?K^zvsTgp8E%$SD4-a diff --git a/bonsai-py/python/bonsai_py/bonsai_py.abi3.so b/bonsai-py/python/bonsai_py/bonsai_py.abi3.so deleted file mode 100755 index ec129265ecc3ecd2ee512c0303f94e56ef187baf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 512888 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≫It8L;YkMu6#6xT@%1 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%0A3w*E`Mq-q%vMxdJ>0QahJ3do&d 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