Add support for non-converting arguments

This adds support for controlling the `convert` flag of arguments
through the py::arg annotation.  This then allows arguments to be
flagged as non-converting, which the type_caster is able to use to
request different behaviour.

Currently, AFAICS `convert` is only used for type converters of regular
pybind11-registered types; all of the other core type_casters ignore it.
We can, however, repurpose it to control internal conversion of
converters like Eigen and `array`: most usefully to give callers a way
to disable the conversion that would otherwise occur when a
`Eigen::Ref<const Eigen::Matrix>` argument is passed a numpy array that
requires conversion (either because it has an incompatible stride or the
wrong dtype).

Specifying a noconvert looks like one of these:

    m.def("f1", &f, "a"_a.noconvert() = "default"); // Named, default, noconvert
    m.def("f2", &f, "a"_a.noconvert()); // Named, no default, no converting
    m.def("f3", &f, py::arg().noconvert()); // Unnamed, no default, no converting

(The last part--being able to declare a py::arg without a name--is new:
previous py::arg() only accepted named keyword arguments).

Such an non-convert argument is then passed `convert = false` by the
type caster when loading the argument.  Whether this has an effect is up
to the type caster itself, but as mentioned above, this would be
extremely helpful for the Eigen support to give a nicer way to specify
a "no-copy" mode than the custom wrapper in the current PR, and
moreover isn't an Eigen-specific hack.
This commit is contained in:
Jason Rhinelander 2017-01-23 03:50:00 -05:00
parent 709675a7aa
commit abc29cad02
7 changed files with 261 additions and 52 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

@ -1202,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, {})
)),
@ -1227,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;
@ -1252,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>;
@ -1274,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>
@ -1291,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;
@ -1384,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();
@ -1416,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

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