mirror of
https://github.com/pybind/pybind11.git
synced 2024-11-22 05:05:11 +00:00
Allow py::arg().none(false) argument attribute
This attribute lets you disable (or explicitly enable) passing None to an argument that otherwise would allow it by accepting a value by raw pointer or shared_ptr.
This commit is contained in:
parent
813d7e8687
commit
4e1e4a580e
@ -406,6 +406,53 @@ name, i.e. by specifying ``py::arg().noconvert()``.
|
|||||||
need to specify a ``py::arg()`` annotation for each argument with the
|
need to specify a ``py::arg()`` annotation for each argument with the
|
||||||
no-convert argument modified to ``py::arg().noconvert()``.
|
no-convert argument modified to ``py::arg().noconvert()``.
|
||||||
|
|
||||||
|
Allow/Prohibiting None arguments
|
||||||
|
================================
|
||||||
|
|
||||||
|
When a C++ type registered with :class:`py::class_` is passed as an argument to
|
||||||
|
a function taking the instance as pointer or shared holder (e.g. ``shared_ptr``
|
||||||
|
or a custom, copyable holder as described in :ref:`smart_pointers`), pybind
|
||||||
|
allows ``None`` to be passed from Python which results in calling the C++
|
||||||
|
function with ``nullptr`` (or an empty holder) for the argument.
|
||||||
|
|
||||||
|
To explicitly enable or disable this behaviour, using the
|
||||||
|
``.none`` method of the :class:`py::arg` object:
|
||||||
|
|
||||||
|
.. code-block:: cpp
|
||||||
|
|
||||||
|
py::class_<Dog>(m, "Dog").def(py::init<>());
|
||||||
|
py::class_<Cat>(m, "Cat").def(py::init<>());
|
||||||
|
m.def("bark", [](Dog *dog) -> std::string {
|
||||||
|
if (dog) return "woof!"; /* Called with a Dog instance */
|
||||||
|
else return "(no dog)"; /* Called with None, d == nullptr */
|
||||||
|
}, py::arg("dog").none(true));
|
||||||
|
m.def("meow", [](Cat *cat) -> std::string {
|
||||||
|
// Can't be called with None argument
|
||||||
|
return "meow";
|
||||||
|
}, py::arg("cat").none(false));
|
||||||
|
|
||||||
|
With the above, the Python call ``bark(None)`` will return the string ``"(no
|
||||||
|
dog)"``, while attempting to call ``meow(None)`` will throw a :exc:`TypeError`:
|
||||||
|
|
||||||
|
.. code-block:: pycon
|
||||||
|
|
||||||
|
>>> from animals import Dog, Cat, bark, meow
|
||||||
|
>>> bark(Dog())
|
||||||
|
'woof!'
|
||||||
|
>>> meow(Cat())
|
||||||
|
'meow'
|
||||||
|
>>> bark(None)
|
||||||
|
'(no dog)'
|
||||||
|
>>> meow(None)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
TypeError: meow(): incompatible function arguments. The following argument types are supported:
|
||||||
|
1. (cat: animals.Cat) -> str
|
||||||
|
|
||||||
|
Invoked with: None
|
||||||
|
|
||||||
|
The default behaviour when the tag is unspecified is to allow ``None``.
|
||||||
|
|
||||||
Overload resolution order
|
Overload resolution order
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
@ -123,9 +123,10 @@ struct argument_record {
|
|||||||
const char *descr; ///< Human-readable version of the argument value
|
const char *descr; ///< Human-readable version of the argument value
|
||||||
handle value; ///< Associated Python object
|
handle value; ///< Associated Python object
|
||||||
bool convert : 1; ///< True if the argument is allowed to convert when loading
|
bool convert : 1; ///< True if the argument is allowed to convert when loading
|
||||||
|
bool none : 1; ///< True if None is allowed when loading
|
||||||
|
|
||||||
argument_record(const char *name, const char *descr, handle value, bool convert)
|
argument_record(const char *name, const char *descr, handle value, bool convert, bool none)
|
||||||
: name(name), descr(descr), value(value), convert(convert) { }
|
: name(name), descr(descr), value(value), convert(convert), none(none) { }
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Internal data structure which holds metadata about a bound function (signature, overloads, etc.)
|
/// Internal data structure which holds metadata about a bound function (signature, overloads, etc.)
|
||||||
@ -338,8 +339,8 @@ template <> struct process_attribute<is_operator> : process_attribute_default<is
|
|||||||
template <> struct process_attribute<arg> : process_attribute_default<arg> {
|
template <> struct process_attribute<arg> : process_attribute_default<arg> {
|
||||||
static void init(const arg &a, function_record *r) {
|
static void init(const arg &a, function_record *r) {
|
||||||
if (r->is_method && r->args.empty())
|
if (r->is_method && r->args.empty())
|
||||||
r->args.emplace_back("self", nullptr, handle(), true /*convert*/);
|
r->args.emplace_back("self", nullptr, handle(), true /*convert*/, false /*none not allowed*/);
|
||||||
r->args.emplace_back(a.name, nullptr, handle(), !a.flag_noconvert);
|
r->args.emplace_back(a.name, nullptr, handle(), !a.flag_noconvert, a.flag_none);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -347,7 +348,7 @@ template <> struct process_attribute<arg> : process_attribute_default<arg> {
|
|||||||
template <> struct process_attribute<arg_v> : process_attribute_default<arg_v> {
|
template <> struct process_attribute<arg_v> : process_attribute_default<arg_v> {
|
||||||
static void init(const arg_v &a, function_record *r) {
|
static void init(const arg_v &a, function_record *r) {
|
||||||
if (r->is_method && r->args.empty())
|
if (r->is_method && r->args.empty())
|
||||||
r->args.emplace_back("self", nullptr /*descr*/, handle() /*parent*/, true /*convert*/);
|
r->args.emplace_back("self", nullptr /*descr*/, handle() /*parent*/, true /*convert*/, false /*none not allowed*/);
|
||||||
|
|
||||||
if (!a.value) {
|
if (!a.value) {
|
||||||
#if !defined(NDEBUG)
|
#if !defined(NDEBUG)
|
||||||
@ -370,7 +371,7 @@ template <> struct process_attribute<arg_v> : process_attribute_default<arg_v> {
|
|||||||
"Compile in debug mode for more information.");
|
"Compile in debug mode for more information.");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
r->args.emplace_back(a.name, a.descr, a.value.inc_ref(), !a.flag_noconvert);
|
r->args.emplace_back(a.name, a.descr, a.value.inc_ref(), !a.flag_noconvert, a.flag_none);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1381,14 +1381,17 @@ template <return_value_policy policy = return_value_policy::automatic_reference,
|
|||||||
/// Annotation for arguments
|
/// Annotation for arguments
|
||||||
struct arg {
|
struct arg {
|
||||||
/// Constructs an argument with the name of the argument; if null or omitted, this is a positional argument.
|
/// 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) { }
|
constexpr explicit arg(const char *name = nullptr) : name(name), flag_noconvert(false), flag_none(true) { }
|
||||||
/// Assign a value to this argument
|
/// Assign a value to this argument
|
||||||
template <typename T> arg_v operator=(T &&value) const;
|
template <typename T> arg_v operator=(T &&value) const;
|
||||||
/// Indicate that the type should not be converted in the type caster
|
/// Indicate that the type should not be converted in the type caster
|
||||||
arg &noconvert(bool flag = true) { flag_noconvert = flag; return *this; }
|
arg &noconvert(bool flag = true) { flag_noconvert = flag; return *this; }
|
||||||
|
/// Indicates that the argument should/shouldn't allow None (e.g. for nullable pointer args)
|
||||||
|
arg &none(bool flag = true) { flag_none = flag; return *this; }
|
||||||
|
|
||||||
const char *name; ///< If non-null, this is a named kwargs argument
|
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!)
|
bool flag_noconvert : 1; ///< If set, do not allow conversion (requires a supporting type caster!)
|
||||||
|
bool flag_none : 1; ///< If set (the default), allow None to be passed to this argument
|
||||||
};
|
};
|
||||||
|
|
||||||
/// \ingroup annotations
|
/// \ingroup annotations
|
||||||
@ -1421,6 +1424,9 @@ public:
|
|||||||
/// Same as `arg::noconvert()`, but returns *this as arg_v&, not arg&
|
/// Same as `arg::noconvert()`, but returns *this as arg_v&, not arg&
|
||||||
arg_v &noconvert(bool flag = true) { arg::noconvert(flag); return *this; }
|
arg_v &noconvert(bool flag = true) { arg::noconvert(flag); return *this; }
|
||||||
|
|
||||||
|
/// Same as `arg::nonone()`, but returns *this as arg_v&, not arg&
|
||||||
|
arg_v &none(bool flag = true) { arg::none(flag); return *this; }
|
||||||
|
|
||||||
/// The default value
|
/// The default value
|
||||||
object value;
|
object value;
|
||||||
/// The (optional) description of the default value
|
/// The (optional) description of the default value
|
||||||
|
@ -466,18 +466,23 @@ protected:
|
|||||||
size_t args_copied = 0;
|
size_t args_copied = 0;
|
||||||
|
|
||||||
// 1. Copy any position arguments given.
|
// 1. Copy any position arguments given.
|
||||||
bool bad_kwarg = false;
|
bool bad_arg = false;
|
||||||
for (; args_copied < args_to_copy; ++args_copied) {
|
for (; args_copied < args_to_copy; ++args_copied) {
|
||||||
if (kwargs_in && args_copied < func.args.size() && func.args[args_copied].name
|
argument_record *arg_rec = args_copied < func.args.size() ? &func.args[args_copied] : nullptr;
|
||||||
&& PyDict_GetItemString(kwargs_in, func.args[args_copied].name)) {
|
if (kwargs_in && arg_rec && arg_rec->name && PyDict_GetItemString(kwargs_in, arg_rec->name)) {
|
||||||
bad_kwarg = true;
|
bad_arg = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
call.args.push_back(PyTuple_GET_ITEM(args_in, args_copied));
|
handle arg(PyTuple_GET_ITEM(args_in, args_copied));
|
||||||
call.args_convert.push_back(args_copied < func.args.size() ? func.args[args_copied].convert : true);
|
if (arg_rec && !arg_rec->none && arg.is_none()) {
|
||||||
|
bad_arg = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
call.args.push_back(arg);
|
||||||
|
call.args_convert.push_back(arg_rec ? arg_rec->convert : true);
|
||||||
}
|
}
|
||||||
if (bad_kwarg)
|
if (bad_arg)
|
||||||
continue; // Maybe it was meant for another overload (issue #688)
|
continue; // Maybe it was meant for another overload (issue #688)
|
||||||
|
|
||||||
// We'll need to copy this if we steal some kwargs for defaults
|
// We'll need to copy this if we steal some kwargs for defaults
|
||||||
|
@ -162,6 +162,14 @@ public:
|
|||||||
/// Issue/PR #648: bad arg default debugging output
|
/// Issue/PR #648: bad arg default debugging output
|
||||||
class NotRegistered {};
|
class NotRegistered {};
|
||||||
|
|
||||||
|
// Test None-allowed py::arg argument policy
|
||||||
|
class NoneTester { public: int answer = 42; };
|
||||||
|
int none1(const NoneTester &obj) { return obj.answer; }
|
||||||
|
int none2(NoneTester *obj) { return obj ? obj->answer : -1; }
|
||||||
|
int none3(std::shared_ptr<NoneTester> &obj) { return obj ? obj->answer : -1; }
|
||||||
|
int none4(std::shared_ptr<NoneTester> *obj) { return obj && *obj ? (*obj)->answer : -1; }
|
||||||
|
int none5(std::shared_ptr<NoneTester> obj) { return obj ? obj->answer : -1; }
|
||||||
|
|
||||||
test_initializer methods_and_attributes([](py::module &m) {
|
test_initializer methods_and_attributes([](py::module &m) {
|
||||||
py::class_<ExampleMandA> emna(m, "ExampleMandA");
|
py::class_<ExampleMandA> emna(m, "ExampleMandA");
|
||||||
emna.def(py::init<>())
|
emna.def(py::init<>())
|
||||||
@ -322,4 +330,18 @@ test_initializer methods_and_attributes([](py::module &m) {
|
|||||||
auto m = py::module::import("pybind11_tests");
|
auto m = py::module::import("pybind11_tests");
|
||||||
m.def("should_fail", [](int, NotRegistered) {}, py::arg(), py::arg() = NotRegistered());
|
m.def("should_fail", [](int, NotRegistered) {}, py::arg(), py::arg() = NotRegistered());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
py::class_<NoneTester, std::shared_ptr<NoneTester>>(m, "NoneTester")
|
||||||
|
.def(py::init<>());
|
||||||
|
m.def("no_none1", &none1, py::arg().none(false));
|
||||||
|
m.def("no_none2", &none2, py::arg().none(false));
|
||||||
|
m.def("no_none3", &none3, py::arg().none(false));
|
||||||
|
m.def("no_none4", &none4, py::arg().none(false));
|
||||||
|
m.def("no_none5", &none5, py::arg().none(false));
|
||||||
|
m.def("ok_none1", &none1);
|
||||||
|
m.def("ok_none2", &none2, py::arg().none(true));
|
||||||
|
m.def("ok_none3", &none3);
|
||||||
|
m.def("ok_none4", &none4, py::arg().none(true));
|
||||||
|
m.def("ok_none5", &none5);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -369,3 +369,47 @@ def test_bad_arg_default(msg):
|
|||||||
"arg(): could not convert default argument into a Python object (type not registered "
|
"arg(): could not convert default argument into a Python object (type not registered "
|
||||||
"yet?). Compile in debug mode for more information."
|
"yet?). Compile in debug mode for more information."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_accepts_none():
|
||||||
|
from pybind11_tests import (NoneTester,
|
||||||
|
no_none1, no_none2, no_none3, no_none4, no_none5,
|
||||||
|
ok_none1, ok_none2, ok_none3, ok_none4, ok_none5)
|
||||||
|
|
||||||
|
a = NoneTester()
|
||||||
|
assert no_none1(a) == 42
|
||||||
|
assert no_none2(a) == 42
|
||||||
|
assert no_none3(a) == 42
|
||||||
|
assert no_none4(a) == 42
|
||||||
|
assert no_none5(a) == 42
|
||||||
|
assert ok_none1(a) == 42
|
||||||
|
assert ok_none2(a) == 42
|
||||||
|
assert ok_none3(a) == 42
|
||||||
|
assert ok_none4(a) == 42
|
||||||
|
assert ok_none5(a) == 42
|
||||||
|
|
||||||
|
with pytest.raises(TypeError) as excinfo:
|
||||||
|
no_none1(None)
|
||||||
|
assert "incompatible function arguments" in str(excinfo.value)
|
||||||
|
with pytest.raises(TypeError) as excinfo:
|
||||||
|
no_none2(None)
|
||||||
|
assert "incompatible function arguments" in str(excinfo.value)
|
||||||
|
with pytest.raises(TypeError) as excinfo:
|
||||||
|
no_none3(None)
|
||||||
|
assert "incompatible function arguments" in str(excinfo.value)
|
||||||
|
with pytest.raises(TypeError) as excinfo:
|
||||||
|
no_none4(None)
|
||||||
|
assert "incompatible function arguments" in str(excinfo.value)
|
||||||
|
with pytest.raises(TypeError) as excinfo:
|
||||||
|
no_none5(None)
|
||||||
|
assert "incompatible function arguments" in str(excinfo.value)
|
||||||
|
|
||||||
|
# The first one still raises because you can't pass None as a lvalue reference arg:
|
||||||
|
with pytest.raises(TypeError) as excinfo:
|
||||||
|
assert ok_none1(None) == -1
|
||||||
|
assert "incompatible function arguments" in str(excinfo.value)
|
||||||
|
# The rest take the argument as pointer or holder, and accept None:
|
||||||
|
assert ok_none2(None) == -1
|
||||||
|
assert ok_none3(None) == -1
|
||||||
|
assert ok_none4(None) == -1
|
||||||
|
assert ok_none5(None) == -1
|
||||||
|
Loading…
Reference in New Issue
Block a user