diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index 57517fa0..c7ff8714 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -179,13 +179,20 @@ struct argument_record { const char *name; ///< Argument name const char *descr; ///< Human-readable version of the argument value handle value; ///< Associated Python object + // The explicit value_is_nullptr variable is for safety, to unambiguously + // distinguish these two cases in all situations: + // * value is nullptr on purpose (value_is_nullptr true). + // * value is nullptr because of an error condition (value_is_nullptr false). + bool value_is_nullptr; from_python_policies policies; argument_record(const char *name, const char *descr, handle value, + bool value_is_nullptr, const from_python_policies &policies) - : name(name), descr(descr), value(value), policies(policies) {} + : name(name), descr(descr), value(value), value_is_nullptr(value_is_nullptr), + policies(policies) {} }; /// Internal data structure which holds metadata about a bound function (signature, overloads, @@ -474,8 +481,11 @@ inline void check_kw_only_arg(const arg &a, function_record *r) { inline void append_self_arg_if_needed(function_record *r) { if (r->is_method && r->args.empty()) { - r->args.emplace_back( - "self", nullptr, handle(), from_python_policies(/*convert=*/true, /*none=*/false)); + r->args.emplace_back("self", + nullptr, + handle(), + /*value_is_nullptr=*/false, + from_python_policies(/*convert=*/true, /*none=*/false)); } } @@ -488,6 +498,7 @@ struct process_attribute : process_attribute_default { a.name, nullptr, handle(), + /*value_is_nullptr=*/false, from_python_policies(a.m_policies.rvpp, !a.flag_noconvert, a.flag_none)); check_kw_only_arg(a, r); @@ -504,10 +515,11 @@ struct process_attribute : process_attribute_default { r->args.emplace_back("self", /*descr=*/nullptr, /*parent=*/handle(), + /*value_is_nullptr=*/false, from_python_policies(/*convert=*/true, /*none=*/false)); } - if (!a.value) { + if (!a.value && !a.value_is_nullptr) { #if defined(PYBIND11_DETAILED_ERROR_MESSAGES) std::string descr("'"); if (a.name) { @@ -537,6 +549,7 @@ struct process_attribute : process_attribute_default { a.name, a.descr, a.value.inc_ref(), + a.value_is_nullptr, from_python_policies(a.m_policies.rvpp, !a.flag_noconvert, a.flag_none)); check_kw_only_arg(a, r); diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 4eb6a1fb..1ff870b2 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1535,6 +1535,8 @@ struct arg : detail::arg_base { from_python_policies m_policies; }; +struct nullptr_default_arg {}; + /// \ingroup annotations /// Annotation for arguments with values struct arg_v : arg { @@ -1544,6 +1546,16 @@ struct arg_v : arg { private: #endif friend struct arg_base; + + arg_v(arg &&base, nullptr_default_arg, const char *descr = nullptr) + : arg(base), value(), value_is_nullptr(true), descr(descr) +#if defined(PYBIND11_DETAILED_ERROR_MESSAGES) + , + type("pybind11::nullptr_default_arg") +#endif + { + } + template arg_v(arg &&base, T &&x, const char *descr = nullptr) : arg(base), value(reinterpret_steal(detail::make_caster::cast( @@ -1587,6 +1599,7 @@ struct arg_v : arg { /// The default value object value; + bool value_is_nullptr = false; /// The (optional) description of the default value const char *descr; #if defined(PYBIND11_DETAILED_ERROR_MESSAGES) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 137921a1..3c28829a 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -910,7 +910,7 @@ class cpp_function : public function { break; } - if (value) { + if (value || arg_rec.value_is_nullptr) { // If we're at the py::args index then first insert a stub for it to be // replaced later if (func.has_args && call.args.size() == func.nargs_pos) { diff --git a/tests/test_type_caster_pyobject_ptr.cpp b/tests/test_type_caster_pyobject_ptr.cpp index 8069f7dc..6bab988f 100644 --- a/tests/test_type_caster_pyobject_ptr.cpp +++ b/tests/test_type_caster_pyobject_ptr.cpp @@ -127,4 +127,28 @@ TEST_SUBMODULE(type_caster_pyobject_ptr, m) { (void) py::cast(*ptr); } #endif + + // This test exercises functionality (`py::cast(handle_nullptr)`) + // that is needed indirectly below in py_arg_handle_nullptr. + m.def("pyobject_ptr_from_handle_nullptr", []() { + py::handle handle_nullptr; + if (handle_nullptr.ptr() != nullptr) { + return "UNEXPECTED: handle_nullptr.ptr() != nullptr"; + } + auto *pyobject_ptr_from_handle = py::cast(handle_nullptr); + if (pyobject_ptr_from_handle != nullptr) { + return "UNEXPECTED: pyobject_ptr_from_handle != nullptr"; + } + return "SUCCESS"; + }); + + m.def( + "py_arg_handle_nullptr", + [](PyObject *ptr) { + if (ptr == nullptr) { + return "ptr == nullptr"; + } + return py::detail::obj_class_name(ptr); + }, + py::arg("ptr") = py::nullptr_default_arg()); } diff --git a/tests/test_type_caster_pyobject_ptr.py b/tests/test_type_caster_pyobject_ptr.py index 1f1ece2b..367af458 100644 --- a/tests/test_type_caster_pyobject_ptr.py +++ b/tests/test_type_caster_pyobject_ptr.py @@ -102,3 +102,13 @@ def test_return_list_pyobject_ptr_reference(): def test_type_caster_name_via_incompatible_function_arguments_type_error(): with pytest.raises(TypeError, match=r"1\. \(arg0: object, arg1: int\) -> None"): m.pass_pyobject_ptr_and_int(ValueHolder(101), ValueHolder(202)) + + +def test_pyobject_ptr_from_handle_nullptr(): + assert m.pyobject_ptr_from_handle_nullptr() == "SUCCESS" + + +def test_py_arg_handle_nullptr(): + assert m.py_arg_handle_nullptr(None) == "NoneType" + assert m.py_arg_handle_nullptr([]) == "list" + assert m.py_arg_handle_nullptr() == "ptr == nullptr"