C++ YASMIN (Yet Another State MachINe)
Loading...
Searching...
No Matches
blackboard_pywrapper.hpp
Go to the documentation of this file.
1// Copyright (C) 2025 Miguel Ángel González Santamarta
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16#ifndef YASMIN__BLACKBOARD_PYWRAPPER_HPP_
17#define YASMIN__BLACKBOARD_PYWRAPPER_HPP_
18
19#include <pybind11/cast.h>
20#include <pybind11/pybind11.h>
21#include <pybind11/stl.h>
22
23#include <cstdint>
24#include <stdexcept>
25#include <string>
26#include <typeinfo>
27#include <unordered_map>
28#include <vector>
29
30#include "yasmin/blackboard.hpp"
31#include "yasmin/types.hpp"
32
33namespace py = pybind11;
34
35namespace yasmin {
36
68private:
71
73 using StringVector = std::vector<std::string>;
75 using IntVector = std::vector<std::int64_t>;
77 using FloatVector = std::vector<double>;
79 using BoolVector = std::vector<bool>;
80
82 using StringDict = std::unordered_map<std::string, std::string>;
84 using IntDict = std::unordered_map<std::string, std::int64_t>;
86 using FloatDict = std::unordered_map<std::string, double>;
88 using BoolDict = std::unordered_map<std::string, bool>;
89
96 static std::vector<uint8_t> py_buffer_to_vector(const py::handle &value) {
97 char *data = nullptr;
98 py::ssize_t size = 0;
99
100 if (PyBytes_Check(value.ptr()) != 0) {
101 if (PyBytes_AsStringAndSize(value.ptr(), &data, &size) != 0) {
102 throw py::error_already_set();
103 }
104 } else if (PyByteArray_Check(value.ptr()) != 0) {
105 data = PyByteArray_AsString(value.ptr());
106 size = PyByteArray_Size(value.ptr());
107 } else {
108 throw std::runtime_error("Expected bytes or bytearray object");
109 }
110
111 return std::vector<uint8_t>(reinterpret_cast<uint8_t *>(data),
112 reinterpret_cast<uint8_t *>(data) + size);
113 }
114
121 template <typename ByteT>
122 static py::bytes byte_vector_to_py_bytes(const std::vector<ByteT> &value) {
123 return py::bytes(reinterpret_cast<const char *>(value.data()),
124 static_cast<py::ssize_t>(value.size()));
125 }
126
132 static bool is_python_bytes_like(const py::handle &value) {
133 return PyBytes_Check(value.ptr()) != 0 ||
134 PyByteArray_Check(value.ptr()) != 0;
135 }
136
145 static bool is_python_int_like(const py::handle &value) {
146 return py::isinstance<py::int_>(value) && !py::isinstance<py::bool_>(value);
147 }
148
154 static bool is_python_float_like(const py::handle &value) {
155 return py::isinstance<py::float_>(value);
156 }
157
163 static bool is_python_number_like(const py::handle &value) {
164 return is_python_int_like(value) || is_python_float_like(value);
165 }
166
172 static bool is_python_sequence_like(const py::handle &value) {
173 return py::isinstance<py::list>(value) || py::isinstance<py::tuple>(value);
174 }
175
183 template <typename Predicate>
184 static bool sequence_matches(const py::sequence &seq, Predicate pred) {
185 for (auto item : seq) {
186 if (!pred(item)) {
187 return false;
188 }
189 }
190 return true;
191 }
192
199 template <typename T>
200 static std::vector<T> sequence_to_vector(const py::sequence &seq) {
201 std::vector<T> result;
202 result.reserve(static_cast<std::size_t>(py::len(seq)));
203
204 for (auto item : seq) {
205 result.push_back(py::cast<T>(item));
206 }
207
208 return result;
209 }
210
216 static bool dict_has_only_string_keys(const py::dict &dict) {
217 for (auto item : dict) {
218 if (!py::isinstance<py::str>(item.first)) {
219 return false;
220 }
221 }
222 return true;
223 }
224
232 template <typename Predicate>
233 static bool dict_values_match(const py::dict &dict, Predicate pred) {
234 for (auto item : dict) {
235 if (!pred(item.second)) {
236 return false;
237 }
238 }
239 return true;
240 }
241
248 template <typename T>
249 static std::unordered_map<std::string, T>
250 dict_to_unordered_map(const py::dict &dict) {
251 std::unordered_map<std::string, T> result;
252 result.reserve(static_cast<std::size_t>(py::len(dict)));
253
254 for (auto item : dict) {
255 result.emplace(py::cast<std::string>(item.first),
256 py::cast<T>(item.second));
257 }
258
259 return result;
260 }
261
271 template <typename T> static bool is_exact_cpp_type(const std::string &type) {
272 return type == demangle_type(typeid(T).name());
273 }
274
275public:
280
286 : blackboard(Blackboard::make_shared(std::move(other))) {}
287
293 : blackboard(std::move(bb_ptr)) {}
294
303 void set(const std::string &key, py::object value) {
304 if (py::isinstance<py::bool_>(value)) {
305 this->blackboard->set<bool>(key, value.cast<bool>());
306 return;
307 }
308
309 if (is_python_int_like(value)) {
310 this->blackboard->set<std::int64_t>(key, value.cast<std::int64_t>());
311 return;
312 }
313
314 if (py::isinstance<py::float_>(value)) {
315 this->blackboard->set<double>(key, value.cast<double>());
316 return;
317 }
318
319 if (is_python_bytes_like(value)) {
320 this->blackboard->set<std::vector<uint8_t>>(key,
321 py_buffer_to_vector(value));
322 return;
323 }
324
325 if (py::isinstance<py::str>(value)) {
326 this->blackboard->set<std::string>(key, value.cast<std::string>());
327 return;
328 }
329
330 if (is_python_sequence_like(value)) {
331 py::sequence seq = value.cast<py::sequence>();
332
333 // Empty sequences do not carry enough type information to infer a native
334 // element type safely.
335 if (py::len(seq) == 0) {
336 this->blackboard->set<py::object>(key, value);
337 return;
338 }
339
340 // Bool must be checked before int because Python bool is also an int.
341 if (sequence_matches(seq, [](const py::handle &item) {
342 return py::isinstance<py::bool_>(item);
343 })) {
344 this->blackboard->set<BoolVector>(key, sequence_to_vector<bool>(seq));
345 return;
346 }
347
348 if (sequence_matches(seq, [](const py::handle &item) {
349 return is_python_int_like(item);
350 })) {
351 this->blackboard->set<IntVector>(key,
353 return;
354 }
355
356 // Mixed int/float sequences are normalized to double.
357 if (sequence_matches(seq, [](const py::handle &item) {
358 return is_python_number_like(item);
359 })) {
360 this->blackboard->set<FloatVector>(key,
362 return;
363 }
364
365 if (sequence_matches(seq, [](const py::handle &item) {
366 return py::isinstance<py::str>(item);
367 })) {
368 this->blackboard->set<StringVector>(
370 return;
371 }
372
373 this->blackboard->set<py::object>(key, value);
374 return;
375 }
376
377 if (py::isinstance<py::dict>(value)) {
378 py::dict dict = value.cast<py::dict>();
379
380 // Only dict[str, T] with a homogeneous value type is converted into a
381 // native unordered_map.
382 if (py::len(dict) == 0 || !dict_has_only_string_keys(dict)) {
383 this->blackboard->set<py::object>(key, value);
384 return;
385 }
386
387 if (dict_values_match(dict, [](const py::handle &item) {
388 return py::isinstance<py::bool_>(item);
389 })) {
390 this->blackboard->set<BoolDict>(key, dict_to_unordered_map<bool>(dict));
391 return;
392 }
393
394 if (dict_values_match(dict, [](const py::handle &item) {
395 return is_python_int_like(item);
396 })) {
397 this->blackboard->set<IntDict>(
399 return;
400 }
401
402 // Mixed int/float dictionaries are normalized to double.
403 if (dict_values_match(dict, [](const py::handle &item) {
404 return is_python_number_like(item);
405 })) {
406 this->blackboard->set<FloatDict>(key,
408 return;
409 }
410
411 if (dict_values_match(dict, [](const py::handle &item) {
412 return py::isinstance<py::str>(item);
413 })) {
414 this->blackboard->set<StringDict>(
416 return;
417 }
418
419 this->blackboard->set<py::object>(key, value);
420 return;
421 }
422
423 this->blackboard->set<py::object>(key, value);
424 }
425
434 py::object get(const std::string &key) const {
435 const std::string type = this->blackboard->get_type(key);
436
437 if (is_exact_cpp_type<std::vector<uint8_t>>(type)) {
439 this->blackboard->get<std::vector<uint8_t>>(key));
440 }
441
442 if (is_exact_cpp_type<std::vector<unsigned char>>(type)) {
444 this->blackboard->get<std::vector<unsigned char>>(key));
445 }
446
447 if (is_exact_cpp_type<std::vector<char>>(type)) {
449 this->blackboard->get<std::vector<char>>(key));
450 }
451
453 return py::cast(this->blackboard->get<StringVector>(key));
454 }
455
457 return py::cast(this->blackboard->get<IntVector>(key));
458 }
459
460 if (is_exact_cpp_type<std::vector<int>>(type)) {
461 return py::cast(this->blackboard->get<std::vector<int>>(key));
462 }
463
464 if (is_exact_cpp_type<std::vector<long>>(type)) {
465 return py::cast(this->blackboard->get<std::vector<long>>(key));
466 }
467
468 if (is_exact_cpp_type<std::vector<long long>>(type)) {
469 return py::cast(this->blackboard->get<std::vector<long long>>(key));
470 }
471
473 return py::cast(this->blackboard->get<FloatVector>(key));
474 }
475
476 if (is_exact_cpp_type<std::vector<float>>(type)) {
477 return py::cast(this->blackboard->get<std::vector<float>>(key));
478 }
479
481 return py::cast(this->blackboard->get<BoolVector>(key));
482 }
483
485 return py::cast(this->blackboard->get<StringDict>(key));
486 }
487
488 if (is_exact_cpp_type<IntDict>(type)) {
489 return py::cast(this->blackboard->get<IntDict>(key));
490 }
491
492 if (is_exact_cpp_type<std::unordered_map<std::string, int>>(type)) {
493 return py::cast(
494 this->blackboard->get<std::unordered_map<std::string, int>>(key));
495 }
496
497 if (is_exact_cpp_type<std::unordered_map<std::string, long>>(type)) {
498 return py::cast(
499 this->blackboard->get<std::unordered_map<std::string, long>>(key));
500 }
501
502 if (is_exact_cpp_type<std::unordered_map<std::string, long long>>(type)) {
503 return py::cast(
504 this->blackboard->get<std::unordered_map<std::string, long long>>(
505 key));
506 }
507
509 return py::cast(this->blackboard->get<FloatDict>(key));
510 }
511
512 if (is_exact_cpp_type<std::unordered_map<std::string, float>>(type)) {
513 return py::cast(
514 this->blackboard->get<std::unordered_map<std::string, float>>(key));
515 }
516
517 if (is_exact_cpp_type<BoolDict>(type)) {
518 return py::cast(this->blackboard->get<BoolDict>(key));
519 }
520
522 return py::cast(this->blackboard->get<std::string>(key));
523 }
524
526 return py::cast(this->blackboard->get<std::int64_t>(key));
527 }
528
529 if (is_exact_cpp_type<int>(type)) {
530 return py::cast(this->blackboard->get<int>(key));
531 }
532
533 if (is_exact_cpp_type<long>(type)) {
534 return py::cast(this->blackboard->get<long>(key));
535 }
536
537 if (is_exact_cpp_type<float>(type)) {
538 return py::cast(this->blackboard->get<float>(key));
539 }
540
541 if (is_exact_cpp_type<double>(type)) {
542 return py::cast(this->blackboard->get<double>(key));
543 }
544
545 if (is_exact_cpp_type<bool>(type)) {
546 return py::cast(this->blackboard->get<bool>(key));
547 }
548
550 return this->blackboard->get<py::object>(key);
551 }
552
553 return py::none();
554 }
555
560 void remove(const std::string &key) { this->blackboard->remove(key); }
561
567 bool contains(const std::string &key) const {
568 return this->blackboard->contains(key);
569 }
570
575 int size() const { return this->blackboard->size(); }
576
581 std::vector<std::string> keys() const { return this->blackboard->keys(); }
582
587 py::list values() const {
588 py::list result;
589
590 for (const auto &key : this->blackboard->keys()) {
591 result.append(this->get(key));
592 }
593
594 return result;
595 }
596
601 py::list items() const {
602 py::list result;
603
604 for (const auto &key : this->blackboard->keys()) {
605 result.append(py::make_tuple(key, this->get(key)));
606 }
607
608 return result;
609 }
610
615 std::string to_string() const { return this->blackboard->to_string(); }
616
621 void set_remappings(const Remappings &remappings) {
622 this->blackboard->set_remappings(remappings);
623 }
624
629 const Remappings &get_remappings() const {
630 return this->blackboard->get_remappings();
631 }
632
637 Blackboard::SharedPtr get_cpp_blackboard() const { return this->blackboard; }
638};
639
640} // namespace yasmin
641
642#endif // YASMIN__BLACKBOARD_PYWRAPPER_HPP_
static bool dict_values_match(const py::dict &dict, Predicate pred)
Check whether all values of a Python dict satisfy a predicate.
Definition blackboard_pywrapper.hpp:233
void set_remappings(const Remappings &remappings)
Set key remappings on the underlying blackboard.
Definition blackboard_pywrapper.hpp:621
Blackboard::SharedPtr blackboard
Shared pointer to the underlying native blackboard.
Definition blackboard_pywrapper.hpp:70
std::unordered_map< std::string, std::string > StringDict
Alias for a typed dictionary with string values.
Definition blackboard_pywrapper.hpp:82
py::list items() const
Get the key-value pairs visible in the current remapping scope.
Definition blackboard_pywrapper.hpp:601
static std::vector< uint8_t > py_buffer_to_vector(const py::handle &value)
Convert a Python bytes-like object into a byte vector.
Definition blackboard_pywrapper.hpp:96
static std::unordered_map< std::string, T > dict_to_unordered_map(const py::dict &dict)
Convert a Python dict with string keys into a typed unordered_map.
Definition blackboard_pywrapper.hpp:250
static bool dict_has_only_string_keys(const py::dict &dict)
Check whether all keys in a Python dict are strings.
Definition blackboard_pywrapper.hpp:216
BlackboardPyWrapper(Blackboard &&other)
Construct a wrapper by moving an existing native Blackboard.
Definition blackboard_pywrapper.hpp:285
static bool is_python_float_like(const py::handle &value)
Check whether a Python object is a floating point value.
Definition blackboard_pywrapper.hpp:154
std::unordered_map< std::string, double > FloatDict
Alias for a typed dictionary with floating point values.
Definition blackboard_pywrapper.hpp:86
std::vector< std::int64_t > IntVector
Alias for a typed vector of integers.
Definition blackboard_pywrapper.hpp:75
int size() const
Get the number of entries in the blackboard.
Definition blackboard_pywrapper.hpp:575
BlackboardPyWrapper()
Construct a wrapper with a newly created native Blackboard.
Definition blackboard_pywrapper.hpp:279
py::list values() const
Get the values visible in the current remapping scope.
Definition blackboard_pywrapper.hpp:587
std::unordered_map< std::string, bool > BoolDict
Alias for a typed dictionary with boolean values.
Definition blackboard_pywrapper.hpp:88
static bool is_python_bytes_like(const py::handle &value)
Check whether a Python object is bytes-like.
Definition blackboard_pywrapper.hpp:132
static bool is_python_number_like(const py::handle &value)
Check whether a Python object is a numeric value.
Definition blackboard_pywrapper.hpp:163
static bool is_python_sequence_like(const py::handle &value)
Check whether a Python object should be treated as a sequence.
Definition blackboard_pywrapper.hpp:172
static bool sequence_matches(const py::sequence &seq, Predicate pred)
Check whether all elements of a Python sequence satisfy a predicate.
Definition blackboard_pywrapper.hpp:184
std::vector< std::string > StringVector
Alias for a typed vector of strings.
Definition blackboard_pywrapper.hpp:73
static std::vector< T > sequence_to_vector(const py::sequence &seq)
Convert a Python sequence into a typed C++ vector.
Definition blackboard_pywrapper.hpp:200
py::object get(const std::string &key) const
Retrieve a Python object from the blackboard.
Definition blackboard_pywrapper.hpp:434
std::vector< double > FloatVector
Alias for a typed vector of floating point values.
Definition blackboard_pywrapper.hpp:77
static bool is_exact_cpp_type(const std::string &type)
Compare a stored type string against an exact C++ type.
Definition blackboard_pywrapper.hpp:271
std::vector< bool > BoolVector
Alias for a typed vector of booleans.
Definition blackboard_pywrapper.hpp:79
const Remappings & get_remappings() const
Get the current key remappings.
Definition blackboard_pywrapper.hpp:629
std::unordered_map< std::string, std::int64_t > IntDict
Alias for a typed dictionary with integer values.
Definition blackboard_pywrapper.hpp:84
static bool is_python_int_like(const py::handle &value)
Check whether a Python object is an integer but not a boolean.
Definition blackboard_pywrapper.hpp:145
void set(const std::string &key, py::object value)
Store a Python object in the blackboard.
Definition blackboard_pywrapper.hpp:303
std::string to_string() const
Get a string representation of the blackboard contents.
Definition blackboard_pywrapper.hpp:615
Blackboard::SharedPtr get_cpp_blackboard() const
Access the underlying native blackboard.
Definition blackboard_pywrapper.hpp:637
BlackboardPyWrapper(Blackboard::SharedPtr bb_ptr)
Construct a wrapper from an existing shared native Blackboard.
Definition blackboard_pywrapper.hpp:292
static py::bytes byte_vector_to_py_bytes(const std::vector< ByteT > &value)
Convert a C++ byte-like vector into Python bytes.
Definition blackboard_pywrapper.hpp:122
void remove(const std::string &key)
Remove a value from the blackboard.
Definition blackboard_pywrapper.hpp:560
bool contains(const std::string &key) const
Check whether a key exists in the blackboard.
Definition blackboard_pywrapper.hpp:567
std::vector< std::string > keys() const
Get the keys visible in the current remapping scope.
Definition blackboard_pywrapper.hpp:581
A thread-safe storage for key-value pairs of varying types.
Definition blackboard.hpp:67
std::shared_ptr< Blackboard > SharedPtr
Shared pointer type for Blackboard.
Definition blackboard.hpp:100
Definition blackboard.hpp:34
std::string demangle_type(const std::string &mangled_name)
Demangle a C++ type name to a human-readable format.
Definition blackboard.hpp:41
StringMap Remappings
Map of remappings (string to string).
Definition types.hpp:88