.. SPDX-FileCopyrightText: 2024 The IceTray Contributors .. .. SPDX-License-Identifier: BSD-2-Clause .. _writing-pybindings: How to write pybindings for your project ======================================== .. highlight:: c++ #. Create a :file:`private/pybindings` directory in your project. #. Write functions to instantiate Python proxies for your classes using `boost::python `_. #. Write a module declaration that gathers your classes together in a common module. #. Write a :file:`CMakeLists.txt` for your :file:`private/pybindings` directory. #. Add the pybindings directory to your top-level :file:`CMakeLists.txt` with :code:`add_subdirectory('private/pybindings')` Registering your class with boost::python _________________________________________ Classes are registered by instantiating boost::python::class_. :: using namespace boost::python; scope particle_scope = class_, boost::shared_ptr >("I3Particle") .def("GetTime", &I3Particle::GetTime) .def("GetX", &I3Particle::GetX) .def("GetY", &I3Particle::GetY) .def("GetZ", &I3Particle::GetZ) ; Each of these methods returns the scope object, so they can be chained together. Wrapping containers ___________________ Vectors of your types can by exposed with: :: class_ >("I3MyClassSeries") .def(vector_indexing_suite >()) ; This will, among other things, make indexing and iteration work as expected for a Python object. Maps can be exposed with: :: class_ > >("I3MyClassSeriesMap") .def(std_map_indexing_suite > >()) ; In addition to indexing, this will make the wrapped map behave as much like a Python dictionary as possible. Wrapping enumerated types _________________________ Once you have exposed the methods of your class, you can expose enumerated types via :code:`boost::python::enum_`. :: enum_("FitStatus") .value("NotSet",I3Particle::NotSet) .value("OK",I3Particle::OK) .value("GeneralFailure",I3Particle::GeneralFailure) .value("InsufficientHits",I3Particle::InsufficientHits) .value("FailedToConverge",I3Particle::FailedToConverge) .value("MissingSeed",I3Particle::MissingSeed) .value("InsufficientQuality",I3Particle::InsufficientQuality) ; Declaring the module ____________________ The wrapping code for each class should be placed in its own function:: #include void register_I3Particle() { { boost::python::class_, boost::shared_ptr >("I3Particle") ... } } The Python module for the project is declared with the macro :c:macro:`I3_PYTHON_MODULE`. After loading your project's library, you can call the registration functions you defined for each class:: I3_PYTHON_MODULE(dataclasses) { load_project("dataclasses",false); void register_I3Particle(); void register_I3Position(); void register_I3RecoPulse(); } If you named all your registration functions :code:`register_*`, you can use preprocessor macros to save some typing:: #define REGISTER_THESE_THINGS (I3Particle)(I3Position)(I3RecoPulse) I3_PYTHON_MODULE(dataclasses) { load_project("dataclasses",false); BOOST_PP_SEQ_FOR_EACH(I3_REGISTER, ~, REGISTER_THESE_THINGS); } Helpful preprocessor macros ___________________________ Writing pybindings can involve plenty of boilerplate code. Luckily, we include some macros that can be used with Boost preprocessor sequences to reduce the tedium. Boost preprocessor sequences ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The header :code:`` defines macros that can manipulate sequences. A sequence is a series of parenthesized tokens: :: #define MY_SEQUENCE (a)(whole)(bunch)(of)(tokens) These tokens can be expanded with .. c:macro:: BOOST_PP_SEQ_FOR_EACH(Macro, Data, Seq) Expand a sequence in place. :param Macro: A macro that takes three parameters: the head of the sequence, auxiliary data in Data, and an element of Seq. :param Data: Arbitrary data to be passed to every call of Macro. :param Seq: A sequence of tokens. Each of these tokens will be passed to Macro. Most of the macros mentioned here can be used with :c:macro:`BOOST_PP_SEQ_FOR_EACH` to automate repetitive declarations. The following macros are defined in :file:`cmake/I3.h.in`: Wrapping methods verbatim ^^^^^^^^^^^^^^^^^^^^^^^^^ .. c:macro:: WRAP_DEF(R, Class, Fn) Method-wrapping macro suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: Parent class of the member function :param Fn: Name of the member function This macro can be used to expose your interface to Python exactly as it is in C++:: #define METHODS_TO_WRAP (GetTime)(GetX)(GetY)(GetZ) BOOST_PP_SEQ_FOR_EACH(WRAP_DEF, I3Particle, METHODS_TO_WRAP) Since the Get/Set pattern is fairly common, there are iterable macros specifically for Get/Set. With these, one sequence can be used to define C++-style Get/Set methods and Python-style properties (see :c:macro:`WRAP_PROP`). .. c:macro:: WRAP_GET(R, Class, Name) Define GetName(). Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: The parent C++ class. :param Name: The base name of the Get method. .. c:macro:: WRAP_GETSET(R, Class, Name) Define GetName() and SetName(). Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: The parent C++ class. :param Name: The base name of the Get/Set methods. **Example**:: #define NAMES_TO_WRAP (Time)(X)(Y)(Z) BOOST_PP_SEQ_FOR_EACH(WRAP_GETSET, I3Particle, NAMES_TO_WRAP) BOOST_PP_SEQ_FOR_EACH(WRAP_PROP, I3Particle, NAMES_TO_WRAP) There are also versions of these macros (:c:macro:`WRAP_GET_INTERNAL_REFERENCE` and :c:macro:`WRAP_GETSET_INTERNAL_REFERENCE`) that return a reference rather than a copy. Exposing private member data via Get/Set ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you want to be nice to your users, you can wrap your Get/Set methods in Python properties: .. c:macro:: PROPERTY(Class, Prop, Fn) Add ``Class.Prop`` as a property with getter/setter functions :code:`GetFn()` / :code:`SetFn()` :param Class: Parent C++ class :param Prop: The name of the Python property :param Fn: The base name of the C++ Get/Set functions .. c:macro:: PROPERTY_TYPE(Class, Prop, GotType, Fn) Add ``Class.Prop`` as a property with getter/setter functions :code:`GetFn()` / :code:`SetFn()`, specifying that :code:`GetFn()` returns ``GotType``. This is useful when wrapping overloaded getter functions. :param Class: Parent C++ class :param Prop: The name of the Python property :param GotType: The type returned by GetFn() :param Fn: The base name of the C++ Get/Set functions .. c:macro:: WRAP_PROP(R, Class, Fn) Add ``Class.fn`` as a property with getter/setter functions :code:`GetFn()` / :code:`SetFn()`. Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: Parent C++ class :param Fn: The name of the Python property and base name of the Get/Set functions .. c:macro:: WRAP_PROP_RO(R, Class, Fn) Add ``Class.fn`` as a property with getter function :code:`GetFn()`. Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: Parent C++ class :param Fn: The name of the Python property and base name of the Get function **Example**:: #define DATA_TO_WRAP (Time)(X)(Y)(Z) BOOST_PP_SEQ_FOR_EACH(WRAP_PROP, I3Particle, DATA_TO_WRAP) Now in Python, I3Particle.x (yes, lowercase) will call and return I3Particle::GetX() and I3Particle.x = 0 will call I3Particle::SetX(0). For finer-grained control of the Python property name, use the trinary form: :: PROPERTY(I3Particle, partyTime, Time) Exposing public member data with access restrictions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can expose public member data as properties, either read/write or read-only: .. c:macro:: WRAP_RW(R, Class, Member) Expose Member as a read/write Python property. Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: Parent C++ class :param Member: Name of public data member and Python property .. c:macro:: WRAP_RO(R, Class, Member) Expose Member as a read-only Python property. Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: Parent C++ class :param Member: Name of public data member and Python property **Example**:: #define MEMBERS_TO_WRAP (value)(some_other_value) BOOST_PP_SEQ_FOR_EACH(WRAP_RO, I3MyClass, MEMBERS_TO_WRAP) Wrapping methods with call policies ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you need finer-grained control of the return type of your wrapped methods, you can use the following macros: .. c:macro:: GETSET(Objtype, GotType, Name) Define getter/setter methods to return by value. :param Objtype: The parent C++ class. :param GotType: The type of object returned by Get() :param Name: The base name of the Get/Set methods. For a name ``X``, this will define :code:`Objtype::GetX()` to return a ``GotType`` by value. This is appropriate for POD like ints and doubles. It will also define :code:`SetX()`. .. c:macro:: GETSET_INTERNAL_REFERENCE(Objtype, GotType, Name) Define getter/setter methods to return by reference. :param Objtype: The parent C++ class. :param GotType: The type of object returned by Get() :param Name: The base name of the Get/Set methods. This will define :code:`Objtype::GetX()` to return a reference to ``GotType``, where ``GotType`` is still owned by the parent object. This is appropriate for compound objects like vectors and maps. There are also trinary versions of these macros for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`: .. c:macro:: WRAP_GET_INTERNAL_REFERENCE(R, Class, Name) Define :code:`GetName()` to return an internal reference. Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: The parent C++ class. :param Name: The base name of the Get method. .. c:macro:: WRAP_GETSET_INTERNAL_REFERENCE(R, Class, Name) Define :code:`GetName()` and :code:`SetName()`. :code:`GetName()` will return an internal reference. Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: The parent C++ class. :param Name: The base name of the Get/Set methods. .. c:macro:: WRAP_PROP_RO_INTERNAL_REFERENCE(R, Class, Fn) Add ``Class.fn`` as a property with getter function :code:`GetFn()`. :code:`GetFn()` will return a reference to the object owned by the C++ instance. Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: Parent C++ class :param Fn: The name of the Python property and base name of the Get function .. c:macro:: WRAP_PROP_INTERNAL_REFERENCE(R, Class, Fn) Add ``Class.fn`` as a property with getter/setter functions :code:`GetFn()` / :code:`SetFn()`. :code:`GetFn()` will return a reference to the object owned by the C++ instance. Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: Parent C++ class :param Fn: The name of the Python property and base name of the Get/Set functions Wrapping enumerated types ^^^^^^^^^^^^^^^^^^^^^^^^^ .. c:macro:: WRAP_ENUM_VALUE(R, Class, Name) Add the value Name to an enumerated type. Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param Class: The parent C++ class. :param Name: The name of the C++ value. **Example**:: enum_("FitStatus") #define FIT_STATUS (NotSet)(OK)(GeneralFailure)(InsufficientHits) \ (FailedToConverge)(MissingSpeed)(InsufficientQuality) BOOST_PP_SEQ_FOR_EACH(WRAP_ENUM_VALUE, I3Particle, FIT_STATUS) ; Constructing a Python module ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. c:macro:: I3_REGISTER(r, data, t) For name ``Name``, call :code:`void register_Name()`. Suitable for use with :c:macro:`BOOST_PP_SEQ_FOR_EACH`. :param t: The suffix of the function name, e.g. register_t(). :param data: unused. .. c:macro:: I3_PYTHON_MODULE(module_name) Declare the following code to be run when the module is initialized. :param module_name: The name of the Python module. Must be a legal Python variable name. **Example**:: #define REGISTER_THESE_THINGS (I3Particle)(I3Position)(I3RecoPulse) I3_PYTHON_MODULE(dataclasses) { load_project("dataclasses",false); BOOST_PP_SEQ_FOR_EACH(I3_REGISTER, ~, REGISTER_THESE_THINGS); } Gotchas _______ errors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You may be mystified by errors like these: .. code-block:: none error: No match for 'boost::python::class_< I3MCPMTResponse, boost::shared_ptr, boost::python::detail::not_specified, boost::python::detail::not_specified >::def(const char [11], )' This can happen when the wrapped class exposes two different versions of the function, for example returning a const or non-const type. In this case, you have to specify the return type by hand. The :c:macro:`BOOST_PP_SEQ_FOR_EACH` tricks will not work; you'll need to use :c:macro:`GETSET` or :c:macro:`PROPERTY_TYPE` to wrap each name individually instead. Naming conventions __________________ Python properties are preferred over C++-style Get/Set methods. The exposed Python module should conform our :ref:`python-coding-standards` as closely as possible. Resources _________ - `Boost::Python wiki at python.org `_ - `Boost::Python reference guide `_ Todo: finer points of return-by-value vs. reference ___________________________________________________