Skip to content

Commit 3ccc3fd

Browse files
committed
Squashed commit of the following:
commit 47f067c Author: shaur-k <[email protected]> Date: Thu Jun 19 14:53:45 2025 -0600 feat: clean up old PR for new version of BTCPP commit 8c6cfac Author: shaur-k <[email protected]> Date: Thu Jun 19 13:16:13 2025 -0600 temp commit 6a6f86e Merge: 890396c 1b9e0e2 Author: shaur-k <[email protected]> Date: Thu Jun 19 12:07:36 2025 -0600 Merge branch 'master' into python-bindings commit 890396c Author: Kyle Cesare <[email protected]> Date: Tue Sep 5 23:45:06 2023 -0600 Fix setup.py package attributes. commit fb33788 Author: Kyle Cesare <[email protected]> Date: Tue Sep 5 21:57:19 2023 -0600 Add useful note for ex05 on shared lib location. commit 632eb66 Author: Kyle Cesare <[email protected]> Date: Tue Sep 5 21:55:52 2023 -0600 Clean up dummy node use in ex05. commit 1a7ac0a Author: Kyle Cesare <[email protected]> Date: Tue Sep 5 21:48:23 2023 -0600 Properly specify __all__ for btpy module. commit b425c91 Author: Kyle Cesare <[email protected]> Date: Mon Sep 4 19:17:31 2023 -0600 Add docs for `JsonExporter::fromJson`. commit 2584aec Author: Kyle Cesare <[email protected]> Date: Mon Sep 4 19:03:16 2023 -0600 Add default impl of AsyncActionNode#on_halted commit 04f435d Author: Kyle Cesare <[email protected]> Date: Mon Sep 4 19:03:07 2023 -0600 Add `halt_tree` binding and use in demo commit 535ea88 Author: Kyle Cesare <[email protected]> Date: Mon Sep 4 18:56:14 2023 -0600 Improve python example README commit 93b58c3 Author: Kyle Cesare <[email protected]> Date: Mon Sep 4 18:56:08 2023 -0600 Formatting. commit 0ee0a20 Author: Kyle Cesare <[email protected]> Date: Mon Sep 4 18:55:57 2023 -0600 Fix some string-embedded XML indentation. commit 0786475 Author: Kyle Cesare <[email protected]> Date: Mon Sep 4 18:54:11 2023 -0600 Don't make Py_StatefulActionNode final. commit 4ad738c Author: Kyle Cesare <[email protected]> Date: Sun Sep 3 19:16:29 2023 -0600 Add some docs to Python ex06. commit 84ae12d Author: Kyle Cesare <[email protected]> Date: Sun Sep 3 19:04:27 2023 -0600 Add some type hints to the Python code commit 46929a8 Author: Kyle Cesare <[email protected]> Date: Sun Sep 3 12:31:00 2023 -0600 Move Python-related source files into subdirectory. commit 9d8db3c Author: Kyle Cesare <[email protected]> Date: Sun Sep 3 11:57:13 2023 -0600 Clean up Python example XMLs. commit ee7f464 Author: Kyle Cesare <[email protected]> Date: Sat Sep 2 11:08:42 2023 -0600 Modify py::type argument to support older pybind commit fdc2232 Author: Kyle Cesare <[email protected]> Date: Fri Sep 1 22:18:04 2023 -0600 Add pyproject.toml/setup.py for building wheels. commit 2c1b18a Author: Kyle Cesare <[email protected]> Date: Fri Sep 1 21:40:14 2023 -0600 Use docstring as tree node description. commit 1a69d3a Author: Kyle Cesare <[email protected]> Date: Sat Aug 26 22:47:56 2023 -0600 Clean up Python ex06. commit 0e35ac0 Author: Kyle Cesare <[email protected]> Date: Fri Sep 1 19:35:57 2023 -0600 Move some dummy_nodes definitions to cpp file to fix linker error commit 83caef7 Author: Kyle Cesare <[email protected]> Date: Fri Sep 1 19:35:46 2023 -0600 Add missing pybind11 dependency to package.xml commit c703efd Author: Kyle Cesare <[email protected]> Date: Sat Aug 19 19:49:01 2023 -0600 Implement coroutine-based Python nodes. commit 4448376 Author: Kyle Cesare <[email protected]> Date: Sat Aug 19 19:07:54 2023 -0600 Add pybind11 conan dependency. commit 21d450e Author: Kyle Cesare <[email protected]> Date: Sat Aug 19 18:52:52 2023 -0600 Add `BehaviorTreeFactory.register_from_plugin` binding. commit d1fe0e3 Author: Kyle Cesare <[email protected]> Date: Sat Aug 19 18:52:39 2023 -0600 Implement C++ <-> Python type interop via JSON. commit cfa553a Author: Kyle Cesare <[email protected]> Date: Sat Aug 19 12:52:01 2023 -0600 Disable zero variadic arg warning. commit 7927e67 Author: Kyle Cesare <[email protected]> Date: Sat Aug 19 12:51:44 2023 -0600 Fix onHalted override copy-paste error. commit 712370b Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 12:30:01 2023 -0600 Add useful command for ex04. commit dc9e953 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 12:28:16 2023 -0600 Fix typo in ex04. commit ed78e84 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 12:27:34 2023 -0600 Fix NodeStatus enum value ordering. commit 2a22fa8 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 12:05:05 2023 -0600 Add note about Py_getInput return value. commit 37ae114 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 12:01:52 2023 -0600 Add simple README. commit adf5cef Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 11:56:52 2023 -0600 Add ROS2 interop example. commit 404a195 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 11:56:38 2023 -0600 Add builder args to be passed to node ctors. commit 9493f10 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 11:56:23 2023 -0600 Return None if blackboard value doesn't exist. commit 6188c21 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 11:10:10 2023 -0600 Ignore pycache. commit b22772e Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 10:55:49 2023 -0600 Put generic methods on abstract base class. commit 17da541 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 10:54:59 2023 -0600 Clean up port handling. commit 4ff5673 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 10:54:37 2023 -0600 Export minimal set of identifiers to Python lib. commit 351c33a Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 10:54:25 2023 -0600 Use proper PYBIND11_OVERRIDE macros. commit 1f9db33 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 10:30:18 2023 -0600 Eliminate some code duplication. commit f560500 Author: Kyle Cesare <[email protected]> Date: Sun Aug 13 00:22:11 2023 -0600 Add stateful action bindings. commit 9fdcda1 Author: Kyle Cesare <[email protected]> Date: Sat Aug 12 23:09:20 2023 -0600 Implement minimal Python bindings to SyncActionNode
1 parent 1b9e0e2 commit 3ccc3fd

22 files changed

+1288
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ build*
44
site/*
55
/.vscode/
66
.vs/
7+
__pycache__
78

89
# clangd cache
910
/.cache/*

CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ option(BTCPP_EXAMPLES "Build tutorials and examples" ON)
3333
option(BTCPP_UNIT_TESTS "Build the unit tests" ON)
3434
option(BTCPP_GROOT_INTERFACE "Add Groot2 connection. Requires ZeroMQ" ON)
3535
option(BTCPP_SQLITE_LOGGING "Add SQLite logging." ON)
36+
option(BTCPP_PYTHON "Add Python bindings" ON)
3637

3738
option(USE_V3_COMPATIBLE_NAMES "Use some alias to compile more easily old 3.x code" OFF)
3839

@@ -142,6 +143,10 @@ if(BTCPP_SQLITE_LOGGING)
142143
list(APPEND BT_SOURCE src/loggers/bt_sqlite_logger.cpp )
143144
endif()
144145

146+
if(BTCPP_PYTHON)
147+
list(APPEND BT_SOURCE src/python/types.cpp)
148+
endif()
149+
145150
######################################################
146151

147152
if (UNIX)
@@ -173,6 +178,18 @@ target_link_libraries(${BTCPP_LIBRARY}
173178
${BTCPP_EXTRA_LIBRARIES}
174179
)
175180

181+
if(BTCPP_PYTHON)
182+
find_package(Python COMPONENTS Interpreter Development)
183+
find_package(pybind11 CONFIG)
184+
185+
pybind11_add_module(btpy_cpp src/python/bindings.cpp)
186+
target_compile_options(btpy_cpp PRIVATE -Wno-gnu-zero-variadic-macro-arguments)
187+
target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY})
188+
189+
target_link_libraries(${BTCPP_LIBRARY} PUBLIC Python::Python pybind11::pybind11)
190+
target_compile_definitions(${BTCPP_LIBRARY} PUBLIC BTCPP_PYTHON)
191+
endif()
192+
176193
target_include_directories(${BTCPP_LIBRARY}
177194
PUBLIC
178195
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>

btpy/__init__.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Top-level module of the BehaviorTree.CPP Python bindings.
5+
"""
6+
7+
# re-export
8+
from btpy_cpp import (
9+
BehaviorTreeFactory,
10+
NodeStatus,
11+
StatefulActionNode,
12+
SyncActionNode,
13+
Tree,
14+
)
15+
16+
17+
def ports(inputs: list[str] = [], outputs: list[str] = []):
18+
"""Decorator to specify input and outputs ports for an action node."""
19+
20+
def specify_ports(cls):
21+
cls.input_ports = list(inputs)
22+
cls.output_ports = list(outputs)
23+
return cls
24+
25+
return specify_ports
26+
27+
28+
class AsyncActionNode(StatefulActionNode):
29+
"""An abstract action node implemented via cooperative multitasking.
30+
31+
Subclasses must implement the `run()` method as a generator. Optionally,
32+
this method can return a final `NodeStatus` value to indicate its exit
33+
condition.
34+
35+
Optionally, subclasses can override the `on_halted()` method which is called
36+
when the tree halts. The default implementation does nothing. The `run()`
37+
method will never be called again after a halt.
38+
39+
Note:
40+
It is the responsibility of the action author to not block the main
41+
behavior tree loop with long-running tasks. `yield` calls should be
42+
placed whenever a pause is appropriate.
43+
"""
44+
45+
def __init__(self, name, config):
46+
super().__init__(name, config)
47+
48+
def on_start(self) -> NodeStatus:
49+
self.coroutine = self.run()
50+
return NodeStatus.RUNNING
51+
52+
def on_running(self) -> NodeStatus:
53+
# The library logic should never allow this to happen, but users can
54+
# still manually call `on_running` without an associated `on_start`
55+
# call. Make sure to print a useful error when this happens.
56+
if self.coroutine is None:
57+
raise "AsyncActionNode run without starting"
58+
59+
# Resume the coroutine (generator). As long as the generator is not
60+
# exhausted, keep this action in the RUNNING state.
61+
try:
62+
next(self.coroutine)
63+
return NodeStatus.RUNNING
64+
except StopIteration as e:
65+
# If the action returns a status then propagate it upwards.
66+
if e.value is not None:
67+
return e.value
68+
# Otherwise, just assume the action finished successfully.
69+
else:
70+
return NodeStatus.SUCCESS
71+
72+
def on_halted(self):
73+
# Default action: do nothing
74+
pass
75+
76+
77+
# Specify the symbols to be imported with `from btpy import *`, as described in
78+
# [1].
79+
#
80+
# [1]: https://docs.python.org/3/tutorial/modules.html#importing-from-a-package
81+
__all__ = [
82+
"ports",
83+
"AsyncActionNode",
84+
"BehaviorTreeFactory",
85+
"NodeStatus",
86+
"StatefulActionNode",
87+
"SyncActionNode",
88+
"Tree",
89+
]

conanfile.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
gtest/1.14.0
33
zeromq/4.3.4
44
sqlite3/3.40.1
5+
pybind11/2.10.4
56

67
[generators]
78
CMakeDeps
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/***************************************************************************
2+
* Copyright (c) 2019, Martin Renou *
3+
* *
4+
* Distributed under the terms of the BSD 3-Clause License. *
5+
* *
6+
* The full license is in the file LICENSE, distributed with this software. *
7+
****************************************************************************/
8+
9+
#ifndef PYBIND11_JSON_HPP
10+
#define PYBIND11_JSON_HPP
11+
12+
#include <string>
13+
#include <vector>
14+
15+
#include "behaviortree_cpp/contrib/json.hpp"
16+
17+
#include "pybind11/pybind11.h"
18+
19+
namespace pyjson
20+
{
21+
namespace py = pybind11;
22+
namespace nl = nlohmann;
23+
24+
inline py::object from_json(const nl::json& j)
25+
{
26+
if (j.is_null())
27+
{
28+
return py::none();
29+
}
30+
else if (j.is_boolean())
31+
{
32+
return py::bool_(j.get<bool>());
33+
}
34+
else if (j.is_number_unsigned())
35+
{
36+
return py::int_(j.get<nl::json::number_unsigned_t>());
37+
}
38+
else if (j.is_number_integer())
39+
{
40+
return py::int_(j.get<nl::json::number_integer_t>());
41+
}
42+
else if (j.is_number_float())
43+
{
44+
return py::float_(j.get<double>());
45+
}
46+
else if (j.is_string())
47+
{
48+
return py::str(j.get<std::string>());
49+
}
50+
else if (j.is_array())
51+
{
52+
py::list obj(j.size());
53+
for (std::size_t i = 0; i < j.size(); i++)
54+
{
55+
obj[i] = from_json(j[i]);
56+
}
57+
return obj;
58+
}
59+
else // Object
60+
{
61+
py::dict obj;
62+
for (nl::json::const_iterator it = j.cbegin(); it != j.cend(); ++it)
63+
{
64+
obj[py::str(it.key())] = from_json(it.value());
65+
}
66+
return obj;
67+
}
68+
}
69+
70+
inline nl::json to_json(const py::handle& obj)
71+
{
72+
if (obj.ptr() == nullptr || obj.is_none())
73+
{
74+
return nullptr;
75+
}
76+
if (py::isinstance<py::bool_>(obj))
77+
{
78+
return obj.cast<bool>();
79+
}
80+
if (py::isinstance<py::int_>(obj))
81+
{
82+
try
83+
{
84+
nl::json::number_integer_t s = obj.cast<nl::json::number_integer_t>();
85+
if (py::int_(s).equal(obj))
86+
{
87+
return s;
88+
}
89+
}
90+
catch (...)
91+
{
92+
}
93+
try
94+
{
95+
nl::json::number_unsigned_t u = obj.cast<nl::json::number_unsigned_t>();
96+
if (py::int_(u).equal(obj))
97+
{
98+
return u;
99+
}
100+
}
101+
catch (...)
102+
{
103+
}
104+
throw std::runtime_error("to_json received an integer out of range for both nl::json::number_integer_t and nl::json::number_unsigned_t type: " + py::repr(obj).cast<std::string>());
105+
}
106+
if (py::isinstance<py::float_>(obj))
107+
{
108+
return obj.cast<double>();
109+
}
110+
if (py::isinstance<py::bytes>(obj))
111+
{
112+
py::module base64 = py::module::import("base64");
113+
return base64.attr("b64encode")(obj).attr("decode")("utf-8").cast<std::string>();
114+
}
115+
if (py::isinstance<py::str>(obj))
116+
{
117+
return obj.cast<std::string>();
118+
}
119+
if (py::isinstance<py::tuple>(obj) || py::isinstance<py::list>(obj))
120+
{
121+
auto out = nl::json::array();
122+
for (const py::handle value : obj)
123+
{
124+
out.push_back(to_json(value));
125+
}
126+
return out;
127+
}
128+
if (py::isinstance<py::dict>(obj))
129+
{
130+
auto out = nl::json::object();
131+
for (const py::handle key : obj)
132+
{
133+
out[py::str(key).cast<std::string>()] = to_json(obj[key]);
134+
}
135+
return out;
136+
}
137+
throw std::runtime_error("to_json not implemented for this type of object: " + py::repr(obj).cast<std::string>());
138+
}
139+
}
140+
141+
// nlohmann_json serializers
142+
namespace nlohmann
143+
{
144+
namespace py = pybind11;
145+
146+
#define MAKE_NLJSON_SERIALIZER_DESERIALIZER(T) \
147+
template <> \
148+
struct adl_serializer<T> \
149+
{ \
150+
inline static void to_json(json& j, const T& obj) \
151+
{ \
152+
j = pyjson::to_json(obj); \
153+
} \
154+
\
155+
inline static T from_json(const json& j) \
156+
{ \
157+
return pyjson::from_json(j); \
158+
} \
159+
}
160+
161+
#define MAKE_NLJSON_SERIALIZER_ONLY(T) \
162+
template <> \
163+
struct adl_serializer<T> \
164+
{ \
165+
inline static void to_json(json& j, const T& obj) \
166+
{ \
167+
j = pyjson::to_json(obj); \
168+
} \
169+
}
170+
171+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::object);
172+
173+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::bool_);
174+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::int_);
175+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::float_);
176+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::str);
177+
178+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::list);
179+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::tuple);
180+
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::dict);
181+
182+
MAKE_NLJSON_SERIALIZER_ONLY(py::handle);
183+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::item_accessor);
184+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::list_accessor);
185+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::tuple_accessor);
186+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::sequence_accessor);
187+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::str_attr_accessor);
188+
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::obj_attr_accessor);
189+
190+
#undef MAKE_NLJSON_SERIALIZER
191+
#undef MAKE_NLJSON_SERIALIZER_ONLY
192+
}
193+
194+
// pybind11 caster
195+
namespace pybind11
196+
{
197+
namespace detail
198+
{
199+
template <> struct type_caster<nlohmann::json>
200+
{
201+
public:
202+
PYBIND11_TYPE_CASTER(nlohmann::json, _("json"));
203+
204+
bool load(handle src, bool)
205+
{
206+
try
207+
{
208+
value = pyjson::to_json(src);
209+
return true;
210+
}
211+
catch (...)
212+
{
213+
return false;
214+
}
215+
}
216+
217+
static handle cast(nlohmann::json src, return_value_policy /* policy */, handle /* parent */)
218+
{
219+
object obj = pyjson::from_json(src);
220+
return obj.release();
221+
}
222+
};
223+
}
224+
}
225+
226+
#endif

include/behaviortree_cpp/json_export.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#pragma once
22

3-
#include "behaviortree_cpp/basic_types.h"
43
#include "behaviortree_cpp/utils/safe_any.hpp"
54
#include "behaviortree_cpp/basic_types.h"
65

@@ -45,7 +44,6 @@ namespace BT
4544
/**
4645
* Use RegisterJsonDefinition<Foo>();
4746
*/
48-
4947
class JsonExporter
5048
{
5149
public:
@@ -80,6 +78,12 @@ class JsonExporter
8078
template <typename T>
8179
Expected<T> fromJson(const nlohmann::json& source) const;
8280

81+
template <typename T>
82+
void fromJsonHelper(const nlohmann::json& src, T& dst) const
83+
{
84+
dst = *fromJson<T>(src);
85+
}
86+
8387
/// Register new JSON converters with addConverter<Foo>().
8488
/// You should have used first the macro BT_JSON_CONVERTER
8589
template <typename T>

0 commit comments

Comments
 (0)