fix: property docstrings missing self (#6252) * Mark lambda properties as docstrings Pybind11 does not mark properties as is_method leading to self in the docstring being arg0 * style: pre-commit fixes * Add constructor * style: pre-commit fixes * Add tests * style: pre-commit fixes * Replace nullptr constructors with template * De-duplicate docstrings * Add lambda and cpp_function tests * style: pre-commit fixes * Fix lambda setter * fix: avoid nullptr ambiguity in cpp_function, xfail cpp_function docstring The lambda constructor accepted std::nullptr_t, which made calls with nullptr and extras ambiguous on GCC 8/9. Also update the late-arg strdup comment, and mark the user-supplied cpp_function docstring case as a known limitation. Assisted-by: ClaudeCode:claude-opus-5-5 * fix: merge nullptr cpp_function constructors, split xfail test Fold cpp_function(std::nullptr_t) into the variadic overload with a NOLINT for google-explicit-constructor, which clang-tidy flagged. Test the getter and setter cpp_function docstrings separately. Assisted-by: ClaudeCode:claude-opus-5-5 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Henry Schreiner <henryfs@princeton.edu>
diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index e8bcadd..2459a12 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h
@@ -143,9 +143,9 @@ class cpp_function : public function { public: cpp_function() = default; + template <typename... Extra> // NOLINTNEXTLINE(google-explicit-constructor) - cpp_function(std::nullptr_t) {} - cpp_function(std::nullptr_t, const is_setter &) {} + cpp_function(std::nullptr_t, const Extra &...) {} /// Construct a cpp_function from a vanilla function pointer template <typename Return, typename... Args, typename... Extra> @@ -157,7 +157,9 @@ /// Construct a cpp_function from a lambda function (possibly with internal state) template <typename Func, typename... Extra, - typename = detail::enable_if_t<detail::is_lambda<Func>::value>> + typename = detail::enable_if_t< + detail::is_lambda<Func>::value + && !detail::is_same_ignoring_cvref<Func, std::nullptr_t>::value>> // NOLINTNEXTLINE(google-explicit-constructor) cpp_function(Func &&f, const Extra &...extra) { initialize( @@ -1585,7 +1587,7 @@ template <typename Getter, typename... Extra> class_ &def_property_readonly(const char *name, const Getter &fget, const Extra &...extra) { return def_property_readonly(name, - cpp_function(method_adaptor<type>(fget)), + cpp_function(method_adaptor<type>(fget), is_method(*this)), return_value_policy::reference_internal, extra...); } @@ -1618,7 +1620,10 @@ class_ & def_property(const char *name, const Getter &fget, const Setter &fset, const Extra &...extra) { return def_property( - name, fget, cpp_function(method_adaptor<type>(fset), is_setter()), extra...); + name, + fget, + cpp_function(method_adaptor<type>(fset), is_setter(), is_method(*this)), + extra...); } template <typename Getter, typename... Extra> class_ &def_property(const char *name, @@ -1626,7 +1631,7 @@ const cpp_function &fset, const Extra &...extra) { return def_property(name, - cpp_function(method_adaptor<type>(fget)), + cpp_function(method_adaptor<type>(fget), is_method(*this)), fset, return_value_policy::reference_internal, extra...); @@ -1676,10 +1681,10 @@ std::free(doc_prev); rec_fget->doc = PYBIND11_COMPAT_STRDUP(rec_fget->doc); } - // Args added by process_attributes (e.g. "self" via is_method + pos_only/kw_only) - // need their strings strdup'd: initialize_generic's strdup loop already ran during - // cpp_function construction, so it won't process these late additions. Without this, - // destruct() would call free() on string literals. See gh-5976. + // process_attributes can add args after construction, e.g. "self" from is_method + + // pos_only/kw_only when a user-supplied cpp_function lacks is_method. The strdup loop + // in initialize_generic already ran, so strdup them here, or destruct() would free() + // string literals. See gh-5976. for (auto i = args_before; i < rec_fget->args.size(); ++i) { if (rec_fget->args[i].name) { rec_fget->args[i].name = PYBIND11_COMPAT_STRDUP(rec_fget->args[i].name);
diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index e80780c..7fa7555 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp
@@ -433,6 +433,13 @@ .def_property("def_property_writeonly", nullptr, &TestProperties::set) .def_property_readonly("def_property_readonly", &TestProperties::get) .def_property("def_property", &TestProperties::get, &TestProperties::set) + .def_property( + "def_property_lambda", + [](const TestProperties &self) { return self.get(); }, + [](TestProperties &self, int value) { return self.set(value); }) + .def_property("def_property_cpp_function", + py::cpp_function(&TestProperties::get), + py::cpp_function(&TestProperties::set)) .def_property("def_property_impossible", nullptr, nullptr) .def_readonly_static("def_readonly_static", &TestProperties::static_value) .def_readwrite_static("def_readwrite_static", &TestProperties::static_value)
diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 6ee4f5a..1d5d8ae 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py
@@ -168,6 +168,80 @@ assert NO_SETTER_MSG in str(excinfo.value) +TestPropertiesGetDocstring = ( + "(self: pybind11_tests.methods_and_attributes.TestProperties) -> int" +) +TestPropertiesSetDocstring = "(self: pybind11_tests.methods_and_attributes.TestProperties, arg0: typing.SupportsInt | typing.SupportsIndex) -> None" + + +def test_properties_docstring(): + assert ( + m.TestProperties.def_readonly.fget.__doc__.strip() == TestPropertiesGetDocstring + ) + + assert ( + m.TestProperties.def_readwrite.fget.__doc__.strip() + == TestPropertiesGetDocstring + ) + assert ( + m.TestProperties.def_readwrite.fset.__doc__.strip() + == TestPropertiesSetDocstring + ) + + assert ( + m.TestProperties.def_writeonly.fset.__doc__.strip() + == TestPropertiesSetDocstring + ) + + assert ( + m.TestProperties.def_property_writeonly.fset.__doc__.strip() + == TestPropertiesSetDocstring + ) + + assert ( + m.TestProperties.def_property_readonly.fget.__doc__.strip() + == TestPropertiesGetDocstring + ) + + assert ( + m.TestProperties.def_property.fget.__doc__.strip() == TestPropertiesGetDocstring + ) + assert ( + m.TestProperties.def_property.fset.__doc__.strip() == TestPropertiesSetDocstring + ) + + assert ( + m.TestProperties.def_property_lambda.fget.__doc__.strip() + == TestPropertiesGetDocstring + ) + assert ( + m.TestProperties.def_property_lambda.fset.__doc__.strip() + == TestPropertiesSetDocstring + ) + + +@pytest.mark.xfail( + reason="The signature of a cpp_function without is_method is fixed at construction", + strict=True, +) +def test_properties_docstring_cpp_function_getter(): + assert ( + m.TestProperties.def_property_cpp_function.fget.__doc__.strip() + == TestPropertiesGetDocstring + ) + + +@pytest.mark.xfail( + reason="The signature of a cpp_function without is_method is fixed at construction", + strict=True, +) +def test_properties_docstring_cpp_function_setter(): + assert ( + m.TestProperties.def_property_cpp_function.fset.__doc__.strip() + == TestPropertiesSetDocstring + ) + + def test_static_properties(): assert m.TestProperties.def_readonly_static == 1 with pytest.raises(AttributeError) as excinfo: