Skip to content

Add support for non-converting arguments #634

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

Merged
merged 2 commits into from
Feb 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions docs/advanced/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,8 @@ crucial that instances are deallocated on the C++ side to avoid memory leaks.
py::class_<MyClass, std::unique_ptr<MyClass, py::nodelete>>(m, "MyClass")
.def(py::init<>())

.. _implicit_conversions:

Implicit conversions
====================

Expand Down
54 changes: 54 additions & 0 deletions docs/advanced/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,57 @@ like so:

py::class_<MyClass>("MyClass")
.def("myFunction", py::arg("arg") = (SomeType *) nullptr);

Non-converting arguments
========================

Certain argument types may support conversion from one type to another. Some
examples of conversions are:

* :ref:`implicit_conversions` declared using ``py::implicitly_convertible<A,B>()``
* Calling a method accepting a double with an integer argument
* Calling a ``std::complex<float>`` argument with a non-complex python type
(for example, with a float). (Requires the optional ``pybind11/complex.h``
header).
* Calling a function taking an Eigen matrix reference with a numpy array of the
wrong type or of an incompatible data layout. (Requires the optional
``pybind11/eigen.h`` header).

This behaviour is sometimes undesirable: the binding code may prefer to raise
an error rather than convert the argument. This behaviour can be obtained
through ``py::arg`` by calling the ``.noconvert()`` method of the ``py::arg``
object, such as:

.. code-block:: cpp

m.def("floats_only", [](double f) { return 0.5 * f; }, py::arg("f").noconvert());
m.def("floats_preferred", [](double f) { return 0.5 * f; }, py::arg("f"));

Attempting the call the second function (the one without ``.noconvert()``) with
an integer will succeed, but attempting to call the ``.noconvert()`` version
will fail with a ``TypeError``:

.. code-block:: pycon

>>> floats_preferred(4)
2.0
>>> floats_only(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: floats_only(): incompatible function arguments. The following argument types are supported:
1. (f: float) -> float

Invoked with: 4

You may, of course, combine this with the :var:`_a` shorthand notation (see
:ref:`keyword_args`) and/or :ref:`default_args`. It is also permitted to omit
the argument name by using the ``py::arg()`` constructor without an argument
name, i.e. by specifying ``py::arg().noconvert()``.

.. note::

When specifying ``py::arg`` options it is necessary to provide the same
number of options as the bound function has arguments. Thus if you want to
enable no-convert behaviour for just one of several arguments, you will
need to specify a ``py::arg()`` annotation for each argument with the
no-convert argument modified to ``py::arg().noconvert()``.
36 changes: 13 additions & 23 deletions include/pybind11/attr.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,17 @@ struct undefined_t;
template <op_id id, op_type ot, typename L = undefined_t, typename R = undefined_t> struct op_;
template <typename... Args> struct init;
template <typename... Args> struct init_alias;
struct function_call;
inline void keep_alive_impl(size_t Nurse, size_t Patient, function_call &call, handle ret);

/// Internal data structure which holds metadata about a keyword argument
struct argument_record {
const char *name; ///< Argument name
const char *descr; ///< Human-readable version of the argument value
handle value; ///< Associated Python object
bool convert : 1; ///< True if the argument is allowed to convert when loading

argument_record(const char *name, const char *descr, handle value)
: name(name), descr(descr), value(value) { }
argument_record(const char *name, const char *descr, handle value, bool convert)
: name(name), descr(descr), value(value), convert(convert) { }
};

/// Internal data structure which holds metadata about a bound function (signature, overloads, etc.)
Expand Down Expand Up @@ -131,7 +131,7 @@ struct function_record {
bool is_method : 1;

/// Number of arguments (including py::args and/or py::kwargs, if present)
uint16_t nargs;
std::uint16_t nargs;

/// Python method object
PyMethodDef *def = nullptr;
Expand Down Expand Up @@ -222,21 +222,11 @@ struct type_record {
}
};

/// Internal data associated with a single function call
struct function_call {
function_call(function_record &f, handle p) : func(f), parent(p) {
args.reserve(f.nargs);
}

/// The function data:
const function_record &func;

/// Arguments passed to the function:
std::vector<handle> args;

/// The parent, if any
handle parent;
};
inline function_call::function_call(function_record &f, handle p) :
func(f), parent(p) {
args.reserve(f.nargs);
args_convert.reserve(f.nargs);
}

/**
* Partial template specializations to process custom attributes provided to
Expand Down Expand Up @@ -300,16 +290,16 @@ template <> struct process_attribute<is_operator> : process_attribute_default<is
template <> struct process_attribute<arg> : process_attribute_default<arg> {
static void init(const arg &a, function_record *r) {
if (r->is_method && r->args.empty())
r->args.emplace_back("self", nullptr, handle());
r->args.emplace_back(a.name, nullptr, handle());
r->args.emplace_back("self", nullptr, handle(), true /*convert*/);
r->args.emplace_back(a.name, nullptr, handle(), !a.flag_noconvert);
}
};

/// Process a keyword argument attribute (*with* a default value)
template <> struct process_attribute<arg_v> : process_attribute_default<arg_v> {
static void init(const arg_v &a, function_record *r) {
if (r->is_method && r->args.empty())
r->args.emplace_back("self", nullptr, handle());
r->args.emplace_back("self", nullptr /*descr*/, handle() /*parent*/, true /*convert*/);

if (!a.value) {
#if !defined(NDEBUG)
Expand All @@ -330,7 +320,7 @@ template <> struct process_attribute<arg_v> : process_attribute_default<arg_v> {
"Compile in debug mode for more information.");
#endif
}
r->args.emplace_back(a.name, a.descr, a.value.inc_ref());
r->args.emplace_back(a.name, a.descr, a.value.inc_ref(), !a.flag_noconvert);
}
};

Expand Down
102 changes: 80 additions & 22 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -473,18 +473,22 @@ template <typename type> class type_caster<std::reference_wrapper<type>> : publi

template <typename T>
struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value>> {
typedef typename std::conditional<sizeof(T) <= sizeof(long), long, long long>::type _py_type_0;
typedef typename std::conditional<std::is_signed<T>::value, _py_type_0, typename std::make_unsigned<_py_type_0>::type>::type _py_type_1;
typedef typename std::conditional<std::is_floating_point<T>::value, double, _py_type_1>::type py_type;
using _py_type_0 = conditional_t<sizeof(T) <= sizeof(long), long, long long>;
using _py_type_1 = conditional_t<std::is_signed<T>::value, _py_type_0, typename std::make_unsigned<_py_type_0>::type>;
using py_type = conditional_t<std::is_floating_point<T>::value, double, _py_type_1>;
public:

bool load(handle src, bool) {
bool load(handle src, bool convert) {
py_type py_value;

if (!src) {
if (!src)
return false;
} if (std::is_floating_point<T>::value) {
py_value = (py_type) PyFloat_AsDouble(src.ptr());

if (std::is_floating_point<T>::value) {
if (convert || PyFloat_Check(src.ptr()))
py_value = (py_type) PyFloat_AsDouble(src.ptr());
else
return false;
} else if (sizeof(T) <= sizeof(long)) {
if (PyFloat_Check(src.ptr()))
return false;
Expand All @@ -511,7 +515,7 @@ struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value>> {
bool type_error = PyErr_ExceptionMatches(PyExc_TypeError);
#endif
PyErr_Clear();
if (type_error && PyNumber_Check(src.ptr())) {
if (type_error && convert && PyNumber_Check(src.ptr())) {
auto tmp = reinterpret_borrow<object>(std::is_floating_point<T>::value
? PyNumber_Float(src.ptr())
: PyNumber_Long(src.ptr()));
Expand Down Expand Up @@ -1198,22 +1202,26 @@ template <return_value_policy policy = return_value_policy::automatic_reference,
}

/// \ingroup annotations
/// Annotation for keyword arguments
/// Annotation for arguments
struct arg {
/// Set the name of the argument
constexpr explicit arg(const char *name) : name(name) { }
/// Constructs an argument with the name of the argument; if null or omitted, this is a positional argument.
constexpr explicit arg(const char *name = nullptr) : name(name), flag_noconvert(false) { }
/// Assign a value to this argument
template <typename T> arg_v operator=(T &&value) const;
/// Indicate that the type should not be converted in the type caster
arg &noconvert(bool flag = true) { flag_noconvert = flag; return *this; }

const char *name;
const char *name; ///< If non-null, this is a named kwargs argument
bool flag_noconvert : 1; ///< If set, do not allow conversion (requires a supporting type caster!)
};

/// \ingroup annotations
/// Annotation for keyword arguments with values
/// Annotation for arguments with values
struct arg_v : arg {
private:
template <typename T>
arg_v(const char *name, T &&x, const char *descr = nullptr)
: arg(name),
arg_v(arg &&base, T &&x, const char *descr = nullptr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand this change -- why the rvalue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was to potentially avoid a copy--that's now just a private constructor with the common code, invoked with an rvalue arg from the direct (first public) or converted-from-arg (second public) constructors.

: arg(base),
value(reinterpret_steal<object>(
detail::make_caster<T>::cast(x, return_value_policy::automatic, {})
)),
Expand All @@ -1223,15 +1231,32 @@ struct arg_v : arg {
#endif
{ }

public:
/// Direct construction with name, default, and description
template <typename T>
arg_v(const char *name, T &&x, const char *descr = nullptr)
: arg_v(arg(name), std::forward<T>(x), descr) { }

/// Called internally when invoking `py::arg("a") = value`
template <typename T>
arg_v(const arg &base, T &&x, const char *descr = nullptr)
: arg_v(arg(base), std::forward<T>(x), descr) { }

/// Same as `arg::noconvert()`, but returns *this as arg_v&, not arg&
arg_v &noconvert(bool flag = true) { arg::noconvert(flag); return *this; }

/// The default value
object value;
/// The (optional) description of the default value
const char *descr;
#if !defined(NDEBUG)
/// The C++ type name of the default value (only available when compiled in debug mode)
std::string type;
#endif
};

template <typename T>
arg_v arg::operator=(T &&value) const { return {name, std::forward<T>(value)}; }
arg_v arg::operator=(T &&value) const { return {std::move(*this), std::forward<T>(value)}; }

/// Alias for backward compatibility -- to be removed in version 2.0
template <typename /*unused*/> using arg_t = arg_v;
Expand All @@ -1248,11 +1273,28 @@ NAMESPACE_BEGIN(detail)
// forward declaration
struct function_record;

/// Internal data associated with a single function call
struct function_call {
function_call(function_record &f, handle p); // Implementation in attr.h

/// The function data:
const function_record &func;

/// Arguments passed to the function:
std::vector<handle> args;

/// The `convert` value the arguments should be loaded with
std::vector<bool> args_convert;

/// The parent, if any
handle parent;
};


/// Helper class which loads arguments for C++ functions called from Python
template <typename... Args>
class argument_loader {
using indices = make_index_sequence<sizeof...(Args)>;
using function_arguments = const std::vector<handle> &;

template <typename Arg> using argument_is_args = std::is_same<intrinsic_t<Arg>, args>;
template <typename Arg> using argument_is_kwargs = std::is_same<intrinsic_t<Arg>, kwargs>;
Expand All @@ -1270,8 +1312,8 @@ class argument_loader {

static PYBIND11_DESCR arg_names() { return detail::concat(make_caster<Args>::name()...); }

bool load_args(function_arguments args) {
return load_impl_sequence(args, indices{});
bool load_args(function_call &call) {
return load_impl_sequence(call, indices{});
}

template <typename Return, typename Func>
Expand All @@ -1287,11 +1329,11 @@ class argument_loader {

private:

static bool load_impl_sequence(function_arguments, index_sequence<>) { return true; }
static bool load_impl_sequence(function_call &, index_sequence<>) { return true; }

template <size_t... Is>
bool load_impl_sequence(function_arguments args, index_sequence<Is...>) {
for (bool r : {std::get<Is>(value).load(args[Is], true)...})
bool load_impl_sequence(function_call &call, index_sequence<Is...>) {
for (bool r : {std::get<Is>(value).load(call.args[Is], call.args_convert[Is])...})
if (!r)
return false;
return true;
Expand Down Expand Up @@ -1380,6 +1422,13 @@ class unpacking_collector {
}

void process(list &/*args_list*/, arg_v a) {
if (!a.name)
#if defined(NDEBUG)
nameless_argument_error();
#else
nameless_argument_error(a.type);
#endif

if (m_kwargs.contains(a.name)) {
#if defined(NDEBUG)
multiple_values_error();
Expand Down Expand Up @@ -1412,6 +1461,15 @@ class unpacking_collector {
}
}

[[noreturn]] static void nameless_argument_error() {
throw type_error("Got kwargs without a name; only named arguments "
"may be passed via py::arg() to a python function call. "
"(compile in debug mode for details)");
}
[[noreturn]] static void nameless_argument_error(std::string type) {
throw type_error("Got kwargs without a name of type '" + type + "'; only named "
"arguments may be passed via py::arg() to a python function call. ");
}
[[noreturn]] static void multiple_values_error() {
throw type_error("Got multiple values for keyword argument "
"(compile in debug mode for details)");
Expand Down
4 changes: 3 additions & 1 deletion include/pybind11/complex.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ template <typename T> struct is_fmt_numeric<std::complex<T>> {

template <typename T> class type_caster<std::complex<T>> {
public:
bool load(handle src, bool) {
bool load(handle src, bool convert) {
if (!src)
return false;
if (!convert && !PyComplex_Check(src.ptr()))
return false;
Py_complex result = PyComplex_AsCComplex(src.ptr());
if (result.real == -1.0 && PyErr_Occurred()) {
PyErr_Clear();
Expand Down
Loading