Merge pull request #634 from jagerman/noconvert-arguments

Add support for non-converting arguments
This commit is contained in:
Wenzel Jakob 2017-02-05 23:51:38 +01:00 committed by GitHub
commit 18e34cb2e6
8 changed files with 276 additions and 61 deletions

View File

@ -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
====================

View File

@ -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()``.

View File

@ -69,7 +69,6 @@ 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
@ -77,9 +76,10 @@ 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.)
@ -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;
@ -222,22 +222,12 @@ 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) {
inline function_call::function_call(function_record &f, handle p) :
func(f), parent(p) {
args.reserve(f.nargs);
args_convert.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;
};
/**
* Partial template specializations to process custom attributes provided to
* cpp_function_ and class_. These are either used to initialize the respective
@ -300,8 +290,8 @@ 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);
}
};
@ -309,7 +299,7 @@ template <> struct process_attribute<arg> : process_attribute_default<arg> {
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)
@ -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);
}
};

View File

@ -473,18 +473,22 @@ public:
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) {
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;
@ -511,7 +515,7 @@ public:
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()));
@ -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)
: arg(base),
value(reinterpret_steal<object>(
detail::make_caster<T>::cast(x, return_value_policy::automatic, {})
)),
@ -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;
@ -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>;
@ -1270,8 +1312,8 @@ public:
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>
@ -1287,11 +1329,11 @@ public:
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;
@ -1380,6 +1422,13 @@ private:
}
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();
@ -1412,6 +1461,15 @@ private:
}
}
[[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)");

View File

@ -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();

View File

@ -122,7 +122,7 @@ protected:
cast_in args_converter;
/* Try to cast the function arguments into the C++ domain */
if (!args_converter.load_args(call.args))
if (!args_converter.load_args(call))
return PYBIND11_TRY_NEXT_OVERLOAD;
/* Invoke call policy pre-call hook */
@ -198,7 +198,7 @@ protected:
if (c == '{') {
// Write arg name for everything except *args, **kwargs and return type.
if (type_depth == 0 && text[char_index] != '*' && arg_index < args) {
if (!rec->args.empty()) {
if (!rec->args.empty() && rec->args[arg_index].name) {
signature += rec->args[arg_index].name;
} else if (arg_index == 0 && rec->is_method) {
signature += "self";
@ -257,7 +257,7 @@ protected:
rec->signature = strdup(signature.c_str());
rec->args.shrink_to_fit();
rec->is_constructor = !strcmp(rec->name, "__init__") || !strcmp(rec->name, "__setstate__");
rec->nargs = (uint16_t) args;
rec->nargs = (std::uint16_t) args;
#if PY_MAJOR_VERSION < 3
if (rec->sibling && PyMethod_Check(rec->sibling.ptr()))
@ -392,8 +392,10 @@ protected:
handle parent = n_args_in > 0 ? PyTuple_GET_ITEM(args_in, 0) : nullptr,
result = PYBIND11_TRY_NEXT_OVERLOAD;
try {
for (; it != nullptr; it = it->next) {
/* For each overload:
1. Copy all positional arguments we were given, also checking to make sure that
named positional arguments weren't *also* specified via kwarg.
@ -435,14 +437,15 @@ protected:
// raise a TypeError like Python does. (We could also continue with the next
// overload, but this seems highly likely to be a caller mistake rather than a
// legitimate overload).
if (kwargs_in && args_copied < it->args.size()) {
handle value = PyDict_GetItemString(kwargs_in, it->args[args_copied].name);
if (kwargs_in && args_copied < func.args.size() && func.args[args_copied].name) {
handle value = PyDict_GetItemString(kwargs_in, func.args[args_copied].name);
if (value)
throw type_error(std::string(it->name) + "(): got multiple values for argument '" +
std::string(it->args[args_copied].name) + "'");
throw type_error(std::string(func.name) + "(): got multiple values for argument '" +
std::string(func.args[args_copied].name) + "'");
}
call.args.push_back(PyTuple_GET_ITEM(args_in, args_copied));
call.args_convert.push_back(args_copied < func.args.size() ? func.args[args_copied].convert : true);
}
// We'll need to copy this if we steal some kwargs for defaults
@ -453,10 +456,10 @@ protected:
bool copied_kwargs = false;
for (; args_copied < pos_args; ++args_copied) {
const auto &arg = it->args[args_copied];
const auto &arg = func.args[args_copied];
handle value;
if (kwargs_in)
if (kwargs_in && arg.name)
value = PyDict_GetItemString(kwargs.ptr(), arg.name);
if (value) {
@ -470,8 +473,10 @@ protected:
value = arg.value;
}
if (value)
if (value) {
call.args.push_back(value);
call.args_convert.push_back(arg.convert);
}
else
break;
}
@ -481,12 +486,12 @@ protected:
}
// 3. Check everything was consumed (unless we have a kwargs arg)
if (kwargs && kwargs.size() > 0 && !it->has_kwargs)
if (kwargs && kwargs.size() > 0 && !func.has_kwargs)
continue; // Unconsumed kwargs, but no py::kwargs argument to accept them
// 4a. If we have a py::args argument, create a new tuple with leftovers
tuple extra_args;
if (it->has_args) {
if (func.has_args) {
if (args_to_copy == 0) {
// We didn't copy out any position arguments from the args_in tuple, so we
// can reuse it directly without copying:
@ -502,31 +507,34 @@ protected:
}
}
call.args.push_back(extra_args);
call.args_convert.push_back(false);
}
// 4b. If we have a py::kwargs, pass on any remaining kwargs
if (it->has_kwargs) {
if (func.has_kwargs) {
if (!kwargs.ptr())
kwargs = dict(); // If we didn't get one, send an empty one
call.args.push_back(kwargs);
call.args_convert.push_back(false);
}
// 5. Put everything in a vector. Not technically step 5, we've been building it
// in `call.args` all along.
#if !defined(NDEBUG)
if (call.args.size() != call.func.nargs)
if (call.args.size() != func.nargs || call.args_convert.size() != func.nargs)
pybind11_fail("Internal error: function call dispatcher inserted wrong number of arguments!");
#endif
// 6. Call the function.
try {
result = it->impl(call);
result = func.impl(call);
} catch (reference_cast_error &) {
result = PYBIND11_TRY_NEXT_OVERLOAD;
}
if (result.ptr() != PYBIND11_TRY_NEXT_OVERLOAD)
break;
}
} catch (error_already_set &e) {
e.restore();

View File

@ -97,6 +97,42 @@ public:
class CppDerivedDynamicClass : public DynamicClass { };
// py::arg/py::arg_v testing: these arguments just record their argument when invoked
class ArgInspector1 { public: std::string arg = "(default arg inspector 1)"; };
class ArgInspector2 { public: std::string arg = "(default arg inspector 2)"; };
namespace pybind11 { namespace detail {
template <> struct type_caster<ArgInspector1> {
public:
PYBIND11_TYPE_CASTER(ArgInspector1, _("ArgInspector1"));
bool load(handle src, bool convert) {
value.arg = "loading ArgInspector1 argument " +
std::string(convert ? "WITH" : "WITHOUT") + " conversion allowed. "
"Argument value = " + (std::string) str(src);
return true;
}
static handle cast(const ArgInspector1 &src, return_value_policy, handle) {
return str(src.arg).release();
}
};
template <> struct type_caster<ArgInspector2> {
public:
PYBIND11_TYPE_CASTER(ArgInspector2, _("ArgInspector2"));
bool load(handle src, bool convert) {
value.arg = "loading ArgInspector2 argument " +
std::string(convert ? "WITH" : "WITHOUT") + " conversion allowed. "
"Argument value = " + (std::string) str(src);
return true;
}
static handle cast(const ArgInspector2 &src, return_value_policy, handle) {
return str(src.arg).release();
}
};
}}
test_initializer methods_and_attributes([](py::module &m) {
py::class_<ExampleMandA>(m, "ExampleMandA")
.def(py::init<>())
@ -183,4 +219,25 @@ test_initializer methods_and_attributes([](py::module &m) {
py::class_<CppDerivedDynamicClass, DynamicClass>(m, "CppDerivedDynamicClass")
.def(py::init());
#endif
class ArgInspector {
public:
ArgInspector1 f(ArgInspector1 a) { return a; }
std::string g(ArgInspector1 a, const ArgInspector1 &b, int c, ArgInspector2 *d) {
return a.arg + "\n" + b.arg + "\n" + std::to_string(c) + "\n" + d->arg;
}
static ArgInspector2 h(ArgInspector2 a) { return a; }
};
py::class_<ArgInspector>(m, "ArgInspector")
.def(py::init<>())
.def("f", &ArgInspector::f)
.def("g", &ArgInspector::g, "a"_a.noconvert(), "b"_a, "c"_a.noconvert()=13, "d"_a=ArgInspector2())
.def_static("h", &ArgInspector::h, py::arg().noconvert())
;
m.def("arg_inspect_func", [](ArgInspector2 a, ArgInspector1 b) { return a.arg + "\n" + b.arg; },
py::arg().noconvert(false), py::arg_v(nullptr, ArgInspector1()).noconvert(true));
m.def("floats_preferred", [](double f) { return 0.5 * f; }, py::arg("f"));
m.def("floats_only", [](double f) { return 0.5 * f; }, py::arg("f").noconvert());
});

View File

@ -203,3 +203,47 @@ def test_cyclic_gc():
assert cstats.alive() == 2
del i1, i2
assert cstats.alive() == 0
def test_noconvert_args(msg):
from pybind11_tests import ArgInspector, arg_inspect_func, floats_only, floats_preferred
a = ArgInspector()
assert msg(a.f("hi")) == """
loading ArgInspector1 argument WITH conversion allowed. Argument value = hi
"""
assert msg(a.g("this is a", "this is b")) == """
loading ArgInspector1 argument WITHOUT conversion allowed. Argument value = this is a
loading ArgInspector1 argument WITH conversion allowed. Argument value = this is b
13
loading ArgInspector2 argument WITH conversion allowed. Argument value = (default arg inspector 2)
""" # noqa: E501 line too long
assert msg(a.g("this is a", "this is b", 42)) == """
loading ArgInspector1 argument WITHOUT conversion allowed. Argument value = this is a
loading ArgInspector1 argument WITH conversion allowed. Argument value = this is b
42
loading ArgInspector2 argument WITH conversion allowed. Argument value = (default arg inspector 2)
""" # noqa: E501 line too long
assert msg(a.g("this is a", "this is b", 42, "this is d")) == """
loading ArgInspector1 argument WITHOUT conversion allowed. Argument value = this is a
loading ArgInspector1 argument WITH conversion allowed. Argument value = this is b
42
loading ArgInspector2 argument WITH conversion allowed. Argument value = this is d
"""
assert (a.h("arg 1") ==
"loading ArgInspector2 argument WITHOUT conversion allowed. Argument value = arg 1")
assert msg(arg_inspect_func("A1", "A2")) == """
loading ArgInspector2 argument WITH conversion allowed. Argument value = A1
loading ArgInspector1 argument WITHOUT conversion allowed. Argument value = A2
"""
assert floats_preferred(4) == 2.0
assert floats_only(4.0) == 2.0
with pytest.raises(TypeError) as excinfo:
floats_only(4)
assert msg(excinfo.value) == """
floats_only(): incompatible function arguments. The following argument types are supported:
1. (f: float) -> float
Invoked with: 4
"""