How to write pybindings for your project

  1. Create a private/pybindings directory in your project.

  2. Write functions to instantiate Python proxies for your classes using boost::python.

  3. Write a module declaration that gathers your classes together in a common module.

  4. Write a CMakeLists.txt for your private/pybindings directory.

  5. Add the pybindings directory to your top-level CMakeLists.txt with add_subdirectory('private/pybindings')

Registering your class with boost::python

Classes are registered by instantiating boost::python::class_<YourClass>.

using namespace boost::python;

scope particle_scope =
  class_<I3Particle, bases<I3FrameObject>, boost::shared_ptr<I3Particle> >("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_<I3Vector<I3MyClass > >("I3MyClassSeries")
  .def(vector_indexing_suite<I3Vector<I3MyClass > >())
;

This will, among other things, make indexing and iteration work as expected for a Python object.

Maps can be exposed with:

class_<I3Map<OMKey,I3Vector<I3MyClass > > >("I3MyClassSeriesMap")
  .def(std_map_indexing_suite<I3Map<OMKey,I3Vector<I3MyClass > > >())
;

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 boost::python::enum_<I3MyClass::MyEnumType>.

enum_<I3Particle::FitStatus>("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 <dataclasses/physics/I3Particle.h>

void register_I3Particle()
{
 {
  boost::python::class_<I3Particle,
                        bases<I3FrameObject>,
                        boost::shared_ptr<I3Particle > >("I3Particle")
  ...
 }
}

The Python module for the project is declared with the 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 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 <boost/preprocessor/seq.hpp> 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

BOOST_PP_SEQ_FOR_EACH(Macro, Data, Seq)

Expand a sequence in place.

Parameters:
  • Macro – A macro that takes three parameters: the head of the sequence, auxiliary data in Data, and an element of Seq.

  • Data – Arbitrary data to be passed to every call of Macro.

  • Seq – A sequence of tokens. Each of these tokens will be passed to Macro.

Most of the macros mentioned here can be used with BOOST_PP_SEQ_FOR_EACH to automate repetitive declarations.

The following macros are defined in cmake/I3.h.in:

Wrapping methods verbatim

WRAP_DEF(R, Class, Fn)

Method-wrapping macro suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – Parent class of the member function

  • 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 WRAP_PROP).

WRAP_GET(R, Class, Name)

Define GetName(). Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – The parent C++ class.

  • Name – The base name of the Get method.

WRAP_GETSET(R, Class, Name)

Define GetName() and SetName(). Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – The parent C++ class.

  • 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 (WRAP_GET_INTERNAL_REFERENCE and 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:

PROPERTY(Class, Prop, Fn)

Add Class.Prop as a property with getter/setter functions GetFn() / SetFn()

Parameters:
  • Class – Parent C++ class

  • Prop – The name of the Python property

  • Fn – The base name of the C++ Get/Set functions

PROPERTY_TYPE(Class, Prop, GotType, Fn)

Add Class.Prop as a property with getter/setter functions GetFn() / SetFn(), specifying that GetFn() returns GotType. This is useful when wrapping overloaded getter functions.

Parameters:
  • Class – Parent C++ class

  • Prop – The name of the Python property

  • GotType – The type returned by GetFn()

  • Fn – The base name of the C++ Get/Set functions

WRAP_PROP(R, Class, Fn)

Add Class.fn as a property with getter/setter functions GetFn() / SetFn(). Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – Parent C++ class

  • Fn – The name of the Python property and base name of the Get/Set functions

WRAP_PROP_RO(R, Class, Fn)

Add Class.fn as a property with getter function GetFn(). Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – Parent C++ class

  • 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:

WRAP_RW(R, Class, Member)

Expose Member as a read/write Python property. Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – Parent C++ class

  • Member – Name of public data member and Python property

WRAP_RO(R, Class, Member)

Expose Member as a read-only Python property. Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – Parent C++ class

  • 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:

GETSET(Objtype, GotType, Name)

Define getter/setter methods to return by value.

Parameters:
  • Objtype – The parent C++ class.

  • GotType – The type of object returned by Get()

  • Name – The base name of the Get/Set methods.

For a name X, this will define Objtype::GetX() to return a GotType by value. This is appropriate for POD like ints and doubles. It will also define SetX().

GETSET_INTERNAL_REFERENCE(Objtype, GotType, Name)

Define getter/setter methods to return by reference.

Parameters:
  • Objtype – The parent C++ class.

  • GotType – The type of object returned by Get()

  • Name – The base name of the Get/Set methods.

This will define 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 BOOST_PP_SEQ_FOR_EACH:

WRAP_GET_INTERNAL_REFERENCE(R, Class, Name)

Define GetName() to return an internal reference. Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – The parent C++ class.

  • Name – The base name of the Get method.

WRAP_GETSET_INTERNAL_REFERENCE(R, Class, Name)

Define GetName() and SetName(). GetName() will return an internal reference. Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – The parent C++ class.

  • Name – The base name of the Get/Set methods.

WRAP_PROP_RO_INTERNAL_REFERENCE(R, Class, Fn)

Add Class.fn as a property with getter function GetFn(). GetFn() will return a reference to the object owned by the C++ instance. Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – Parent C++ class

  • Fn – The name of the Python property and base name of the Get function

WRAP_PROP_INTERNAL_REFERENCE(R, Class, Fn)

Add Class.fn as a property with getter/setter functions GetFn() / SetFn(). GetFn() will return a reference to the object owned by the C++ instance. Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – Parent C++ class

  • Fn – The name of the Python property and base name of the Get/Set functions

Wrapping enumerated types

WRAP_ENUM_VALUE(R, Class, Name)

Add the value Name to an enumerated type. Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • Class – The parent C++ class.

  • Name – The name of the C++ value.

Example:

enum_<I3Particle::FitStatus>("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

I3_REGISTER(r, data, t)

For name Name, call void register_Name(). Suitable for use with BOOST_PP_SEQ_FOR_EACH.

Parameters:
  • t – The suffix of the function name, e.g. register_t().

  • data – unused.

I3_PYTHON_MODULE(module_name)

Declare the following code to be run when the module is initialized.

Parameters:
  • 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

<unresolved overloaded function type> errors

You may be mystified by errors like these:

error: No match for 'boost::python::class_<
    I3MCPMTResponse, boost::shared_ptr<I3MCPMTResponse>,
    boost::python::detail::not_specified,
    boost::python::detail::not_specified
>::def(const char [11], <unresolved overloaded function type>)'

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 BOOST_PP_SEQ_FOR_EACH tricks will not work; you’ll need to use GETSET or 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 Python Coding Standards as closely as possible.

Resources

Todo: finer points of return-by-value vs. reference