Skip to content

Commit 1b7aa0b

Browse files
feat: rework of arg/return type hints to support .noconvert() (#5486)
* Added rework of arg/return typing * Changed `Path` to `pathlib.Path` for compatibility with pybind11-stubgen * Removed old arg/return type hint implementation * Added noconvert support for arg/return type hints * Added commented failing tests for Literals with special characters * Added return_descr/arg_descr for correct typing in typing::Callable * Fixed clang-tidy issues * Changed io_name to have explicit return type (for C++11 support) * style: pre-commit fixes * Added support for nested callables * Fixed missing include * Fixed is_return_value constructor call * Fixed clang-tidy issue * Uncommented test cases for special characters in literals * Moved literal tests to correct test case * Added escaping of special characters in typing::Literal * Readded mistakenly deleted bracket * Moved sanitize_string_literal to correct namespace * Added test for Literal with `!` and changed StringLiteral template param name * Added test for Literal with multiple and repeated special chars * Simplified string literal sanitization function * Added test for `->` in literal * Added test for `->` with io_name * Removed unused parameter name to prevent warning * Added escaping of `-` in literal to prevent processing of `->` * Fixed wrong computation of sanitized string literal length * Added cast to prevent error with MSVC * Simplified special character check --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 15d9dae commit 1b7aa0b

File tree

10 files changed

+230
-131
lines changed

10 files changed

+230
-131
lines changed

docs/advanced/cast/custom.rst

+6-8
Original file line numberDiff line numberDiff line change
@@ -61,27 +61,25 @@ type is explicitly allowed.
6161
6262
template <>
6363
struct type_caster<user_space::Point2D> {
64-
// This macro inserts a lot of boilerplate code and sets the default type hint to `tuple`
65-
PYBIND11_TYPE_CASTER(user_space::Point2D, const_name("tuple"));
66-
// `arg_name` and `return_name` may optionally be used to specify type hints separately for
67-
// arguments and return values.
64+
// This macro inserts a lot of boilerplate code and sets the type hint.
65+
// `io_name` is used to specify different type hints for arguments and return values.
6866
// The signature of our negate function would then look like:
6967
// `negate(Sequence[float]) -> tuple[float, float]`
70-
static constexpr auto arg_name = const_name("Sequence[float]");
71-
static constexpr auto return_name = const_name("tuple[float, float]");
68+
PYBIND11_TYPE_CASTER(user_space::Point2D, io_name("Sequence[float]", "tuple[float, float]"));
7269
7370
// C++ -> Python: convert `Point2D` to `tuple[float, float]`. The second and third arguments
7471
// are used to indicate the return value policy and parent object (for
7572
// return_value_policy::reference_internal) and are often ignored by custom casters.
76-
// The return value should reflect the type hint specified by `return_name`.
73+
// The return value should reflect the type hint specified by the second argument of `io_name`.
7774
static handle
7875
cast(const user_space::Point2D &number, return_value_policy /*policy*/, handle /*parent*/) {
7976
return py::make_tuple(number.x, number.y).release();
8077
}
8178
8279
// Python -> C++: convert a `PyObject` into a `Point2D` and return false upon failure. The
8380
// second argument indicates whether implicit conversions should be allowed.
84-
// The accepted types should reflect the type hint specified by `arg_name`.
81+
// The accepted types should reflect the type hint specified by the first argument of
82+
// `io_name`.
8583
bool load(handle src, bool /*convert*/) {
8684
// Check if handle is a Sequence
8785
if (!py::isinstance<py::sequence>(src)) {

include/pybind11/cast.h

+1-36
Original file line numberDiff line numberDiff line change
@@ -34,39 +34,6 @@ PYBIND11_WARNING_DISABLE_MSVC(4127)
3434

3535
PYBIND11_NAMESPACE_BEGIN(detail)
3636

37-
// Type trait checker for `descr`
38-
template <typename>
39-
struct is_descr : std::false_type {};
40-
41-
template <size_t N, typename... Ts>
42-
struct is_descr<descr<N, Ts...>> : std::true_type {};
43-
44-
template <size_t N, typename... Ts>
45-
struct is_descr<const descr<N, Ts...>> : std::true_type {};
46-
47-
// Use arg_name instead of name when available
48-
template <typename T, typename SFINAE = void>
49-
struct as_arg_type {
50-
static constexpr auto name = T::name;
51-
};
52-
53-
template <typename T>
54-
struct as_arg_type<T, typename std::enable_if<is_descr<decltype(T::arg_name)>::value>::type> {
55-
static constexpr auto name = T::arg_name;
56-
};
57-
58-
// Use return_name instead of name when available
59-
template <typename T, typename SFINAE = void>
60-
struct as_return_type {
61-
static constexpr auto name = T::name;
62-
};
63-
64-
template <typename T>
65-
struct as_return_type<T,
66-
typename std::enable_if<is_descr<decltype(T::return_name)>::value>::type> {
67-
static constexpr auto name = T::return_name;
68-
};
69-
7037
template <typename type, typename SFINAE = void>
7138
class type_caster : public type_caster_base<type> {};
7239
template <typename type>
@@ -1113,8 +1080,6 @@ struct pyobject_caster {
11131080
return src.inc_ref();
11141081
}
11151082
PYBIND11_TYPE_CASTER(type, handle_type_name<type>::name);
1116-
static constexpr auto arg_name = as_arg_type<handle_type_name<type>>::name;
1117-
static constexpr auto return_name = as_return_type<handle_type_name<type>>::name;
11181083
};
11191084

11201085
template <typename T>
@@ -1668,7 +1633,7 @@ class argument_loader {
16681633
"py::args cannot be specified more than once");
16691634

16701635
static constexpr auto arg_names
1671-
= ::pybind11::detail::concat(type_descr(as_arg_type<make_caster<Args>>::name)...);
1636+
= ::pybind11::detail::concat(type_descr(make_caster<Args>::name)...);
16721637

16731638
bool load_args(function_call &call) { return load_impl_sequence(call, indices{}); }
16741639

include/pybind11/detail/descr.h

+17
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ constexpr descr<1, Type> const_name() {
9999
return {'%'};
100100
}
101101

102+
// Use a different name based on whether the parameter is used as input or output
103+
template <size_t N1, size_t N2>
104+
constexpr descr<N1 + N2 + 1> io_name(char const (&text1)[N1], char const (&text2)[N2]) {
105+
return const_name("@") + const_name(text1) + const_name("@") + const_name(text2)
106+
+ const_name("@");
107+
}
108+
102109
// If "_" is defined as a macro, py::detail::_ cannot be provided.
103110
// It is therefore best to use py::detail::const_name universally.
104111
// This block is for backward compatibility only.
@@ -167,5 +174,15 @@ constexpr descr<N + 2, Ts...> type_descr(const descr<N, Ts...> &descr) {
167174
return const_name("{") + descr + const_name("}");
168175
}
169176

177+
template <size_t N, typename... Ts>
178+
constexpr descr<N + 4, Ts...> arg_descr(const descr<N, Ts...> &descr) {
179+
return const_name("@^") + descr + const_name("@!");
180+
}
181+
182+
template <size_t N, typename... Ts>
183+
constexpr descr<N + 4, Ts...> return_descr(const descr<N, Ts...> &descr) {
184+
return const_name("@$") + descr + const_name("@!");
185+
}
186+
170187
PYBIND11_NAMESPACE_END(detail)
171188
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

include/pybind11/pybind11.h

+60-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <cstring>
2323
#include <memory>
2424
#include <new>
25+
#include <stack>
2526
#include <string>
2627
#include <utility>
2728
#include <vector>
@@ -336,8 +337,8 @@ class cpp_function : public function {
336337

337338
/* Generate a readable signature describing the function's arguments and return
338339
value types */
339-
static constexpr auto signature = const_name("(") + cast_in::arg_names
340-
+ const_name(") -> ") + as_return_type<cast_out>::name;
340+
static constexpr auto signature
341+
= const_name("(") + cast_in::arg_names + const_name(") -> ") + cast_out::name;
341342
PYBIND11_DESCR_CONSTEXPR auto types = decltype(signature)::types();
342343

343344
/* Register the function with Python from generic (non-templated) code */
@@ -440,6 +441,13 @@ class cpp_function : public function {
440441
std::string signature;
441442
size_t type_index = 0, arg_index = 0;
442443
bool is_starred = false;
444+
// `is_return_value.top()` is true if we are currently inside the return type of the
445+
// signature. Using `@^`/`@$` we can force types to be arg/return types while `@!` pops
446+
// back to the previous state.
447+
std::stack<bool> is_return_value({false});
448+
// The following characters have special meaning in the signature parsing. Literals
449+
// containing these are escaped with `!`.
450+
std::string special_chars("!@%{}-");
443451
for (const auto *pc = text; *pc != '\0'; ++pc) {
444452
const auto c = *pc;
445453

@@ -493,7 +501,57 @@ class cpp_function : public function {
493501
} else {
494502
signature += detail::quote_cpp_type_name(detail::clean_type_id(t->name()));
495503
}
504+
} else if (c == '!' && special_chars.find(*(pc + 1)) != std::string::npos) {
505+
// typing::Literal escapes special characters with !
506+
signature += *++pc;
507+
} else if (c == '@') {
508+
// `@^ ... @!` and `@$ ... @!` are used to force arg/return value type (see
509+
// typing::Callable/detail::arg_descr/detail::return_descr)
510+
if (*(pc + 1) == '^') {
511+
is_return_value.emplace(false);
512+
++pc;
513+
continue;
514+
}
515+
if (*(pc + 1) == '$') {
516+
is_return_value.emplace(true);
517+
++pc;
518+
continue;
519+
}
520+
if (*(pc + 1) == '!') {
521+
is_return_value.pop();
522+
++pc;
523+
continue;
524+
}
525+
// Handle types that differ depending on whether they appear
526+
// in an argument or a return value position (see io_name<text1, text2>).
527+
// For named arguments (py::arg()) with noconvert set, return value type is used.
528+
++pc;
529+
if (!is_return_value.top()
530+
&& !(arg_index < rec->args.size() && !rec->args[arg_index].convert)) {
531+
while (*pc != '\0' && *pc != '@') {
532+
signature += *pc++;
533+
}
534+
if (*pc == '@') {
535+
++pc;
536+
}
537+
while (*pc != '\0' && *pc != '@') {
538+
++pc;
539+
}
540+
} else {
541+
while (*pc != '\0' && *pc != '@') {
542+
++pc;
543+
}
544+
if (*pc == '@') {
545+
++pc;
546+
}
547+
while (*pc != '\0' && *pc != '@') {
548+
signature += *pc++;
549+
}
550+
}
496551
} else {
552+
if (c == '-' && *(pc + 1) == '>') {
553+
is_return_value.emplace(true);
554+
}
497555
signature += c;
498556
}
499557
}

include/pybind11/stl/filesystem.h

+1-3
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,7 @@ struct path_caster {
106106
return true;
107107
}
108108

109-
PYBIND11_TYPE_CASTER(T, const_name("os.PathLike"));
110-
static constexpr auto arg_name = const_name("Union[os.PathLike, str, bytes]");
111-
static constexpr auto return_name = const_name("Path");
109+
PYBIND11_TYPE_CASTER(T, io_name("Union[os.PathLike, str, bytes]", "pathlib.Path"));
112110
};
113111

114112
#endif // PYBIND11_HAS_FILESYSTEM || defined(PYBIND11_HAS_EXPERIMENTAL_FILESYSTEM)

0 commit comments

Comments
 (0)