diff --git a/newsfragments/6363.fixed.md b/newsfragments/6363.fixed.md new file mode 100644 index 00000000000..90818189d2c --- /dev/null +++ b/newsfragments/6363.fixed.md @@ -0,0 +1 @@ +`experimental-inspect`: `__pow__`, `__rpow__` and `__get__` now introspect their trailing argument as defaulting to `None`, matching the CPython slot wrappers which substitute `None` when it is omitted. diff --git a/pyo3-macros-backend/src/pyfunction/signature.rs b/pyo3-macros-backend/src/pyfunction/signature.rs index f8873eebffd..4243f60f658 100644 --- a/pyo3-macros-backend/src/pyfunction/signature.rs +++ b/pyo3-macros-backend/src/pyfunction/signature.rs @@ -10,6 +10,7 @@ use quote::ToTokens; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, + parse_quote, punctuated::Punctuated, spanned::Spanned, Expr, Token, @@ -585,6 +586,26 @@ impl<'a> FunctionSignature<'a> { } } + /// Gives the last `count` positional parameters a `None` default, matching a CPython slot + /// wrapper which substitutes `None` for the trailing arguments the caller may omit. + pub fn default_trailing_parameters_to_none(&mut self, count: usize) { + let mut defaulted = 0; + for arg in self.arguments.iter_mut().rev() { + if defaulted == count { + break; + } + if let FnArg::Regular(arg) = arg { + arg.default_value = Some(Box::new(parse_quote!(None))); + defaulted += 1; + } + } + for _ in 0..defaulted { + self.python_signature + .default_positional_parameters + .push(parse_quote!(None)); + } + } + pub fn text_signature(&self, self_argument: Option<&str>) -> String { let mut output = String::new(); output.push('('); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index dacf1edbb5e..7f3e2f4fec9 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -213,6 +213,14 @@ impl PyMethodProtoKind { | PyMethodProtoKind::Clear => false, } } + + fn optional_trailing_args(&self) -> usize { + match self { + PyMethodProtoKind::Slot(slot) => slot.optional_trailing_args(), + PyMethodProtoKind::SlotFragment(fragment) => fragment.optional_trailing_args(), + PyMethodProtoKind::Call | PyMethodProtoKind::Traverse | PyMethodProtoKind::Clear => 0, + } + } } impl<'a> PyMethod<'a> { @@ -236,6 +244,8 @@ impl<'a> PyMethod<'a> { spec.signature .python_signature .make_all_parameters_positional_only(); + spec.signature + .default_trailing_parameters_to_none(proto.optional_trailing_args()); } } @@ -1096,7 +1106,9 @@ pub const __HASH__: SlotDef = )); pub const __RICHCMP__: SlotDef = SlotDef::new("Py_tp_richcompare", "richcmpfunc") .extract_error_mode(ExtractErrorMode::NotImplemented); -const __GET__: SlotDef = SlotDef::new("Py_tp_descr_get", "descrgetfunc"); +const __GET__: SlotDef = SlotDef::new("Py_tp_descr_get", "descrgetfunc") + // `__get__($self, instance, owner=None, /)` + .with_optional_trailing_args(1); const __ITER__: SlotDef = SlotDef::new("Py_tp_iter", "getiterfunc"); const __NEXT__: SlotDef = SlotDef::new("Py_tp_iternext", "iternextfunc").return_iter_conversion( StaticIdent::new("IterNextOutput"), @@ -1345,6 +1357,7 @@ pub struct SlotDef { extract_error_mode: ExtractErrorMode, return_mode: Option, require_unsafe: bool, + optional_trailing_args: usize, } enum SlotCallingConvention { @@ -1369,6 +1382,17 @@ impl SlotDef { ) } + /// How many trailing arguments CPython's slot wrapper lets the caller omit, each of which + /// reaches the slot as `None`. + pub const fn optional_trailing_args(&self) -> usize { + self.optional_trailing_args + } + + const fn with_optional_trailing_args(mut self, count: usize) -> Self { + self.optional_trailing_args = count; + self + } + const fn new(slot: &'static str, func_ty: &'static str) -> Self { // The FFI function pointer type determines the arguments and return type let (calling_convention, ret_ty) = match func_ty.as_bytes() { @@ -1424,6 +1448,7 @@ impl SlotDef { extract_error_mode: ExtractErrorMode::Raise, return_mode: None, require_unsafe: false, + optional_trailing_args: 0, } } @@ -1475,6 +1500,8 @@ impl SlotDef { ret_ty, return_mode, require_unsafe, + // introspection only, not part of codegen + optional_trailing_args: _, } = self; if *require_unsafe { ensure_spanned!( @@ -1693,6 +1720,7 @@ struct SlotFragmentDef { /// Those fragments must use `Checked` so that a type mismatch returns /// `NotImplemented` instead of causing undefined behaviour. self_conversion: SelfConversionPolicy, + optional_trailing_args: usize, } impl SlotFragmentDef { @@ -1703,6 +1731,7 @@ impl SlotFragmentDef { extract_error_mode: ExtractErrorMode::Raise, ret_ty: Ty::Void, self_conversion: SelfConversionPolicy::checked(), + optional_trailing_args: 0, } } @@ -1722,6 +1751,7 @@ impl SlotFragmentDef { extract_error_mode: ExtractErrorMode::NotImplemented, ret_ty: Ty::Object, self_conversion: SelfConversionPolicy::checked(), + optional_trailing_args: 0, } } @@ -1740,6 +1770,16 @@ impl SlotFragmentDef { self } + /// See [`SlotDef::optional_trailing_args`]. + const fn optional_trailing_args(&self) -> usize { + self.optional_trailing_args + } + + const fn with_optional_trailing_args(mut self, count: usize) -> Self { + self.optional_trailing_args = count; + self + } + fn generate_pyproto_fragment( &self, cls: &syn::Type, @@ -1753,6 +1793,8 @@ impl SlotFragmentDef { extract_error_mode, ret_ty, self_conversion, + // introspection only, not part of codegen + optional_trailing_args: _, } = self; let fragment_trait = format_ident!("PyClass{}SlotFragment", fragment); let method = syn::Ident::new(fragment, Span::call_site()); @@ -1867,10 +1909,14 @@ const __ROR__: SlotFragmentDef = SlotFragmentDef::binary_operator("__ror__"); const __POW__: SlotFragmentDef = SlotFragmentDef::new("__pow__", &[Ty::Object, Ty::Object]) .extract_error_mode(ExtractErrorMode::NotImplemented) - .ret_ty(Ty::Object); + .ret_ty(Ty::Object) + // `__pow__($self, value, mod=None, /)` + .with_optional_trailing_args(1); const __RPOW__: SlotFragmentDef = SlotFragmentDef::new("__rpow__", &[Ty::Object, Ty::Object]) .extract_error_mode(ExtractErrorMode::NotImplemented) - .ret_ty(Ty::Object); + .ret_ty(Ty::Object) + // `__rpow__($self, value, mod=None, /)` + .with_optional_trailing_args(1); const __LT__: SlotFragmentDef = SlotFragmentDef::new("__lt__", &[Ty::Object]) .extract_error_mode(ExtractErrorMode::NotImplemented) @@ -1970,3 +2016,46 @@ fn doc_to_optional_cstr(doc: Option<&PythonDoc>, ctx: &Ctx) -> Result Number: ... def __or__(self, other: object, /) -> Number: ... def __pos__(self, /) -> Number: ... - def __pow__(self, other: object, modulo: object, /) -> Number: ... + def __pow__(self, other: object, modulo: object = None, /) -> Number: ... def __repr__(self, /) -> str: ... def __rshift__(self, other: object, /) -> Number: ... def __str__(self, /) -> str: ...