diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index 9b631fa48d..a2fd189ead 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -191,9 +191,62 @@ struct argument_record { : name(name), descr(descr), value(value), convert(convert), none(none) {} }; +#if defined(PYBIND11_HAS_OPTIONAL) +/// Internal data structure which holds metadata about a typevar argument +struct typevar_record { + const char *name; ///< TypeVar name + std::optional bound; ///< Upper Bound of the TypeVar + std::optional default_; ///< Default value of the TypeVar + std::vector constraints; ///< Constraints (mutually exclusive with bound) + + typevar_record(const char *name, + std::optional bound, + std::optional default_, + std::vector constraints) + : name(name), bound(std::move(bound)), default_(std::move(default_)), + constraints(std::move(constraints)) {} + + static typevar_record from_name(const char *name) { + return typevar_record(name, std::nullopt, std::nullopt, {}); + } + + template + static typevar_record with_bound(const char *name) { + return typevar_record(name, generate_type_signature(), std::nullopt, {}); + } + + template + static typevar_record with_constraints(const char *name) { + return typevar_record( + name, std::nullopt, std::nullopt, {generate_type_signature()...}); + } + + template + static typevar_record with_default(const char *name) { + return typevar_record(name, std::nullopt, generate_type_signature(), {}); + } + + template + static typevar_record with_default_and_bound(const char *name) { + return typevar_record( + name, generate_type_signature(), generate_type_signature(), {}); + } + + template + static typevar_record with_default_and_constraints(const char *name) { + return typevar_record(name, + std::nullopt, + generate_type_signature(), + {generate_type_signature()...}); + } +}; +#else +struct typevar_record {}; +#endif + /// Internal data structure which holds metadata about a bound function (signature, overloads, /// etc.) -#define PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID "v1" // PLEASE UPDATE if the struct is changed. +#define PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID "v2" // PLEASE UPDATE if the struct is changed. struct function_record { function_record() : is_constructor(false), is_new_style_constructor(false), is_stateless(false), @@ -251,6 +304,9 @@ struct function_record { /// True if this function is to be inserted at the beginning of the overload resolution chain bool prepend : 1; + /// List of registered typevar arguments + std::vector type_vars; + /// Number of arguments (including py::args and/or py::kwargs, if present) std::uint16_t nargs; @@ -482,6 +538,12 @@ struct process_attribute } }; +/// Process an attribute which adds a typevariable +template <> +struct process_attribute : process_attribute_default { + static void init(const typevar_record &t, function_record *r) { r->type_vars.push_back(t); } +}; + inline void check_kw_only_arg(const arg &a, function_record *r) { if (r->args.size() > r->nargs_pos && (!a.name || a.name[0] == '\0')) { pybind11_fail("arg(): cannot specify an unnamed argument after a kw_only() annotation or " diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 06be7f1d4f..35fdb21882 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -101,6 +101,50 @@ inline std::string replace_newlines_and_squash(const char *text) { return result.substr(str_begin, str_range); } +#if defined(PYBIND11_HAS_OPTIONAL) +/* Generate the pep695 typevariable part of function signatures */ +inline std::string generate_typevariable_component(std::vector type_vars) { + std::string signature; + + for (auto it = type_vars.begin(); it != type_vars.end(); ++it) { + const auto &type_var = *it; + signature += type_var.name; + + if (type_var.bound != std::nullopt) { + signature += ": "; + signature += *type_var.bound; + } + + auto constraints = type_var.constraints; + if (!constraints.empty()) { + signature += ": ("; + for (size_t j = 0; j < constraints.size(); ++j) { + signature += constraints[j]; + if (j != constraints.size() - 1) { + signature += ", "; + } + } + signature += ")"; + } + + if (type_var.default_ != std::nullopt) { + signature += " = "; + signature += *type_var.default_; + } + + if (std::next(it) != type_vars.end()) { + signature += ", "; + } + } + + return "[" + signature + "]"; +} +#else +inline std::string generate_typevariable_component(std::vector &) { + return ""; +} +#endif + /* Generate a proper function signature */ inline std::string generate_function_signature(const char *type_caster_name_field, detail::function_record *func_rec, @@ -116,6 +160,10 @@ inline std::string generate_function_signature(const char *type_caster_name_fiel // The following characters have special meaning in the signature parsing. Literals // containing these are escaped with `!`. std::string special_chars("!@%{}-"); + + if (!func_rec->type_vars.empty()) { + signature += generate_typevariable_component(func_rec->type_vars); + } for (const auto *pc = type_caster_name_field; *pc != '\0'; ++pc) { const auto c = *pc; if (c == '{') { diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index 1c136cf0f2..f8bac58a28 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -986,19 +986,93 @@ TEST_SUBMODULE(pytypes, m) { const RealNumber &)> &x) { return x; }); m.def("identity_literal_all_special_chars", [](const py::typing::Literal<"\"!@!!->{%}\""> &x) { return x; }); - m.def("annotate_generic_containers", - [](const py::typing::List &l) -> py::typing::List { - return l; - }); + m.def( + "annotate_generic_containers", + [](const py::typing::List &l) -> py::typing::List { + return l; + }, + pybind11::detail::typevar_record::from_name("T"), + pybind11::detail::typevar_record::from_name("V")); + + m.def( + "annotate_listT_to_T", + [](const py::typing::List &l) -> typevar::TypeVarT { return l[0]; }, + pybind11::detail::typevar_record::from_name("T")); + m.def( + "annotate_object_to_T", + [](const py::object &o) -> typevar::TypeVarT { return o; }, + pybind11::detail::typevar_record::from_name("T")); + + m.def( + "typevar_bound_int", + [](const typevar::TypeVarT &) -> void {}, + pybind11::detail::typevar_record::with_bound("T")); + + m.def( + "typevar_constraints_int_str", + [](const typevar::TypeVarT &) -> void {}, + pybind11::detail::typevar_record::with_constraints("T")); + + m.def( + "typevar_default_int", + [](const typevar::TypeVarT &) -> void {}, + pybind11::detail::typevar_record::with_default("T")); + + m.def( + "typevar_bound_and_default_int", + [](const typevar::TypeVarT &) -> void {}, + pybind11::detail::typevar_record::with_default_and_bound("T")); + + m.def( + "typevar_constraints_and_default", + [](const typevar::TypeVarT &) -> void {}, + pybind11::detail::typevar_record:: + with_default_and_constraints, py::str>("T")); - m.def("annotate_listT_to_T", - [](const py::typing::List &l) -> typevar::TypeVarT { return l[0]; }); - m.def("annotate_object_to_T", [](const py::object &o) -> typevar::TypeVarT { return o; }); m.attr("defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL") = true; #else m.attr("defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL") = false; #endif +#if defined(PYBIND11_TEST_PTYPES_HAS_OPTIONAL) + m.def("generic_T", []() -> void {}, pybind11::detail::typevar_record::from_name("T")); + + m.def( + "generic_T_V", + []() -> void {}, + pybind11::detail::typevar_record::from_name("T"), + pybind11::detail::typevar_record::from_name("V")); + + m.def( + "generic_bound_int", + []() -> void {}, + pybind11::detail::typevar_record::with_bound("T")); + + m.def( + "generic_constraints_int_str", + []() -> void {}, + pybind11::detail::typevar_record::with_constraints("T")); + + m.def( + "generic_default_int", + []() -> void {}, + pybind11::detail::typevar_record::with_default("T")); + + m.def( + "generic_bound_and_default_int", + []() -> void {}, + pybind11::detail::typevar_record::with_default_and_bound("T")); + + m.def( + "generic_constraints_and_default", + []() -> void {}, + pybind11::detail::typevar_record:: + with_default_and_constraints, py::str>("T")); + m.attr("defined_PYBIND11_TEST_PTYPES_HAS_OPTIONAL") = true; +#else + m.attr("defined_PYBIND11_TEST_PTYPES_HAS_OPTIONAL") = false; +#endif + #if defined(PYBIND11_TEST_PYTYPES_HAS_RANGES) // test_tuple_ranges diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index a199d72f0a..ca84e77644 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -1085,6 +1085,38 @@ def test_literal(doc): ) +@pytest.mark.skipif( + not m.defined_PYBIND11_TEST_PTYPES_HAS_OPTIONAL, + reason="use of optional feature not available.", +) +def test_generic(doc): + assert doc(m.generic_T) == "generic_T[T]() -> None" + + assert ( + doc(m.generic_bound_int) == "generic_bound_int[T: typing.SupportsInt]() -> None" + ) + + assert ( + doc(m.generic_constraints_int_str) + == "generic_constraints_int_str[T: (typing.SupportsInt, str)]() -> None" + ) + + assert ( + doc(m.generic_default_int) + == "generic_default_int[T = typing.SupportsInt]() -> None" + ) + + assert ( + doc(m.generic_bound_and_default_int) + == "generic_bound_and_default_int[T: typing.SupportsInt = typing.SupportsInt]() -> None" + ) + + assert ( + doc(m.generic_constraints_and_default) + == "generic_constraints_and_default[T: (list[typing.SupportsInt], str) = str]() -> None" + ) + + @pytest.mark.skipif( not m.defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL, reason="C++20 non-type template args feature not available.", @@ -1092,12 +1124,37 @@ def test_literal(doc): def test_typevar(doc): assert ( doc(m.annotate_generic_containers) - == "annotate_generic_containers(arg0: list[T]) -> list[V]" + == "annotate_generic_containers[T, V](arg0: list[T]) -> list[V]" ) - assert doc(m.annotate_listT_to_T) == "annotate_listT_to_T(arg0: list[T]) -> T" + assert doc(m.annotate_listT_to_T) == "annotate_listT_to_T[T](arg0: list[T]) -> T" + + assert doc(m.annotate_object_to_T) == "annotate_object_to_T[T](arg0: object) -> T" + + assert ( + doc(m.typevar_bound_int) + == "typevar_bound_int[T: typing.SupportsInt](arg0: T) -> None" + ) + + assert ( + doc(m.typevar_constraints_int_str) + == "typevar_constraints_int_str[T: (typing.SupportsInt, str)](arg0: T) -> None" + ) - assert doc(m.annotate_object_to_T) == "annotate_object_to_T(arg0: object) -> T" + assert ( + doc(m.typevar_default_int) + == "typevar_default_int[T = typing.SupportsInt](arg0: T) -> None" + ) + + assert ( + doc(m.typevar_bound_and_default_int) + == "typevar_bound_and_default_int[T: typing.SupportsInt = typing.SupportsInt](arg0: T) -> None" + ) + + assert ( + doc(m.typevar_constraints_and_default) + == "typevar_constraints_and_default[T: (list[typing.SupportsInt], str) = str](arg0: T) -> None" + ) @pytest.mark.skipif(