How to validated a boost::python::object argument is a python function signature with an argument?
void subscribe_py(boost::python::object callback){
//check callback is a function signature
}
Boost.Python does not provide a higher-level type to help perform introspection. However, one can use the Python C-API's PyCallable_Check()
to check if a Python object is callable, and then use a Python introspection module, such as inspect
, to determine the callable object's signature. Boost.Python's interoperability between C++ and the Python makes this fairly seamless to use Python modules.
Here is an auxiliary function, require_arity(fn, n)
that requires the expression fn(a_0, a_1, ... a_n)
to be valid:
/// @brief Given a Python object `fn` and an arity of `n`, requires
/// that the expression `fn(a_0, a_1, ..., a_2` to be valid.
/// Raise TypeError if `fn` is not callable and `ValueError`
/// if `fn` is callable, but has the wrong arity.
void require_arity(
std::string name,
boost::python::object fn,
std::size_t arity)
{
namespace python = boost::python;
std::stringstream error_msg;
error_msg << name << "() must take exactly " << arity << " arguments";
// Throw if the callback is not callable.
if (!PyCallable_Check(fn.ptr()))
{
PyErr_SetString(PyExc_TypeError, error_msg.str().c_str());
python::throw_error_already_set();
}
// Use the inspect module to extract the arg spec.
// >>> import inspect
auto inspect = python::import("inspect");
// >>> args, varargs, keywords, defaults = inspect.getargspec(fn)
auto arg_spec = inspect.attr("getargspec")(fn);
python::object args = arg_spec[0];
python::object varargs = arg_spec[1];
python::object defaults = arg_spec[3];
// Calculate the number of required arguments.
auto args_count = args ? python::len(args) : 0;
auto defaults_count = defaults ? python::len(defaults) : 0;
// If the function is a bound method or a class method, then the
// first argument (`self` or `cls`) will be implicitly provided.
// >>> has_self = inspect.ismethod(fn) and fn.__self__ is not None
if (static_cast<bool>(inspect.attr("ismethod")(fn))
&& fn.attr("__self__"))
{
--args_count;
}
// Require at least one argument. The function should support
// any of the following specs:
// >>> fn(a1)
// >>> fn(a1, a2=42)
// >>> fn(a1=42)
// >>> fn(*args)
auto required_count = args_count - defaults_count;
if (!( (required_count == 1) // fn(a1), fn(a1, a2=42)
|| (args_count > 0 && required_count == 0) // fn(a1=42)
|| (varargs) // fn(*args)
))
{
PyErr_SetString(PyExc_ValueError, error_msg.str().c_str());
python::throw_error_already_set();
}
}
And its usage would be:
void subscribe_py(boost::python::object callback)
{
require_arity("callback", callback, 1); // callback(a1) is valid
...
}
Here is a complete example demonstrating the usage:
#include <boost/python.hpp>
#include <sstream>
/// @brief Given a Python object `fn` and an arity of `n`, requires
/// that the expression `fn(a_0, a_1, ..., a_2` to be valid.
/// Raise TypeError if `fn` is not callable and `ValueError`
/// if `fn` is callable, but has the wrong arity.
void require_arity(
std::string name,
boost::python::object fn,
std::size_t arity)
{
namespace python = boost::python;
std::stringstream error_msg;
error_msg << name << "() must take exactly " << arity << " arguments";
// Throw if the callback is not callable.
if (!PyCallable_Check(fn.ptr()))
{
PyErr_SetString(PyExc_TypeError, error_msg.str().c_str());
python::throw_error_already_set();
}
// Use the inspect module to extract the arg spec.
// >>> import inspect
auto inspect = python::import("inspect");
// >>> args, varargs, keywords, defaults = inspect.getargspec(fn)
auto arg_spec = inspect.attr("getargspec")(fn);
python::object args = arg_spec[0];
python::object varargs = arg_spec[1];
python::object defaults = arg_spec[3];
// Calculate the number of required arguments.
auto args_count = args ? python::len(args) : 0;
auto defaults_count = defaults ? python::len(defaults) : 0;
// If the function is a bound method or a class method, then the
// first argument (`self` or `cls`) will be implicitly provided.
// >>> has_self = inspect.ismethod(fn) and fn.__self__ is not None
if (static_cast<bool>(inspect.attr("ismethod")(fn))
&& fn.attr("__self__"))
{
--args_count;
}
// Require at least one argument. The function should support
// any of the following specs:
// >>> fn(a1)
// >>> fn(a1, a2=42)
// >>> fn(a1=42)
// >>> fn(*args)
auto required_count = args_count - defaults_count;
if (!( (required_count == 1) // fn(a1), fn(a1, a2=42)
|| (args_count > 0 && required_count == 0) // fn(a1=42)
|| (varargs) // fn(*args)
))
{
PyErr_SetString(PyExc_ValueError, error_msg.str().c_str());
python::throw_error_already_set();
}
}
void perform(
boost::python::object callback,
boost::python::object arg1)
{
require_arity("callback", callback, 1);
callback(arg1);
}
BOOST_PYTHON_MODULE(example)
{
namespace python = boost::python;
python::def("perform", &perform);
}
Interactive usage:
>>> import example
>>> def test(fn, a1, expect=None):
... try:
... example.perform(fn, a1)
... assert(expect is None)
... except Exception as e:
... assert(isinstance(e, expect))
...
>>> test(lambda x: 42, None)
>>> test(lambda x, y=2: 42, None)
>>> test(lambda x=1, y=2: 42, None)
>>> test(lambda *args: None, None)
>>> test(lambda: 42, None, ValueError)
>>> test(lambda x, y: 42, None, ValueError)
>>>
>>> class Mock:
... def method_no_arg(self): pass
... def method_with_arg(self, x): pass
... def method_default_arg(self, x=1): pass
... @classmethod
... def cls_no_arg(cls): pass
... @classmethod
... def cls_with_arg(cls, x): pass
... @classmethod
... def cls_with_default_arg(cls, x=1): pass
...
>>> mock = Mock()
>>> test(Mock.method_no_arg, mock)
>>> test(mock.method_no_arg, mock, ValueError)
>>> test(Mock.method_with_arg, mock, ValueError)
>>> test(mock.method_with_arg, mock)
>>> test(Mock.method_default_arg, mock)
>>> test(mock.method_default_arg, mock)
>>> test(Mock.cls_no_arg, mock, ValueError)
>>> test(mock.cls_no_arg, mock, ValueError)
>>> test(Mock.cls_with_arg, mock)
>>> test(mock.cls_with_arg, mock)
>>> test(Mock.cls_with_default_arg, mock)
>>> test(mock.cls_with_default_arg, mock)
Strict checking of function types can be argued as being non-Pythonic and can become complicated due to the various types of callables (bound-method, unbound method, classmethod, function, etc). Before applying strict type checking, it may be worth assessing if strict type checking is required, or if alternatives checks, such as Abstract Base Classes, would be sufficient. For instance, if the callback
functor will be invoked within a Python thread, then it may be worth not performing type checking, and allow the Python exception to be raised when invoked. On the other hand, if the callback
functor will be invoked from within a non-Python thread, then type checking within the initiating function can allow one to throw an exception within the calling Python thread.