Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Python bindings #634

Open
wants to merge 43 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9fdcda1
Implement minimal Python bindings to SyncActionNode
kylc Aug 13, 2023
f560500
Add stateful action bindings.
kylc Aug 13, 2023
1f9db33
Eliminate some code duplication.
kylc Aug 13, 2023
351c33a
Use proper PYBIND11_OVERRIDE macros.
kylc Aug 13, 2023
4ff5673
Export minimal set of identifiers to Python lib.
kylc Aug 13, 2023
17da541
Clean up port handling.
kylc Aug 13, 2023
b22772e
Put generic methods on abstract base class.
kylc Aug 13, 2023
6188c21
Ignore pycache.
kylc Aug 13, 2023
9493f10
Return None if blackboard value doesn't exist.
kylc Aug 13, 2023
404a195
Add builder args to be passed to node ctors.
kylc Aug 13, 2023
adf5cef
Add ROS2 interop example.
kylc Aug 13, 2023
37ae114
Add simple README.
kylc Aug 13, 2023
2a22fa8
Add note about Py_getInput return value.
kylc Aug 13, 2023
ed78e84
Fix NodeStatus enum value ordering.
kylc Aug 13, 2023
dc9e953
Fix typo in ex04.
kylc Aug 13, 2023
712370b
Add useful command for ex04.
kylc Aug 13, 2023
7927e67
Fix onHalted override copy-paste error.
kylc Aug 19, 2023
cfa553a
Disable zero variadic arg warning.
kylc Aug 19, 2023
d1fe0e3
Implement C++ <-> Python type interop via JSON.
kylc Aug 20, 2023
21d450e
Add `BehaviorTreeFactory.register_from_plugin` binding.
kylc Aug 20, 2023
4448376
Add pybind11 conan dependency.
kylc Aug 20, 2023
c703efd
Implement coroutine-based Python nodes.
kylc Aug 20, 2023
83caef7
Add missing pybind11 dependency to package.xml
kylc Sep 2, 2023
0e35ac0
Move some dummy_nodes definitions to cpp file to fix linker error
kylc Sep 2, 2023
1a69d3a
Clean up Python ex06.
kylc Aug 27, 2023
2c1b18a
Use docstring as tree node description.
kylc Sep 2, 2023
fdc2232
Add pyproject.toml/setup.py for building wheels.
kylc Sep 2, 2023
ee7f464
Modify py::type argument to support older pybind
kylc Sep 2, 2023
9d8db3c
Clean up Python example XMLs.
kylc Sep 3, 2023
46929a8
Move Python-related source files into subdirectory.
kylc Sep 3, 2023
84ae12d
Add some type hints to the Python code
kylc Sep 4, 2023
4ad738c
Add some docs to Python ex06.
kylc Sep 4, 2023
0786475
Don't make Py_StatefulActionNode final.
kylc Sep 5, 2023
0ee0a20
Fix some string-embedded XML indentation.
kylc Sep 5, 2023
93b58c3
Formatting.
kylc Sep 5, 2023
535ea88
Improve python example README
kylc Sep 5, 2023
04f435d
Add `halt_tree` binding and use in demo
kylc Sep 5, 2023
2584aec
Add default impl of AsyncActionNode#on_halted
kylc Sep 5, 2023
b425c91
Add docs for `JsonExporter::fromJson`.
kylc Sep 5, 2023
1a7ac0a
Properly specify __all__ for btpy module.
kylc Sep 6, 2023
632eb66
Clean up dummy node use in ex05.
kylc Sep 6, 2023
fb33788
Add useful note for ex05 on shared lib location.
kylc Sep 6, 2023
890396c
Fix setup.py package attributes.
kylc Sep 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build*
site/*
/.vscode/
.vs/
__pycache__

# clangd cache
/.cache/*
Expand Down
17 changes: 17 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ option(BTCPP_EXAMPLES "Build tutorials and examples" ON)
option(BTCPP_UNIT_TESTS "Build the unit tests" ON)
option(BTCPP_GROOT_INTERFACE "Add Groot2 connection. Requires ZeroMQ" ON)
option(BTCPP_SQLITE_LOGGING "Add SQLite logging." ON)
option(BTCPP_PYTHON "Add Python bindings" ON)

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

Expand Down Expand Up @@ -134,6 +135,10 @@ if(BTCPP_SQLITE_LOGGING)
list(APPEND BT_SOURCE src/loggers/bt_sqlite_logger.cpp )
endif()

if(BTCPP_PYTHON)
list(APPEND BT_SOURCE src/python/types.cpp)
endif()

######################################################

if (UNIX)
Expand Down Expand Up @@ -163,6 +168,18 @@ target_link_libraries(${BTCPP_LIBRARY}
${BTCPP_EXTRA_LIBRARIES}
)

if(BTCPP_PYTHON)
find_package(Python COMPONENTS Interpreter Development)
find_package(pybind11 CONFIG)

pybind11_add_module(btpy_cpp src/python/bindings.cpp)
target_compile_options(btpy_cpp PRIVATE -Wno-gnu-zero-variadic-macro-arguments)
target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY})

target_link_libraries(${BTCPP_LIBRARY} PUBLIC Python::Python pybind11::pybind11)
target_compile_definitions(${BTCPP_LIBRARY} PUBLIC BTCPP_PYTHON)
endif()

target_include_directories(${BTCPP_LIBRARY}
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
Expand Down
89 changes: 89 additions & 0 deletions btpy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env python3

"""
Top-level module of the BehaviorTree.CPP Python bindings.
"""

# re-export
from btpy_cpp import (
BehaviorTreeFactory,
NodeStatus,
StatefulActionNode,
SyncActionNode,
Tree,
)


def ports(inputs: list[str] = [], outputs: list[str] = []):
"""Decorator to specify input and outputs ports for an action node."""

def specify_ports(cls):
cls.input_ports = list(inputs)
cls.output_ports = list(outputs)
return cls

return specify_ports


class AsyncActionNode(StatefulActionNode):
"""An abstract action node implemented via cooperative multitasking.

Subclasses must implement the `run()` method as a generator. Optionally,
this method can return a final `NodeStatus` value to indicate its exit
condition.

Optionally, subclasses can override the `on_halted()` method which is called
when the tree halts. The default implementation does nothing. The `run()`
method will never be called again after a halt.

Note:
It is the responsibility of the action author to not block the main
behavior tree loop with long-running tasks. `yield` calls should be
placed whenever a pause is appropriate.
"""

def __init__(self, name, config):
super().__init__(name, config)

def on_start(self) -> NodeStatus:
self.coroutine = self.run()
return NodeStatus.RUNNING

def on_running(self) -> NodeStatus:
# The library logic should never allow this to happen, but users can
# still manually call `on_running` without an associated `on_start`
# call. Make sure to print a useful error when this happens.
if self.coroutine is None:
raise "AsyncActionNode run without starting"

# Resume the coroutine (generator). As long as the generator is not
# exhausted, keep this action in the RUNNING state.
try:
next(self.coroutine)
return NodeStatus.RUNNING
except StopIteration as e:
# If the action returns a status then propagate it upwards.
if e.value is not None:
return e.value
# Otherwise, just assume the action finished successfully.
else:
return NodeStatus.SUCCESS

def on_halted(self):
# Default action: do nothing
pass


# Specify the symbols to be imported with `from btpy import *`, as described in
# [1].
#
# [1]: https://docs.python.org/3/tutorial/modules.html#importing-from-a-package
__all__ = [
"ports",
"AsyncActionNode",
"BehaviorTreeFactory",
"NodeStatus",
"StatefulActionNode",
"SyncActionNode",
"Tree",
]
1 change: 1 addition & 0 deletions conanfile.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
gtest/1.12.1
zeromq/4.3.4
sqlite3/3.40.1
pybind11/2.10.4

[generators]
CMakeDeps
Expand Down
226 changes: 226 additions & 0 deletions include/behaviortree_cpp/contrib/pybind11_json.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/***************************************************************************
* Copyright (c) 2019, Martin Renou *
* *
* Distributed under the terms of the BSD 3-Clause License. *
* *
* The full license is in the file LICENSE, distributed with this software. *
****************************************************************************/

#ifndef PYBIND11_JSON_HPP
#define PYBIND11_JSON_HPP

#include <string>
#include <vector>

#include "behaviortree_cpp/contrib/json.hpp"

#include "pybind11/pybind11.h"

namespace pyjson
{
namespace py = pybind11;
namespace nl = nlohmann;

inline py::object from_json(const nl::json& j)
{
if (j.is_null())
{
return py::none();
}
else if (j.is_boolean())
{
return py::bool_(j.get<bool>());
}
else if (j.is_number_unsigned())
{
return py::int_(j.get<nl::json::number_unsigned_t>());
}
else if (j.is_number_integer())
{
return py::int_(j.get<nl::json::number_integer_t>());
}
else if (j.is_number_float())
{
return py::float_(j.get<double>());
}
else if (j.is_string())
{
return py::str(j.get<std::string>());
}
else if (j.is_array())
{
py::list obj(j.size());
for (std::size_t i = 0; i < j.size(); i++)
{
obj[i] = from_json(j[i]);
}
return obj;
}
else // Object
{
py::dict obj;
for (nl::json::const_iterator it = j.cbegin(); it != j.cend(); ++it)
{
obj[py::str(it.key())] = from_json(it.value());
}
return obj;
}
}

inline nl::json to_json(const py::handle& obj)
{
if (obj.ptr() == nullptr || obj.is_none())
{
return nullptr;
}
if (py::isinstance<py::bool_>(obj))
{
return obj.cast<bool>();
}
if (py::isinstance<py::int_>(obj))
{
try
{
nl::json::number_integer_t s = obj.cast<nl::json::number_integer_t>();
if (py::int_(s).equal(obj))
{
return s;
}
}
catch (...)
{
}
try
{
nl::json::number_unsigned_t u = obj.cast<nl::json::number_unsigned_t>();
if (py::int_(u).equal(obj))
{
return u;
}
}
catch (...)
{
}
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>());
}
if (py::isinstance<py::float_>(obj))
{
return obj.cast<double>();
}
if (py::isinstance<py::bytes>(obj))
{
py::module base64 = py::module::import("base64");
return base64.attr("b64encode")(obj).attr("decode")("utf-8").cast<std::string>();
}
if (py::isinstance<py::str>(obj))
{
return obj.cast<std::string>();
}
if (py::isinstance<py::tuple>(obj) || py::isinstance<py::list>(obj))
{
auto out = nl::json::array();
for (const py::handle value : obj)
{
out.push_back(to_json(value));
}
return out;
}
if (py::isinstance<py::dict>(obj))
{
auto out = nl::json::object();
for (const py::handle key : obj)
{
out[py::str(key).cast<std::string>()] = to_json(obj[key]);
}
return out;
}
throw std::runtime_error("to_json not implemented for this type of object: " + py::repr(obj).cast<std::string>());
}
}

// nlohmann_json serializers
namespace nlohmann
{
namespace py = pybind11;

#define MAKE_NLJSON_SERIALIZER_DESERIALIZER(T) \
template <> \
struct adl_serializer<T> \
{ \
inline static void to_json(json& j, const T& obj) \
{ \
j = pyjson::to_json(obj); \
} \
\
inline static T from_json(const json& j) \
{ \
return pyjson::from_json(j); \
} \
}

#define MAKE_NLJSON_SERIALIZER_ONLY(T) \
template <> \
struct adl_serializer<T> \
{ \
inline static void to_json(json& j, const T& obj) \
{ \
j = pyjson::to_json(obj); \
} \
}

MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::object);

MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::bool_);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::int_);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::float_);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::str);

MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::list);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::tuple);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::dict);

MAKE_NLJSON_SERIALIZER_ONLY(py::handle);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::item_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::list_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::tuple_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::sequence_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::str_attr_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::obj_attr_accessor);

#undef MAKE_NLJSON_SERIALIZER
#undef MAKE_NLJSON_SERIALIZER_ONLY
}

// pybind11 caster
namespace pybind11
{
namespace detail
{
template <> struct type_caster<nlohmann::json>
{
public:
PYBIND11_TYPE_CASTER(nlohmann::json, _("json"));

bool load(handle src, bool)
{
try
{
value = pyjson::to_json(src);
return true;
}
catch (...)
{
return false;
}
}

static handle cast(nlohmann::json src, return_value_policy /* policy */, handle /* parent */)
{
object obj = pyjson::from_json(src);
return obj.release();
}
};
}
}

#endif
Loading
Loading