Skip to content

Proper pep695 generic function docstrings #5743

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion include/pybind11/attr.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> bound; ///< Upper Bound of the TypeVar
std::optional<std::string> default_; ///< Default value of the TypeVar
std::vector<std::string> constraints; ///< Constraints (mutually exclusive with bound)

typevar_record(const char *name,
std::optional<std::string> bound,
std::optional<std::string> default_,
std::vector<std::string> 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 <typename Bound>
static typevar_record with_bound(const char *name) {
return typevar_record(name, generate_type_signature<Bound>(), std::nullopt, {});
}

template <typename... Constraints>
static typevar_record with_constraints(const char *name) {
return typevar_record(
name, std::nullopt, std::nullopt, {generate_type_signature<Constraints>()...});
}

template <typename Default>
static typevar_record with_default(const char *name) {
return typevar_record(name, std::nullopt, generate_type_signature<Default>(), {});
}

template <typename Default, typename Bound>
static typevar_record with_default_and_bound(const char *name) {
return typevar_record(
name, generate_type_signature<Bound>(), generate_type_signature<Default>(), {});
}

template <typename Default, typename... Constraints>
static typevar_record with_default_and_constraints(const char *name) {
return typevar_record(name,
std::nullopt,
generate_type_signature<Default>(),
{generate_type_signature<Constraints>()...});
}
};
#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),
Expand Down Expand Up @@ -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<typevar_record> type_vars;

/// Number of arguments (including py::args and/or py::kwargs, if present)
std::uint16_t nargs;

Expand Down Expand Up @@ -482,6 +538,12 @@ struct process_attribute<is_new_style_constructor>
}
};

/// Process an attribute which adds a typevariable
template <>
struct process_attribute<typevar_record> : process_attribute_default<typevar_record> {
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 "
Expand Down
48 changes: 48 additions & 0 deletions include/pybind11/pybind11.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<detail::typevar_record> 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<detail::typevar_record> &) {
return "";
}
#endif

/* Generate a proper function signature */
inline std::string generate_function_signature(const char *type_caster_name_field,
detail::function_record *func_rec,
Expand All @@ -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 == '{') {
Expand Down
88 changes: 81 additions & 7 deletions tests/test_pytypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<typevar::TypeVarT> &l) -> py::typing::List<typevar::TypeVarV> {
return l;
});
m.def(
"annotate_generic_containers",
[](const py::typing::List<typevar::TypeVarT> &l) -> py::typing::List<typevar::TypeVarV> {
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<typevar::TypeVarT> &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<int>("T"));

m.def(
"typevar_constraints_int_str",
[](const typevar::TypeVarT &) -> void {},
pybind11::detail::typevar_record::with_constraints<py::int_, py::str>("T"));

m.def(
"typevar_default_int",
[](const typevar::TypeVarT &) -> void {},
pybind11::detail::typevar_record::with_default<int>("T"));

m.def(
"typevar_bound_and_default_int",
[](const typevar::TypeVarT &) -> void {},
pybind11::detail::typevar_record::with_default_and_bound<int, int>("T"));

m.def(
"typevar_constraints_and_default",
[](const typevar::TypeVarT &) -> void {},
pybind11::detail::typevar_record::
with_default_and_constraints<py::str, py::typing::List<int>, py::str>("T"));

m.def("annotate_listT_to_T",
[](const py::typing::List<typevar::TypeVarT> &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<int>("T"));

m.def(
"generic_constraints_int_str",
[]() -> void {},
pybind11::detail::typevar_record::with_constraints<py::int_, py::str>("T"));

m.def(
"generic_default_int",
[]() -> void {},
pybind11::detail::typevar_record::with_default<int>("T"));

m.def(
"generic_bound_and_default_int",
[]() -> void {},
pybind11::detail::typevar_record::with_default_and_bound<int, int>("T"));

m.def(
"generic_constraints_and_default",
[]() -> void {},
pybind11::detail::typevar_record::
with_default_and_constraints<py::str, py::typing::List<int>, 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
Expand Down
63 changes: 60 additions & 3 deletions tests/test_pytypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1085,19 +1085,76 @@ 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.",
)
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(
Expand Down
Loading