1111# include < cxxabi.h>
1212#endif
1313
14+ #ifndef NB_INTROSPECT_SKIP_NB_SIG
15+ # define NB_INTROSPECT_SKIP_NB_SIG 1
16+ #endif
17+
1418NAMESPACE_BEGIN (NB_NAMESPACE)
1519NAMESPACE_BEGIN(detail)
1620
1721namespace {
1822
19- /* This file keeps the PEP 3107/362 plumbing self-contained so nb_func only
20- exposes the hooks. We translate nanobind's signature metadata directly
21- into annotations/text signatures without touching nb.__nb_signature__ or
22- cached Python objects to avoid ABI changes.
23-
24- Detailed behavior for overloads and how the three PEP-style introspection
25- outputs are produced:
26-
27- - Goals
28- * Provide `__annotations__`, `__text_signature__` and `__signature__`
29- (inspect.Signature) for nanobind-wrapped callables where possible,
30- based on internal C++ metadata (`func_data::descr`, `func_data::args`,
31- `func_data::descr_types`, etc.).
32- * Avoid touching or replacing the legacy `__nb_signature__` text blob
33- stored in `func_data::signature` (this keeps backward compatibility).
34-
35- - High-level flow
36- 1. `collect_signature_metadata()` iterates overloads and calls
37- `build_signature_metadata()` to produce a `signature_metadata` for each
38- overload. This captures parameter names, kinds (positional-only,
39- positional-or-keyword, keyword-only, *args, **kwargs), sanitized
40- tokens for a compact text signature, and return type text.
41- 2. If `collect_signature_metadata()` fails (returns false), the
42- introspection functions return `Py_None`. A primary reason for
43- failure is when an overload is marked with
44- `func_flags::has_signature` (i.e. the binding author provided an
45- explicit `nb::sig(...)`/legacy signature string). In that case the
46- code intentionally refuses to auto-generate PEP-style metadata so as
47- not to overwrite or conflict with an explicit signature provided by
48- the author.
49- 3. If collection succeeds, the code checks whether all overloads are
50- "compatible" via `signatures_are_compatible()`. Compatibility
51- requires:
52- - same number of parameters across overloads
53- - same parameter names and kinds at every position
54- - identical `sanitized_tokens` sequences (this encodes layout such
55- as `self`, `*`, `**name`, `/`, default value text, etc.)
56-
57- - Result rules per introspection output
58- * `__annotations__` (nb_introspect_annotations)
59- - If `collect_signature_metadata()` fails: returns `Py_None`.
60- - If collection succeeds but there are no metas or overloads are
61- incompatible: returns an empty `dict` (not `None`) to indicate
62- "we have no typed annotations to provide".
63- - If collection succeeds and overloads are compatible: merge
64- parameter annotations and return types across overloads. Merging
65- logic:
66- - Collect all distinct annotation strings for each parameter
67- (only when `has_annotation` is true for that parameter).
68- - If any annotation is `"typing.Any"`, that wins and the merged
69- result for that parameter is `"typing.Any"`.
70- - If there is exactly one distinct annotation, use it.
71- - If multiple distinct concrete annotations are present, emit
72- `"typing.Union[T1, T2, ...]"` as the merged annotation string.
73- - The merged return type is constructed the same way and stored
74- under the `"return"` key.
75-
76- * `__text_signature__` (nb_introspect_text_signature)
77- - If `collect_signature_metadata()` fails: returns `Py_None`.
78- - If collection succeeds but metas are empty or overloads are
79- incompatible: returns `Py_None` (no single compact text signature
80- can be provided).
81- - If collection succeeds and overloads are compatible: return a
82- compact text signature string built from the `sanitized_tokens`
83- of the first meta (compatibility guarantees all metas have the
84- same `sanitized_tokens`). Example: "(self, arg0, /)".
85-
86- * `__signature__` (nb_introspect_signature -> inspect.Signature)
87- - If `collect_signature_metadata()` fails: returns `Py_None`.
88- - If collection yields no metas: returns `Py_None`.
89- - If collection succeeds and overloads are compatible: build and
90- return an `inspect.Signature` object derived from the first
91- `signature_metadata` (parameters include kinds, default values, and
92- annotation strings where present).
93- - If collection succeeds but overloads are *incompatible*: do not
94- return `Py_None`. Instead, construct and return a fallback
95- variadic `Signature` equivalent to `(*args, **kwargs)` so that
96- `inspect.signature()` still returns a usable, permissive signature
97- object.
98-
99- - Why `has_signature` causes collection failure
100- * `func_flags::has_signature` is set when the binding author provided
101- an explicit signature string via `nb::sig(...)`. That string is
102- stored in `func_data::signature` and is used for legacy nb-style
103- signatures and doc rendering (`__nb_signature__`, `nb_func_render_signature`,
104- etc.). If at least one overload has this flag, `build_signature_metadata`
105- returns `false` to signal: "do not auto-generate/merge PEP-style
106- metadata for this callable". In this codebase `Py_None` is used as
107- the signal value to indicate that automatic PEP introspection was
108- intentionally skipped.
109-
110- - Notes and implementation details
111- * `sanitized_tokens` contains tokens used for text signature merging
112- (includes param names, '*'/'**' markers, '/' for positional-only
113- boundary, and default-value text when available). Differences in
114- sanitized tokens across overloads make them incompatible.
115- * Default argument text is included in `sanitized_tokens` (so
116- difference in default values usually prevents merging of text
117- signatures).
118- * Annotations are stored/merged as *strings* (e.g. "typing.Any",
119- "module.Type"), not as evaluated Python objects. This avoids
120- importing/resolving types at C++ side and keeps ABI surface stable.
121- * The implementation purposely avoids mutating or replacing the
122- legacy `__nb_signature__` (stored in `func_data::signature`) so that
123- existing code relying on that string is not affected.
124-
125- Summary:
126- - `collect_signature_metadata()` failure -> all `nb_introspect_*`
127- return `Py_None` (signal: skip auto-generation).
128- - collection success + compatible overloads -> produce merged
129- `__annotations__` (dict), a single `__text_signature__` (str), and a
130- concrete `inspect.Signature`.
131- - collection success + incompatible overloads -> annotations = `{}`;
132- text_signature = `None`; signature = permissive variadic
133- `inspect.Signature`.
134- */
23+ /* PEP 3107/362 helpers:
24+ - NB_INTROSPECT_SKIP_NB_SIG (default 1) controls whether overloads that
25+ used nb::sig skip PEP metadata (1) or are parsed like regular overloads (0).
26+ - Only merge overloads when parameter order, names, kinds, and sanitized
27+ tokens align; otherwise surface empty annotations and None for
28+ __signature__/__text_signature__.
29+ - For compatible overloads, annotations/return types are merged (Union when
30+ multiple concrete values, typing.Any when unspecified) and reused by all
31+ three exported attributes. */
13532
13633static char *dup_string (const char *s) {
13734#if defined(_WIN32)
@@ -408,10 +305,13 @@ static PyObject *inspect_kind_object(signature_param_kind kind) {
408305
409306} // namespace
410307
308+ // Build a single overload's parameter/return description; skip when nb::sig is present.
411309static bool build_signature_metadata (const func_data *f,
412310 signature_metadata &out) noexcept {
311+ #if NB_INTROSPECT_SKIP_NB_SIG
413312 if (f->flags & (uint32_t ) func_flags::has_signature)
414313 return false ;
314+ #endif
415315
416316 const bool is_method = f->flags & (uint32_t ) func_flags::is_method,
417317 has_args = f->flags & (uint32_t ) func_flags::has_args,
@@ -612,8 +512,18 @@ static bool build_signature_metadata(const func_data *f,
612512 return true ;
613513}
614514
615- static bool collect_signature_metadata (nb_func *func, const func_data *f,
616- std::vector<signature_metadata> &out) noexcept {
515+ static bool signatures_are_compatible (const std::vector<signature_metadata> &metas);
516+
517+ enum class metadata_state {
518+ skip,
519+ empty,
520+ incompatible,
521+ compatible
522+ };
523+
524+ // Traverse all overloads, classify compatibility, and short-circuit on skip.
525+ static metadata_state collect_signature_metadata (nb_func *func, const func_data *f,
526+ std::vector<signature_metadata> &out) noexcept {
617527 Py_ssize_t overloads = Py_SIZE ((PyObject *) func);
618528 if (overloads < 1 )
619529 overloads = 1 ;
@@ -624,13 +534,20 @@ static bool collect_signature_metadata(nb_func *func, const func_data *f,
624534 for (Py_ssize_t i = 0 ; i < overloads; ++i) {
625535 signature_metadata meta;
626536 if (!build_signature_metadata (f + i, meta))
627- return false ;
537+ return metadata_state::skip ;
628538 out.emplace_back (std::move (meta));
629539 }
630540
631- return true ;
541+ if (out.empty ())
542+ return metadata_state::empty;
543+
544+ if (!signatures_are_compatible (out))
545+ return metadata_state::incompatible;
546+
547+ return metadata_state::compatible;
632548}
633549
550+ // Overloads are mergeable only when layout and tokens match exactly.
634551static bool signatures_are_compatible (const std::vector<signature_metadata> &metas) {
635552 if (metas.empty ())
636553 return true ;
@@ -765,6 +682,7 @@ static PyObject *build_annotation_dict(const std::vector<signature_metadata> &me
765682 return annotations;
766683}
767684
685+ // Render the compact text signature string from sanitized tokens.
768686static PyObject *build_text_signature_from_meta (const signature_metadata &meta) {
769687 std::string signature = " (" ;
770688 for (size_t i = 0 ; i < meta.sanitized_tokens .size (); ++i) {
@@ -777,6 +695,7 @@ static PyObject *build_text_signature_from_meta(const signature_metadata &meta)
777695 return PyUnicode_FromString (signature.c_str ());
778696}
779697
698+ // Materialize inspect.Signature using cached inspect objects.
780699static PyObject *build_signature_object (const signature_metadata &meta) {
781700 if (!ensure_inspect_cache ())
782701 return nullptr ;
@@ -804,8 +723,18 @@ static PyObject *build_signature_object(const signature_metadata &meta) {
804723 Py_DECREF (param_list);
805724 return nullptr ;
806725 }
807- PyTuple_SET_ITEM (args, 0 , name);
808- PyTuple_SET_ITEM (args, 1 , kind);
726+ if (PyTuple_SetItem (args, 0 , name) != 0 ) {
727+ Py_DECREF (kind);
728+ Py_DECREF (args);
729+ Py_DECREF (param_list);
730+ return nullptr ;
731+ }
732+ if (PyTuple_SetItem (args, 1 , kind) != 0 ) {
733+ Py_DECREF (kind);
734+ Py_DECREF (args);
735+ Py_DECREF (param_list);
736+ return nullptr ;
737+ }
809738
810739 PyObject *kwargs = PyDict_New ();
811740 if (!kwargs) {
@@ -847,7 +776,10 @@ static PyObject *build_signature_object(const signature_metadata &meta) {
847776 Py_DECREF (param_list);
848777 return nullptr ;
849778 }
850- PyList_SET_ITEM (param_list, i, param_obj);
779+ if (PyList_SetItem (param_list, i, param_obj) != 0 ) {
780+ Py_DECREF (param_list);
781+ return nullptr ;
782+ }
851783 }
852784
853785 PyObject *parameters_tuple = PyList_AsTuple (param_list);
@@ -899,75 +831,53 @@ static PyObject *build_signature_object(const signature_metadata &meta) {
899831 return result;
900832}
901833
902- static signature_metadata build_variadic_signature_metadata () {
903- signature_metadata meta;
904-
905- signature_param args;
906- args.name = " args" ;
907- args.annotation .clear ();
908- args.kind = signature_param_kind::var_positional;
909- args.default_value = nullptr ;
910- args.has_annotation = false ;
911- meta.parameters .push_back (std::move (args));
912-
913- signature_param kwargs;
914- kwargs.name = " kwargs" ;
915- kwargs.annotation .clear ();
916- kwargs.kind = signature_param_kind::var_keyword;
917- kwargs.default_value = nullptr ;
918- kwargs.has_annotation = false ;
919- meta.parameters .push_back (std::move (kwargs));
920-
921- return meta;
834+ static PyObject *return_none () noexcept {
835+ Py_INCREF (Py_None);
836+ return Py_None;
922837}
923838
839+ // __annotations__: empty dict on skip/incompat, merged dict otherwise.
924840PyObject *nb_introspect_annotations (nb_func *func, const func_data *f) noexcept {
925841 std::vector<signature_metadata> metas;
926- if (!collect_signature_metadata (func, f, metas)) {
927- Py_INCREF (Py_None);
928- return Py_None;
842+ switch (collect_signature_metadata (func, f, metas)) {
843+ case metadata_state::skip:
844+ case metadata_state::empty:
845+ case metadata_state::incompatible:
846+ return PyDict_New ();
847+ case metadata_state::compatible:
848+ return build_annotation_dict (metas);
929849 }
930-
931- if (metas.empty () || !signatures_are_compatible (metas)) {
932- PyObject *annotations = PyDict_New ();
933- return annotations;
934- }
935-
936- return build_annotation_dict (metas);
850+ return PyDict_New ();
937851}
938852
853+ // __text_signature__: AttributeError on skip/incompat, compact text otherwise.
939854PyObject *nb_introspect_text_signature (nb_func *func, const func_data *f) noexcept {
940855 std::vector<signature_metadata> metas;
941- if (! collect_signature_metadata (func, f, metas)) {
942- Py_INCREF (Py_None);
943- return Py_None;
944- }
945-
946- if (metas. empty () || ! signatures_are_compatible (metas)) {
947- Py_INCREF (Py_None);
948- return Py_None ;
856+ switch ( collect_signature_metadata (func, f, metas)) {
857+ case metadata_state::skip:
858+ case metadata_state::empty:
859+ case metadata_state::incompatible:
860+ PyErr_SetString (PyExc_AttributeError, " __text_signature__ " );
861+ return nullptr ;
862+ case metadata_state::compatible:
863+ return build_text_signature_from_meta (metas. front ()) ;
949864 }
950-
951- return build_text_signature_from_meta (metas.front ());
865+ return return_none ();
952866}
953867
868+ // __signature__: AttributeError on skip/incompat, inspect.Signature otherwise.
954869PyObject *nb_introspect_signature (nb_func *func, const func_data *f) noexcept {
955870 std::vector<signature_metadata> metas;
956- if (! collect_signature_metadata (func, f, metas)) {
957- Py_INCREF (Py_None);
958- return Py_None;
959- }
960-
961- if (metas. empty ()) {
962- Py_INCREF (Py_None);
963- return Py_None ;
871+ switch ( collect_signature_metadata (func, f, metas)) {
872+ case metadata_state::skip:
873+ case metadata_state::empty:
874+ case metadata_state::incompatible:
875+ PyErr_SetString (PyExc_AttributeError, " __signature__ " );
876+ return nullptr ;
877+ case metadata_state::compatible:
878+ return build_signature_object (metas. front ()) ;
964879 }
965-
966- if (signatures_are_compatible (metas))
967- return build_signature_object (metas.front ());
968-
969- signature_metadata fallback = build_variadic_signature_metadata ();
970- return build_signature_object (fallback);
880+ return return_none ();
971881}
972882
973883NAMESPACE_END (detail)
0 commit comments