diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst index 9d17364b3..e0463b451 100644 --- a/docs/advanced/classes.rst +++ b/docs/advanced/classes.rst @@ -388,6 +388,8 @@ crucial that instances are deallocated on the C++ side to avoid memory leaks. py::class_>(m, "MyClass") .def(py::init<>()) +.. _implicit_conversions: + Implicit conversions ==================== diff --git a/docs/advanced/functions.rst b/docs/advanced/functions.rst index 7ffdfaa80..e67d7bc37 100644 --- a/docs/advanced/functions.rst +++ b/docs/advanced/functions.rst @@ -318,3 +318,57 @@ like so: py::class_("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()`` +* Calling a method accepting a double with an integer argument +* Calling a ``std::complex`` 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 "", line 1, in + 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()``. diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index e9468b856..129db0548 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -69,7 +69,6 @@ struct undefined_t; template struct op_; template struct init; template 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,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 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 @@ -300,8 +290,8 @@ template <> struct process_attribute : process_attribute_default struct process_attribute : process_attribute_default { 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 : process_attribute_default { template <> struct process_attribute : process_attribute_default { 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 : process_attribute_default { "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); } }; diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 4dc3082fc..67ab2169b 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1202,22 +1202,26 @@ template 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 - 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( detail::make_caster::cast(x, return_value_policy::automatic, {}) )), @@ -1227,15 +1231,32 @@ struct arg_v : arg { #endif { } +public: + /// Direct construction with name, default, and description + template + arg_v(const char *name, T &&x, const char *descr = nullptr) + : arg_v(arg(name), std::forward(x), descr) { } + + /// Called internally when invoking `py::arg("a") = value` + template + arg_v(const arg &base, T &&x, const char *descr = nullptr) + : arg_v(arg(base), std::forward(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 -arg_v arg::operator=(T &&value) const { return {name, std::forward(value)}; } +arg_v arg::operator=(T &&value) const { return {std::move(*this), std::forward(value)}; } /// Alias for backward compatibility -- to be removed in version 2.0 template 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 args; + + /// The `convert` value the arguments should be loaded with + std::vector args_convert; + + /// The parent, if any + handle parent; +}; + + /// Helper class which loads arguments for C++ functions called from Python template class argument_loader { using indices = make_index_sequence; - using function_arguments = const std::vector &; template using argument_is_args = std::is_same, args>; template using argument_is_kwargs = std::is_same, kwargs>; @@ -1274,8 +1312,8 @@ public: static PYBIND11_DESCR arg_names() { return detail::concat(make_caster::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 @@ -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 - bool load_impl_sequence(function_arguments args, index_sequence) { - for (bool r : {std::get(value).load(args[Is], true)...}) + bool load_impl_sequence(function_call &call, index_sequence) { + for (bool r : {std::get(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)"); diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 14dc56ca6..fc283704e 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -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(); diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index f7d6d6855..6f5e5ef2f 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -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 { +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 { +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_(m, "ExampleMandA") .def(py::init<>()) @@ -183,4 +219,25 @@ test_initializer methods_and_attributes([](py::module &m) { py::class_(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_(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()); + }); diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 840ee707b..f890c6aaa 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -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 + """