Skip to content
Draft
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
1 change: 1 addition & 0 deletions newsfragments/6363.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions pyo3-macros-backend/src/pyfunction/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use quote::ToTokens;
use syn::{
ext::IdentExt,
parse::{Parse, ParseStream},
parse_quote,
punctuated::Punctuated,
spanned::Spanned,
Expr, Token,
Expand Down Expand Up @@ -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('(');
Expand Down
95 changes: 92 additions & 3 deletions pyo3-macros-backend/src/pymethod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -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());
}
}

Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -1345,6 +1357,7 @@ pub struct SlotDef {
extract_error_mode: ExtractErrorMode,
return_mode: Option<ReturnMode>,
require_unsafe: bool,
optional_trailing_args: usize,
}

enum SlotCallingConvention {
Expand All @@ -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() {
Expand Down Expand Up @@ -1424,6 +1448,7 @@ impl SlotDef {
extract_error_mode: ExtractErrorMode::Raise,
return_mode: None,
require_unsafe: false,
optional_trailing_args: 0,
}
}

Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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 {
Expand All @@ -1703,6 +1731,7 @@ impl SlotFragmentDef {
extract_error_mode: ExtractErrorMode::Raise,
ret_ty: Ty::Void,
self_conversion: SelfConversionPolicy::checked(),
optional_trailing_args: 0,
}
}

Expand All @@ -1722,6 +1751,7 @@ impl SlotFragmentDef {
extract_error_mode: ExtractErrorMode::NotImplemented,
ret_ty: Ty::Object,
self_conversion: SelfConversionPolicy::checked(),
optional_trailing_args: 0,
}
}

Expand All @@ -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,
Expand All @@ -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());
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1970,3 +2016,46 @@ fn doc_to_optional_cstr(doc: Option<&PythonDoc>, ctx: &Ctx) -> Result<TokenStrea
quote!(::core::option::Option::None)
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn only_short_callable_slots_have_optional_trailing_args() {
assert_eq!(__GET__.optional_trailing_args(), 1);
assert_eq!(__POW__.optional_trailing_args(), 1);
assert_eq!(__RPOW__.optional_trailing_args(), 1);
assert_eq!(__ITER__.optional_trailing_args(), 0);
assert_eq!(__LT__.optional_trailing_args(), 0);
assert_eq!(__IADD__.optional_trailing_args(), 0);
}

#[test]
fn optional_trailing_args_defaults_to_zero() {
assert_eq!(
SlotDef::new("Py_tp_iter", "getiterfunc").optional_trailing_args(),
0
);
assert_eq!(
SlotDef::new("Py_tp_iter", "getiterfunc")
.with_optional_trailing_args(2)
.optional_trailing_args(),
2
);
assert_eq!(
SlotFragmentDef::new("__pow__", &[Ty::Object]).optional_trailing_args(),
0
);
assert_eq!(
SlotFragmentDef::new("__pow__", &[Ty::Object])
.with_optional_trailing_args(1)
.optional_trailing_args(),
1
);
assert_eq!(
SlotFragmentDef::binary_operator("__add__").optional_trailing_args(),
0
);
}
}
2 changes: 1 addition & 1 deletion pytests/stubs/pyclasses.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class Number:
def __new__(cls, /, value: int) -> 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: ...
Expand Down