Skip to content

experimental-inspect: give slot-wrapper trailing arguments their None default - #6363

Draft
jonasdedden wants to merge 2 commits into
PyO3:mainfrom
jonasdedden:introspection-slot-arg-defaults
Draft

experimental-inspect: give slot-wrapper trailing arguments their None default#6363
jonasdedden wants to merge 2 commits into
PyO3:mainfrom
jonasdedden:introspection-slot-arg-defaults

Conversation

@jonasdedden

@jonasdedden jonasdedden commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What was wrong

PyMethod::parse marks a protocol method's parameters positional-only but never records that CPython's slot wrappers substitute None for an omitted trailing argument. So __pow__(&self, other, modulo: Option<&Self>) was introspected as def __pow__(self, other: object, modulo: object, /) while the runtime wrapper advertises ($self, value, mod=None, /). Same defect for __get__, whose wrapper advertises ($self, instance, owner=None, /).

The fix

The fact lives on the slot definition, next to the arity it belongs with, rather than in a name match. SlotDef and SlotFragmentDef gain an optional_trailing_args: usize, set through the existing const builder chain:

    const __GET__: SlotDef = SlotDef::new("Py_tp_descr_get", "descrgetfunc")
        // `__get__($self, instance, owner=None, /)`
        .with_optional_trailing_args(1);

PyMethodProtoKind::optional_trailing_args delegates to whichever definition applies, and PyMethod::parse passes the count to FunctionSignature::default_trailing_parameters_to_none(count) beside the existing make_all_parameters_positional_only call. Codegen destructuring sites bind the new field as _, so it stays introspection-only and a future field still forces a decision.

__rpow__ is included because its wrapper carries the same mod=None.

Why a plain count, and why always None

The three affected methods are not an arbitrary selection. Scanning every builtin type for slot wrappers whose __text_signature__ carries a default yields exactly three, and all three default to None:

    __get__    ($self, instance, owner=None, /)
    __pow__    ($self, value, mod=None, /)
    __rpow__   ($self, value, mod=None, /)

A non-None sentinel cannot arise. An omitted argument reaches a slot wrapper as NULL at the C level and the wrapper substitutes None. There is no channel for any other default, so a count is sufficient and not lossy. If Python ever adds a slot wrapper with a different default, the field becomes &'static [Option<&'static str>] and only the three call sites change.

Left out

__ipow__. PyO3 gives it a third parameter, but CPython's IBSLOT entry advertises ($self, value, /) and slot_nb_inplace_power drops the modulo, so a None default there would not match the runtime. Confirmed the IBSLOT shape against list.__iadd__.__text_signature__.

@jonasdedden
jonasdedden marked this pull request as draft August 28, 2026 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant