diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 037c430a0e..507f909f61 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -1202,7 +1202,7 @@ class type_caster_base : public type_caster_generic { }; inline std::string quote_cpp_type_name(const std::string &cpp_type_name) { - return cpp_type_name; // No-op for now. See PR #4888 + return "`" + cpp_type_name + "`"; // See PR #4888 } PYBIND11_NOINLINE std::string type_info_description(const std::type_info &ti) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f350943203..677725d859 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -117,6 +117,7 @@ set(PYBIND11_TEST_FILES test_builtin_casters test_call_policies test_callbacks + test_cases_for_stubgen test_chrono test_class test_const_name diff --git a/tests/test_cases_for_stubgen.cpp b/tests/test_cases_for_stubgen.cpp new file mode 100644 index 0000000000..cd947f97b1 --- /dev/null +++ b/tests/test_cases_for_stubgen.cpp @@ -0,0 +1,222 @@ +#include "pybind11/stl.h" +#include "pybind11/stl_bind.h" +#include "pybind11_tests.h" + +#include +#include +#include + +namespace test_cases_for_stubgen { + +// The `basics` code was copied from here (to have all test cases for stubgen in one place): +// https://github.com/python/mypy/blob/c6cb3c6282003dd3dadcf028735f9ba6190a0c84/test-data/pybind11_mypy_demo/src/main.cpp +// Copyright (c) 2016 The Pybind Development Team, All rights reserved. + +namespace basics { + +int answer() { return 42; } + +int sum(int a, int b) { return a + b; } + +double midpoint(double left, double right) { return left + (right - left) / 2; } + +double weighted_midpoint(double left, double right, double alpha = 0.5) { + return left + (right - left) * alpha; +} + +struct Point { + + enum class LengthUnit { mm = 0, pixel, inch }; + + enum class AngleUnit { radian = 0, degree }; + + Point() : Point(0, 0) {} + Point(double x, double y) : x(x), y(y) {} + + static const Point origin; + static const Point x_axis; + static const Point y_axis; + + static LengthUnit length_unit; + static AngleUnit angle_unit; + + double length() const { return std::sqrt(x * x + y * y); } + + double distance_to(double other_x, double other_y) const { + double dx = x - other_x; + double dy = y - other_y; + return std::sqrt(dx * dx + dy * dy); + } + + double distance_to(const Point &other) const { return distance_to(other.x, other.y); } + + double x, y; +}; + +const Point Point::origin = Point(0, 0); +const Point Point::x_axis = Point(1, 0); +const Point Point::y_axis = Point(0, 1); + +Point::LengthUnit Point::length_unit = Point::LengthUnit::mm; +Point::AngleUnit Point::angle_unit = Point::AngleUnit::radian; + +} // namespace basics + +void bind_basics(py::module &basics) { + + using namespace basics; + + // Functions + basics.def( + "answer", &answer, "answer docstring, with end quote\""); // tests explicit docstrings + basics.def("sum", &sum, "multiline docstring test, edge case quotes \"\"\"'''"); + basics.def("midpoint", &midpoint, py::arg("left"), py::arg("right")); + basics.def("weighted_midpoint", + weighted_midpoint, + py::arg("left"), + py::arg("right"), + py::arg("alpha") = 0.5); + + // Classes + py::class_ pyPoint(basics, "Point"); + py::enum_ pyLengthUnit(pyPoint, "LengthUnit"); + py::enum_ pyAngleUnit(pyPoint, "AngleUnit"); + + pyPoint.def(py::init<>()) + .def(py::init(), py::arg("x"), py::arg("y")) +#ifdef PYBIND11_CPP14 + .def("distance_to", + py::overload_cast(&Point::distance_to, py::const_), + py::arg("x"), + py::arg("y")) + .def("distance_to", + py::overload_cast(&Point::distance_to, py::const_), + py::arg("other")) +#else + .def("distance_to", + static_cast(&Point::distance_to), + py::arg("x"), + py::arg("y")) + .def("distance_to", + static_cast(&Point::distance_to), + py::arg("other")) +#endif + .def_readwrite("x", &Point::x) + .def_property( + "y", + [](Point &self) { return self.y; }, + [](Point &self, double value) { self.y = value; }) + .def_property_readonly("length", &Point::length) + .def_property_readonly_static("x_axis", [](py::handle /*cls*/) { return Point::x_axis; }) + .def_property_readonly_static("y_axis", [](py::handle /*cls*/) { return Point::y_axis; }) + .def_readwrite_static("length_unit", &Point::length_unit) + .def_property_static( + "angle_unit", + [](py::handle /*cls*/) { return Point::angle_unit; }, + [](py::handle /*cls*/, Point::AngleUnit value) { Point::angle_unit = value; }); + + pyPoint.attr("origin") = Point::origin; + + pyLengthUnit.value("mm", Point::LengthUnit::mm) + .value("pixel", Point::LengthUnit::pixel) + .value("inch", Point::LengthUnit::inch); + + pyAngleUnit.value("radian", Point::AngleUnit::radian) + .value("degree", Point::AngleUnit::degree); + + // Module-level attributes + basics.attr("PI") = std::acos(-1); + basics.attr("__version__") = "0.0.1"; +} + +struct UserType { + bool operator<(const UserType &) const { return false; } +}; + +struct minimal_caster { + static constexpr auto name = py::detail::const_name(); + + static py::handle + cast(UserType const & /*src*/, py::return_value_policy /*policy*/, py::handle /*parent*/) { + return py::none().release(); + } + + // Maximizing simplicity. This will go terribly wrong for other arg types. + template + using cast_op_type = const UserType &; + + // NOLINTNEXTLINE(google-explicit-constructor) + operator UserType const &() { + static UserType obj; + return obj; + } + + bool load(py::handle /*src*/, bool /*convert*/) { return false; } +}; + +} // namespace test_cases_for_stubgen + +namespace pybind11 { +namespace detail { + +template <> +struct type_caster : test_cases_for_stubgen::minimal_caster {}; + +} // namespace detail +} // namespace pybind11 + +PYBIND11_MAKE_OPAQUE(std::map); +PYBIND11_MAKE_OPAQUE(std::map); +PYBIND11_MAKE_OPAQUE(std::map); +PYBIND11_MAKE_OPAQUE(std::map); + +TEST_SUBMODULE(cases_for_stubgen, m) { + auto basics = m.def_submodule("basics"); + test_cases_for_stubgen::bind_basics(basics); + + using UserType = test_cases_for_stubgen::UserType; + + m.def("pass_user_type", [](const UserType &) {}); + m.def("return_user_type", []() { return UserType(); }); + + py::bind_map>(m, "MapIntUserType"); + py::bind_map>(m, "MapUserTypeInt"); + +#define LOCAL_HELPER(MapTypePythonName, ...) \ + py::class_<__VA_ARGS__>(m, MapTypePythonName) \ + .def( \ + "keys", \ + [](const __VA_ARGS__ &v) { return py::make_key_iterator(v); }, \ + py::keep_alive<0, 1>()) \ + .def( \ + "values", \ + [](const __VA_ARGS__ &v) { return py::make_value_iterator(v); }, \ + py::keep_alive<0, 1>()) \ + .def( \ + "__iter__", \ + [](const __VA_ARGS__ &v) { return py::make_iterator(v.begin(), v.end()); }, \ + py::keep_alive<0, 1>()) + + LOCAL_HELPER("MapFloatUserType", std::map); + LOCAL_HELPER("MapUserTypeFloat", std::map); +#undef LOCAL_HELPER + + m.def("pass_std_array_int_2", [](const std::array &) {}); + m.def("return_std_array_int_3", []() { return std::array{{1, 2, 3}}; }); + + // Rather arbitrary, meant to be a torture test for recursive processing. + using nested_case_01a = std::vector>; + using nested_case_02a = std::vector; + using nested_case_03a = std::map, UserType>; + using nested_case_04a = std::map; + using nested_case_05a = std::vector; + using nested_case_06a = std::map; +#define LOCAL_HELPER(name) m.def(#name, [](const name &) {}) + LOCAL_HELPER(nested_case_01a); + LOCAL_HELPER(nested_case_02a); + LOCAL_HELPER(nested_case_03a); + LOCAL_HELPER(nested_case_04a); + LOCAL_HELPER(nested_case_05a); + LOCAL_HELPER(nested_case_06a); +#undef LOCAL_HELPER +} diff --git a/tests/test_cases_for_stubgen.py b/tests/test_cases_for_stubgen.py new file mode 100644 index 0000000000..ed59aaedf9 --- /dev/null +++ b/tests/test_cases_for_stubgen.py @@ -0,0 +1,45 @@ +import pytest + +from pybind11_tests import cases_for_stubgen as m + +TEST_CASES = { + "m.basics.answer.__doc__": 'answer() -> int\n\nanswer docstring, with end quote"\n', + "m.basics.sum.__doc__": "sum(arg0: int, arg1: int) -> int\n\nmultiline docstring test, edge case quotes \"\"\"'''\n", + "m.basics.midpoint.__doc__": "midpoint(left: float, right: float) -> float\n", + "m.basics.weighted_midpoint.__doc__": "weighted_midpoint(left: float, right: float, alpha: float = 0.5) -> float\n", + "m.basics.Point.__init__.__doc__": "__init__(*args, **kwargs)\nOverloaded function.\n\n1. __init__(self: pybind11_tests.cases_for_stubgen.basics.Point) -> None\n\n2. __init__(self: pybind11_tests.cases_for_stubgen.basics.Point, x: float, y: float) -> None\n", + "m.basics.Point.distance_to.__doc__": "distance_to(*args, **kwargs)\nOverloaded function.\n\n1. distance_to(self: pybind11_tests.cases_for_stubgen.basics.Point, x: float, y: float) -> float\n\n2. distance_to(self: pybind11_tests.cases_for_stubgen.basics.Point, other: pybind11_tests.cases_for_stubgen.basics.Point) -> float\n", + "m.basics.Point.length_unit.__doc__": "Members:\n\n mm\n\n pixel\n\n inch", + "m.basics.Point.angle_unit.__doc__": "Members:\n\n radian\n\n degree", + "m.pass_user_type.__doc__": "pass_user_type(arg0: `test_cases_for_stubgen::UserType`) -> None\n", + "m.return_user_type.__doc__": "return_user_type() -> `test_cases_for_stubgen::UserType`\n", + "m.MapIntUserType.keys.__doc__": "keys(self: pybind11_tests.cases_for_stubgen.MapIntUserType) -> pybind11_tests.cases_for_stubgen.KeysView\n", + "m.MapIntUserType.values.__doc__": "values(self: pybind11_tests.cases_for_stubgen.MapIntUserType) -> pybind11_tests.cases_for_stubgen.ValuesView\n", + "m.MapIntUserType.items.__doc__": "items(self: pybind11_tests.cases_for_stubgen.MapIntUserType) -> pybind11_tests.cases_for_stubgen.ItemsView\n", + "m.MapUserTypeInt.keys.__doc__": "keys(self: pybind11_tests.cases_for_stubgen.MapUserTypeInt) -> pybind11_tests.cases_for_stubgen.KeysView\n", + "m.MapUserTypeInt.values.__doc__": "values(self: pybind11_tests.cases_for_stubgen.MapUserTypeInt) -> pybind11_tests.cases_for_stubgen.ValuesView\n", + "m.MapUserTypeInt.items.__doc__": "items(self: pybind11_tests.cases_for_stubgen.MapUserTypeInt) -> pybind11_tests.cases_for_stubgen.ItemsView\n", + "m.MapFloatUserType.keys.__doc__": "keys(self: pybind11_tests.cases_for_stubgen.MapFloatUserType) -> Iterator[float]\n", + "m.MapFloatUserType.values.__doc__": "values(self: pybind11_tests.cases_for_stubgen.MapFloatUserType) -> Iterator[`test_cases_for_stubgen::UserType`]\n", + "m.MapFloatUserType.__iter__.__doc__": "__iter__(self: pybind11_tests.cases_for_stubgen.MapFloatUserType) -> Iterator[tuple[float, `test_cases_for_stubgen::UserType`]]\n", + "m.MapUserTypeFloat.keys.__doc__": "keys(self: pybind11_tests.cases_for_stubgen.MapUserTypeFloat) -> Iterator[`test_cases_for_stubgen::UserType`]\n", + "m.MapUserTypeFloat.values.__doc__": "values(self: pybind11_tests.cases_for_stubgen.MapUserTypeFloat) -> Iterator[float]\n", + "m.MapUserTypeFloat.__iter__.__doc__": "__iter__(self: pybind11_tests.cases_for_stubgen.MapUserTypeFloat) -> Iterator[tuple[`test_cases_for_stubgen::UserType`, float]]\n", + "m.pass_std_array_int_2.__doc__": "pass_std_array_int_2(arg0: Annotated[list[int], FixedSize(2)]) -> None\n", + "m.return_std_array_int_3.__doc__": "return_std_array_int_3() -> Annotated[list[int], FixedSize(3)]\n", + "m.nested_case_01a.__doc__": "nested_case_01a(arg0: list[Annotated[list[int], FixedSize(2)]]) -> None\n", + "m.nested_case_02a.__doc__": "nested_case_02a(arg0: list[`test_cases_for_stubgen::UserType`]) -> None\n", + "m.nested_case_03a.__doc__": "nested_case_03a(arg0: dict[Annotated[list[int], FixedSize(2)], `test_cases_for_stubgen::UserType`]) -> None\n", + "m.nested_case_04a.__doc__": "nested_case_04a(arg0: dict[list[Annotated[list[int], FixedSize(2)]], list[`test_cases_for_stubgen::UserType`]]) -> None\n", + "m.nested_case_05a.__doc__": "nested_case_05a(arg0: list[dict[list[Annotated[list[int], FixedSize(2)]], list[`test_cases_for_stubgen::UserType`]]]) -> None\n", + "m.nested_case_06a.__doc__": "nested_case_06a(arg0: dict[dict[list[Annotated[list[int], FixedSize(2)]], list[`test_cases_for_stubgen::UserType`]], list[dict[list[Annotated[list[int], FixedSize(2)]], list[`test_cases_for_stubgen::UserType`]]]]) -> None\n", +} + + +@pytest.mark.parametrize("test_case", TEST_CASES.keys()) +def test_docstring(test_case): + assert dir(m) # Only direct use of m, to stop tooling from removing the import. + # On some platforms the stl_binders module name prevails for KeysView, ValuesView, ItemsView. + docstring = eval(test_case).replace(".stl_binders.", ".cases_for_stubgen.") + expected = TEST_CASES[test_case] + assert docstring == expected diff --git a/tests/test_numpy_dtypes.py b/tests/test_numpy_dtypes.py index 790f63641c..e5dc858b51 100644 --- a/tests/test_numpy_dtypes.py +++ b/tests/test_numpy_dtypes.py @@ -348,8 +348,8 @@ def test_complex_array(): def test_signature(doc): assert ( - doc(m.create_rec_nested) - == "create_rec_nested(arg0: int) -> numpy.ndarray[NestedStruct]" + doc(m.create_rec_nested) == "create_rec_nested(arg0: int) " + "-> numpy.ndarray[`NestedStruct`]" )