diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 12a49def29b..dd81eeaa79c 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -263,6 +263,7 @@ impl FnType { &self, cls: Option<&syn::Type>, error_mode: ExtractErrorMode, + self_conversion: SelfConversionPolicy, holders: &mut Holders, ctx: &Ctx, ) -> Option { @@ -272,6 +273,7 @@ impl FnType { Some(st.receiver( cls.expect("no class given for Fn with a \"self\" receiver"), error_mode, + self_conversion, holders, ctx, )) @@ -320,6 +322,41 @@ pub enum SelfType { }, } +/// Receiver conversion policy for extension-type method wrappers. +/// +/// Controls whether the `self` receiver is validated with a runtime type check +/// (`Checked`) or treated as trusted and cast directly without checking +/// (`Trusted`). +/// +/// # Invariant +/// +/// The `Trusted` path is valid due to CPython's slot/method receiver contract: +/// when CPython dispatches a method call on an extension type — whether through +/// a type slot or through `tp_methods` — the receiver is guaranteed to be an +/// instance of the owning type (or a compatible subtype). For `tp_methods` +/// entries, CPython's method-wrapper descriptor enforces this before the C +/// function is reached. +/// +/// `Checked` should be used in cases where that guarantee does not hold: +/// - Standalone `#[pyfunction]`s (no class receiver). +/// - Number-protocol binary operator fragments (`__add__`, `__radd__`, …, +/// `__pow__`, `__rpow__`): CPython combines the forward and reflected +/// fragments into a single `nb_add`/`nb_power` slot, and the runtime helper +/// may call the reflected fragment with the operands swapped, meaning `_slf` +/// can arrive with a non-class type. The existing +/// `ExtractErrorMode::NotImplemented` behaviour on type mismatch is preserved +/// by using `Checked` for those fragments. +#[derive(Clone, Copy, Debug)] +pub enum SelfConversionPolicy { + /// The receiver's type is guaranteed by CPython's slot/method dispatch contract. + /// Used for all extension-type method and slot entrypoints. + Trusted, + /// The receiver's type is verified at runtime. Used for standalone functions + /// and number-protocol binary operator fragments where the CPython dispatch + /// contract does not guarantee the receiver type. + Checked, +} + #[derive(Clone, Copy)] pub enum ExtractErrorMode { NotImplemented, @@ -346,6 +383,7 @@ impl SelfType { &self, cls: &syn::Type, error_mode: ExtractErrorMode, + self_conversion: SelfConversionPolicy, holders: &mut Holders, ctx: &Ctx, ) -> TokenStream { @@ -367,22 +405,47 @@ impl SelfType { }; let arg = quote! { unsafe { #pyo3_path::impl_::extract_argument::#cast_fn(#py, #slf) } }; - let method = if *mutable { - syn::Ident::new("extract_pyclass_ref_mut", *span) - } else { - syn::Ident::new("extract_pyclass_ref", *span) - }; let holder = holders.push_holder(*span); let pyo3_path = pyo3_path.to_tokens_spanned(*span); - error_mode.handle_error( - quote_spanned! { *span => - #pyo3_path::impl_::extract_argument::#method::<#cls>( - #arg, - &mut #holder, + match self_conversion { + SelfConversionPolicy::Trusted => { + let method = if *mutable { + syn::Ident::new("extract_pyclass_ref_mut_trusted", *span) + } else { + syn::Ident::new("extract_pyclass_ref_trusted", *span) + }; + // Use `quote!` (not `quote_spanned!`) for the `unsafe` block so that + // the `unsafe` keyword has `Span::call_site()` and does not inherit the + // user's code span. This prevents triggering `#![forbid(unsafe_code)]` + // in user crates (see the analogous comment in `impl_py_getter_def`). + // Safety: slot wrappers are only installed on the extension type itself. + // CPython's slot dispatch contract ensures the receiver is an instance + // of the correct type before invoking the slot. + let trusted_call = quote! { + unsafe { #pyo3_path::impl_::extract_argument::#method::<#cls>( + #arg, + &mut #holder, + ) } + }; + error_mode.handle_error(trusted_call, ctx) + } + SelfConversionPolicy::Checked => { + let method = if *mutable { + syn::Ident::new("extract_pyclass_ref_mut", *span) + } else { + syn::Ident::new("extract_pyclass_ref", *span) + }; + error_mode.handle_error( + quote_spanned! { *span => + #pyo3_path::impl_::extract_argument::#method::<#cls>( + #arg, + &mut #holder, + ) + }, + ctx, ) - }, - ctx, - ) + } + } } SelfType::TryFromBoundRef { span, non_null } => { let bound_ref = if *non_null { @@ -391,10 +454,32 @@ impl SelfType { quote! { unsafe { #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf) } } }; let pyo3_path = pyo3_path.to_tokens_spanned(*span); + let receiver = match self_conversion { + SelfConversionPolicy::Trusted => { + // Use `quote!` (not `quote_spanned!`) for the inner `unsafe` block so + // that it has `Span::call_site()` and does not trigger + // `#![forbid(unsafe_code)]` in user crates. + // Safety: slot wrappers are only installed on the extension type + // itself. CPython's slot dispatch contract ensures the receiver is + // an instance of the correct type (or a compatible subtype) before + // invoking the slot. + let cast = quote! { + unsafe { #bound_ref.cast_unchecked::<#cls>() } + }; + quote_spanned! { *span => + ::std::result::Result::<_, #pyo3_path::PyErr>::Ok(#cast) + } + } + SelfConversionPolicy::Checked => { + quote_spanned! { *span => + #bound_ref.cast::<#cls>() + .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into) + } + } + }; error_mode.handle_error( quote_spanned! { *span => - #bound_ref.cast::<#cls>() - .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into) + #receiver .and_then( #[allow(clippy::unnecessary_fallible_conversions, reason = "anything implementing `TryFrom` is permitted")] |bound| ::std::convert::TryFrom::try_from(bound).map_err(::std::convert::Into::into) @@ -673,6 +758,7 @@ impl<'a> FnSpec<'a> { ident: &proc_macro2::Ident, cls: Option<&syn::Type>, convention: CallingConvention, + self_conversion: SelfConversionPolicy, ctx: &Ctx, ) -> Result { let Ctx { @@ -695,9 +781,13 @@ impl<'a> FnSpec<'a> { } let rust_call = |args: Vec, mut holders: Holders| { - let self_arg = self - .tp - .self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx); + let self_arg = self.tp.self_arg( + cls, + ExtractErrorMode::Raise, + self_conversion, + &mut holders, + ctx, + ); let init_holders = holders.init_holders(ctx); // We must assign the output_span to the return value of the call, diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 3fa4b9b5317..f0ce82dbf87 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -10,7 +10,7 @@ use crate::{ self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute, FromPyWithAttribute, NameAttribute, TextSignatureAttribute, }, - method::{self, CallingConvention, FnArg}, + method::{self, CallingConvention, FnArg, SelfConversionPolicy}, pymethod::check_generic, }; use proc_macro2::{Span, TokenStream}; @@ -430,7 +430,13 @@ pub fn impl_wrap_pyfunction( ); } let calling_convention = CallingConvention::from_signature(&spec.signature); - let wrapper = spec.get_wrapper_function(&wrapper_ident, None, calling_convention, ctx)?; + let wrapper = spec.get_wrapper_function( + &wrapper_ident, + None, + calling_convention, + SelfConversionPolicy::Checked, + ctx, + )?; let methoddef = spec.get_methoddef( wrapper_ident, spec.get_doc(&func.attrs).as_ref(), diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index d1d8c39e5c3..0b93a32f63e 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -4,7 +4,7 @@ use std::ffi::CString; use crate::attributes::{FromPyWithAttribute, NameAttribute, RenamingRule}; #[cfg(feature = "experimental-inspect")] use crate::introspection::unique_element_id; -use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; +use crate::method::{CallingConvention, ExtractErrorMode, PyArg, SelfConversionPolicy}; use crate::params::{impl_arg_params, impl_regular_arg_param, Holders}; use crate::pyfunction::WarningFactory; use crate::utils::PythonDoc; @@ -374,8 +374,16 @@ pub fn impl_py_method_def( let Ctx { pyo3_path, .. } = ctx; let wrapper_ident = format_ident!("__pymethod_{}__", spec.python_name); let calling_convention = CallingConvention::from_signature(&spec.signature); - let associated_method = - spec.get_wrapper_function(&wrapper_ident, Some(cls), calling_convention, ctx)?; + let associated_method = spec.get_wrapper_function( + &wrapper_ident, + Some(cls), + calling_convention, + // Methods in `tp_methods` are dispatched through CPython's method-wrapper + // descriptor, which enforces that the receiver is an instance of the owning + // type before reaching the C function. The trusted path is therefore valid. + SelfConversionPolicy::Trusted, + ctx, + )?; let methoddef = spec.get_methoddef( quote! { #cls::#wrapper_ident }, doc, @@ -394,8 +402,15 @@ pub fn impl_py_method_def( fn impl_call_slot(cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx) -> Result { let Ctx { pyo3_path, .. } = ctx; let wrapper_ident = syn::Ident::new("__pymethod___call____", Span::call_site()); - let associated_method = - spec.get_wrapper_function(&wrapper_ident, Some(cls), CallingConvention::Varargs, ctx)?; + let associated_method = spec.get_wrapper_function( + &wrapper_ident, + Some(cls), + CallingConvention::Varargs, + // The `tp_call` slot is dispatched by CPython, which guarantees the receiver + // is of the correct type. + SelfConversionPolicy::Trusted, + ctx, + )?; let slot_def = quote! { #pyo3_path::ffi::PyType_Slot { slot: #pyo3_path::ffi::Py_tp_call, @@ -473,7 +488,13 @@ fn impl_clear_slot(cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx) -> syn::Result _ => bail_spanned!(spec.name.span() => "expected instance method for `__clear__` function"), }; let mut holders = Holders::new(); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx); + let slf = self_type.receiver( + cls, + ExtractErrorMode::Raise, + SelfConversionPolicy::Trusted, + &mut holders, + ctx, + ); if let [arg, ..] = args { bail_spanned!(arg.ty().span() => "`__clear__` function expected to have no arguments"); @@ -571,7 +592,13 @@ fn impl_call_setter( ctx: &Ctx, ) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); + let slf = self_type.receiver( + cls, + ExtractErrorMode::Raise, + SelfConversionPolicy::Trusted, + holders, + ctx, + ); if args.is_empty() { bail_spanned!(spec.name.span() => "setter function expected to have one argument"); @@ -611,7 +638,13 @@ pub fn impl_py_setter_def( span: Span::call_site(), non_null: true, } - .receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx); + .receiver( + cls, + ExtractErrorMode::Raise, + SelfConversionPolicy::Trusted, + &mut holders, + ctx, + ); if let Some(ident) = &field.ident { // named struct field quote!({ #slf.#ident = _val; }) @@ -757,7 +790,13 @@ fn impl_call_getter( ctx: &Ctx, ) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); + let slf = self_type.receiver( + cls, + ExtractErrorMode::Raise, + SelfConversionPolicy::Trusted, + holders, + ctx, + ); ensure_spanned!( args.is_empty(), args[0].ty().span() => "getter function can only have one argument (of type pyo3::Python)" @@ -932,7 +971,13 @@ fn impl_call_deleter( ctx: &Ctx, ) -> Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); + let slf = self_type.receiver( + cls, + ExtractErrorMode::Raise, + SelfConversionPolicy::Trusted, + holders, + ctx, + ); if !args.is_empty() { bail_spanned!(spec.name.span() => @@ -1395,6 +1440,9 @@ impl SlotDef { spec, calling_convention, *extract_error_mode, + // All extension-type slots use trusted self: CPython's slot dispatch + // contract guarantees the receiver is of the correct type. + SelfConversionPolicy::Trusted, &mut holders, return_mode.as_ref(), ctx, @@ -1425,11 +1473,16 @@ impl SlotDef { } } +#[allow( + clippy::too_many_arguments, + reason = "slot wrapper generation needs the self-conversion policy flag" +)] fn generate_method_body( cls: &syn::Type, spec: &FnSpec<'_>, calling_convention: &SlotCallingConvention, extract_error_mode: ExtractErrorMode, + self_conversion: SelfConversionPolicy, holders: &mut Holders, // NB ignored if calling_convention is SlotCallingConvention::TpNew, possibly should merge into that enum return_mode: Option<&ReturnMode>, @@ -1441,7 +1494,7 @@ fn generate_method_body( } = ctx; let self_arg = spec .tp - .self_arg(Some(cls), extract_error_mode, holders, ctx); + .self_arg(Some(cls), extract_error_mode, self_conversion, holders, ctx); let rust_name = spec.name; let warnings = spec.warnings.build_py_warning(ctx); @@ -1562,6 +1615,16 @@ struct SlotFragmentDef { arguments: &'static [Ty], extract_error_mode: ExtractErrorMode, ret_ty: Ty, + /// Self-conversion policy for this slot fragment. + /// + /// Most slot fragments are called by CPython with a receiver that is + /// guaranteed to be of the correct type (`Trusted`). However, binary + /// operator fragments are combined into a single slot (e.g. `nb_add`) + /// where the runtime helper may swap operands and call the reflected + /// fragment with a receiver of an unknown (potentially wrong) type. + /// Those fragments must use `Checked` so that a type mismatch returns + /// `NotImplemented` instead of causing undefined behaviour. + self_conversion: SelfConversionPolicy, } impl SlotFragmentDef { @@ -1571,16 +1634,26 @@ impl SlotFragmentDef { arguments, extract_error_mode: ExtractErrorMode::Raise, ret_ty: Ty::Void, + self_conversion: SelfConversionPolicy::Trusted, } } - /// Specialized constructor for binary operators (which are a common pattern) + /// Specialized constructor for binary operators. + /// + /// Binary operator fragments (`__add__`, `__radd__`, etc.) are combined + /// into a shared slot (e.g. `nb_add`) that may call the forward fragment + /// with a non-class receiver (e.g. `1 + MyClass()` → `nb_add(1, c)`). + /// The runtime helper then tries the reflected fragment with the operands + /// swapped, which can also produce a non-class `_slf`. Both cases require + /// a checked type conversion so that a mismatch gracefully returns + /// `NotImplemented` rather than causing undefined behaviour. const fn binary_operator(fragment: &'static str) -> Self { SlotFragmentDef { fragment, arguments: &[Ty::Object], extract_error_mode: ExtractErrorMode::NotImplemented, ret_ty: Ty::Object, + self_conversion: SelfConversionPolicy::Checked, } } @@ -1594,6 +1667,11 @@ impl SlotFragmentDef { self } + const fn checked_self(mut self) -> Self { + self.self_conversion = SelfConversionPolicy::Checked; + self + } + fn generate_pyproto_fragment( &self, cls: &syn::Type, @@ -1606,6 +1684,7 @@ impl SlotFragmentDef { arguments, extract_error_mode, ret_ty, + self_conversion, } = self; let fragment_trait = format_ident!("PyClass{}SlotFragment", fragment); let method = syn::Ident::new(fragment, Span::call_site()); @@ -1621,6 +1700,7 @@ impl SlotFragmentDef { spec, &SlotCallingConvention::FixedArguments(arguments), *extract_error_mode, + *self_conversion, &mut holders, None, ctx, @@ -1703,10 +1783,12 @@ 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) + .checked_self(); const __RPOW__: SlotFragmentDef = SlotFragmentDef::new("__rpow__", &[Ty::Object, Ty::Object]) .extract_error_mode(ExtractErrorMode::NotImplemented) - .ret_ty(Ty::Object); + .ret_ty(Ty::Object) + .checked_self(); const __LT__: SlotFragmentDef = SlotFragmentDef::new("__lt__", &[Ty::Object]) .extract_error_mode(ExtractErrorMode::NotImplemented) diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 6fb9c84057d..d0189a89c69 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -216,6 +216,47 @@ pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( Ok(&mut *holder.insert(PyClassGuardMut::try_borrow_mut_from_borrowed(obj.cast()?)?)) } +/// Trusted variant of [`extract_pyclass_ref`]: performs an unchecked cast for +/// extension-type slot receivers where CPython guarantees the receiver type. +/// +/// This is valid when called from generated slot wrappers installed on a specific +/// extension type, because CPython's slot dispatch contract ensures the receiver +/// is an instance of that type (or a compatible subtype) before invoking the slot. +/// +/// # Safety +/// The caller must ensure that `obj` is an instance of `T`. This invariant is +/// upheld by CPython when dispatching through type slots. +#[inline] +pub unsafe fn extract_pyclass_ref_trusted<'a, 'holder, T: PyClass>( + obj: Borrowed<'a, '_, PyAny>, + holder: &'holder mut Option>, +) -> PyResult<&'holder T> { + // Safety: caller guarantees obj is of type T via CPython slot receiver contract + Ok( + &*holder.insert(PyClassGuard::try_borrow_from_borrowed(unsafe { + obj.cast_unchecked::() + })?), + ) +} + +/// Trusted variant of [`extract_pyclass_ref_mut`]: performs an unchecked cast for +/// extension-type slot receivers where CPython guarantees the receiver type. +/// +/// # Safety +/// Same as [`extract_pyclass_ref_trusted`]. +#[inline] +pub unsafe fn extract_pyclass_ref_mut_trusted<'a, 'holder, T: PyClass>( + obj: Borrowed<'a, '_, PyAny>, + holder: &'holder mut Option>, +) -> PyResult<&'holder mut T> { + // Safety: caller guarantees obj is of type T via CPython slot receiver contract + Ok( + &mut *holder.insert(PyClassGuardMut::try_borrow_mut_from_borrowed(unsafe { + obj.cast_unchecked::() + })?), + ) +} + /// The standard implementation of how PyO3 extracts a `#[pyfunction]` or `#[pymethod]` function argument. pub fn extract_argument<'a, 'holder, 'py, T, const IMPLEMENTS_FROMPYOBJECT: bool>( obj: Borrowed<'a, 'py, PyAny>, diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 9153845a1ea..c0fec231f36 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -44,6 +44,22 @@ fn instance_method() { }); } +/// Test that CPython's method-wrapper descriptor rejects wrong receiver types +/// when `tp_methods` entries are called with a bad `self` from Python. +/// This validates that the trusted self conversion in generated wrappers is safe: +/// even though the Rust code skips a runtime type check, CPython enforces the +/// receiver type before the C function is reached. +#[test] +fn tp_methods_receiver_type_checked_by_cpython() { + Python::attach(|py| { + let cls = py.get_type::(); + // Calling an unbound method with a wrong-type `self` raises TypeError. + // CPython's method-wrapper descriptor enforces the type before our Rust + // wrapper is invoked. + py_expect_exception!(py, cls, "cls.method(object())", PyTypeError); + }); +} + #[pyclass] struct InstanceMethodWithArgs { member: i32, diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index a44025cb45e..1c7a298ae75 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -813,6 +813,18 @@ assert c.counter.count == 4 # __delete__ del c.counter assert c.counter.count == 1 + +# wrong receiver type should be rejected by CPython slot wrapper +for call in ( + lambda: Counter.__get__(object(), Class()), + lambda: Counter.__set__(object(), Class(), Counter()), + lambda: Counter.__delete__(object(), Class()), +): + try: + call() + assert False, "expected TypeError" + except TypeError: + pass "# ); let globals = PyModule::import(py, "__main__").unwrap().dict(); diff --git a/tests/test_trusted_self_conversion_safety.rs b/tests/test_trusted_self_conversion_safety.rs new file mode 100644 index 00000000000..30abfaae452 --- /dev/null +++ b/tests/test_trusted_self_conversion_safety.rs @@ -0,0 +1,343 @@ +#![cfg(feature = "macros")] +//! Safety tests for `SelfConversionPolicy::Trusted`. +//! +//! For every pyo3-generated wrapper that uses `SelfConversionPolicy::Trusted` +//! (i.e. the Rust code skips the `isinstance` check), CPython's own dispatch +//! machinery ensures the receiver is an instance of the correct type *before* +//! our C function is ever called. The tests below verify that invariant: for +//! each slot category, passing a wrong-type receiver from Python raises +//! `TypeError`, proving it is safe to skip the redundant Rust-side check. + +use pyo3::prelude::*; + +mod test_utils; + +// --------------------------------------------------------------------------- +// tp_str / tp_repr / tp_hash +// --------------------------------------------------------------------------- + +#[pyclass] +struct FormatAndHash; + +#[pymethods] +impl FormatAndHash { + #[new] + fn new() -> Self { + FormatAndHash + } + fn __repr__(&self) -> &'static str { + "FormatAndHash()" + } + fn __str__(&self) -> &'static str { + "FormatAndHash" + } + fn __hash__(&self) -> isize { + 42 + } +} + +#[test] +fn unary_format_slots_reject_wrong_receiver() { + Python::attach(|py| { + let cls = py.get_type::(); + py_expect_exception!(py, cls, "cls.__repr__(object())", PyTypeError); + py_expect_exception!(py, cls, "cls.__str__(object())", PyTypeError); + py_expect_exception!(py, cls, "cls.__hash__(object())", PyTypeError); + }); +} + +// --------------------------------------------------------------------------- +// tp_richcompare (via per-comparison SlotFragmentDef entries) +// --------------------------------------------------------------------------- + +#[pyclass] +struct RichCmp; + +#[pymethods] +impl RichCmp { + #[new] + fn new() -> Self { + RichCmp + } + fn __lt__(&self, _other: i32) -> bool { + false + } + fn __le__(&self, _other: i32) -> bool { + false + } + fn __eq__(&self, _other: i32) -> bool { + false + } + fn __ne__(&self, _other: i32) -> bool { + true + } + fn __gt__(&self, _other: i32) -> bool { + false + } + fn __ge__(&self, _other: i32) -> bool { + false + } +} + +#[test] +fn richcmp_slots_reject_wrong_receiver() { + Python::attach(|py| { + let cls = py.get_type::(); + py_expect_exception!(py, cls, "cls.__lt__(object(), 0)", PyTypeError); + py_expect_exception!(py, cls, "cls.__le__(object(), 0)", PyTypeError); + py_expect_exception!(py, cls, "cls.__eq__(object(), 0)", PyTypeError); + py_expect_exception!(py, cls, "cls.__ne__(object(), 0)", PyTypeError); + py_expect_exception!(py, cls, "cls.__gt__(object(), 0)", PyTypeError); + py_expect_exception!(py, cls, "cls.__ge__(object(), 0)", PyTypeError); + }); +} + +// --------------------------------------------------------------------------- +// tp_iter / tp_iternext +// --------------------------------------------------------------------------- + +#[pyclass] +struct IterSlots; + +#[pymethods] +impl IterSlots { + #[new] + fn new() -> Self { + IterSlots + } + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + fn __next__(&mut self) -> Option { + None + } +} + +#[test] +fn iter_slots_reject_wrong_receiver() { + Python::attach(|py| { + let cls = py.get_type::(); + py_expect_exception!(py, cls, "cls.__iter__(object())", PyTypeError); + py_expect_exception!(py, cls, "cls.__next__(object())", PyTypeError); + }); +} + +// --------------------------------------------------------------------------- +// mp_length / mp_subscript / mp_ass_subscript / sq_contains +// --------------------------------------------------------------------------- + +#[pyclass] +struct ContainerSlots; + +#[pymethods] +impl ContainerSlots { + #[new] + fn new() -> Self { + ContainerSlots + } + fn __len__(&self) -> usize { + 0 + } + fn __getitem__(&self, _key: i32) -> i32 { + 0 + } + fn __setitem__(&mut self, _key: i32, _val: i32) {} + fn __delitem__(&mut self, _key: i32) {} + fn __contains__(&self, _item: i32) -> bool { + false + } +} + +#[test] +fn container_slots_reject_wrong_receiver() { + Python::attach(|py| { + let cls = py.get_type::(); + py_expect_exception!(py, cls, "cls.__len__(object())", PyTypeError); + py_expect_exception!(py, cls, "cls.__getitem__(object(), 0)", PyTypeError); + py_expect_exception!(py, cls, "cls.__setitem__(object(), 0, 1)", PyTypeError); + py_expect_exception!(py, cls, "cls.__delitem__(object(), 0)", PyTypeError); + py_expect_exception!(py, cls, "cls.__contains__(object(), 0)", PyTypeError); + }); +} + +// --------------------------------------------------------------------------- +// tp_setattro (via __setattr__ / __delattr__ SlotFragmentDef entries) +// --------------------------------------------------------------------------- + +#[pyclass] +struct AttrSlots { + _value: i32, +} + +#[pymethods] +impl AttrSlots { + #[new] + fn new() -> Self { + AttrSlots { _value: 0 } + } + fn __setattr__(&mut self, _attr: &str, _val: i32) {} + fn __delattr__(&mut self, _attr: &str) {} +} + +#[test] +fn setattr_slots_reject_wrong_receiver() { + Python::attach(|py| { + let cls = py.get_type::(); + py_expect_exception!(py, cls, "cls.__setattr__(object(), 'x', 1)", PyTypeError); + py_expect_exception!(py, cls, "cls.__delattr__(object(), 'x')", PyTypeError); + }); +} + +// --------------------------------------------------------------------------- +// tp_call +// --------------------------------------------------------------------------- + +#[pyclass] +struct CallSlot; + +#[pymethods] +impl CallSlot { + #[new] + fn new() -> Self { + CallSlot + } + fn __call__(&self) -> i32 { + 0 + } +} + +#[test] +fn call_slot_rejects_wrong_receiver() { + Python::attach(|py| { + let cls = py.get_type::(); + py_expect_exception!(py, cls, "cls.__call__(object())", PyTypeError); + }); +} + +// --------------------------------------------------------------------------- +// nb_positive / nb_negative / nb_invert / nb_int / nb_float / nb_index / nb_bool +// --------------------------------------------------------------------------- + +#[pyclass] +struct NumericUnary; + +#[pymethods] +impl NumericUnary { + #[new] + fn new() -> Self { + NumericUnary + } + fn __pos__(&self) -> i32 { + 0 + } + fn __neg__(&self) -> i32 { + 0 + } + fn __invert__(&self) -> i32 { + 0 + } + fn __int__(&self) -> i32 { + 0 + } + fn __float__(&self) -> f64 { + 0.0 + } + fn __index__(&self) -> i32 { + 0 + } + fn __bool__(&self) -> bool { + false + } +} + +#[test] +fn numeric_unary_slots_reject_wrong_receiver() { + Python::attach(|py| { + let cls = py.get_type::(); + py_expect_exception!(py, cls, "cls.__pos__(object())", PyTypeError); + py_expect_exception!(py, cls, "cls.__neg__(object())", PyTypeError); + py_expect_exception!(py, cls, "cls.__invert__(object())", PyTypeError); + py_expect_exception!(py, cls, "cls.__int__(object())", PyTypeError); + py_expect_exception!(py, cls, "cls.__float__(object())", PyTypeError); + py_expect_exception!(py, cls, "cls.__index__(object())", PyTypeError); + py_expect_exception!(py, cls, "cls.__bool__(object())", PyTypeError); + }); +} + +// --------------------------------------------------------------------------- +// nb_inplace_add / nb_inplace_subtract / nb_inplace_multiply +// (SlotDef::binary_inplace_operator — Trusted self, return_self) +// --------------------------------------------------------------------------- + +#[pyclass] +struct InplaceOps; + +#[pymethods] +impl InplaceOps { + #[new] + fn new() -> Self { + InplaceOps + } + fn __iadd__(&mut self, _other: i32) {} + fn __isub__(&mut self, _other: i32) {} + fn __imul__(&mut self, _other: i32) {} +} + +#[test] +fn inplace_operator_slots_reject_wrong_receiver() { + Python::attach(|py| { + let cls = py.get_type::(); + py_expect_exception!(py, cls, "cls.__iadd__(object(), 1)", PyTypeError); + py_expect_exception!(py, cls, "cls.__isub__(object(), 1)", PyTypeError); + py_expect_exception!(py, cls, "cls.__imul__(object(), 1)", PyTypeError); + }); +} + +// --------------------------------------------------------------------------- +// tp_getset (getter / setter installed via PyGetSetDef) +// --------------------------------------------------------------------------- + +#[pyclass] +struct GetSetSlots { + value: i32, +} + +#[pymethods] +impl GetSetSlots { + #[new] + fn new() -> Self { + GetSetSlots { value: 0 } + } + // pyo3 strips the `get_`/`set_` prefix to derive the Python attribute name `prop`. + #[getter] + fn get_prop(&self) -> i32 { + self.value + } + #[setter] + fn set_prop(&mut self, v: i32) { + self.value = v; + } +} + +#[test] +fn getset_descriptor_rejects_wrong_receiver() { + Python::attach(|py| { + let cls = py.get_type::(); + // `cls.__dict__['prop']` gives the raw getset_descriptor without going + // through the descriptor protocol. Calling __get__/__set__ on it with a + // wrong-type instance goes through CPython's `descr_check`, which raises + // TypeError before our Rust getter/setter code is ever reached. + py_expect_exception!( + py, + cls, + "cls.__dict__['prop'].__get__(object(), type(object()))", + PyTypeError + ); + py_expect_exception!( + py, + cls, + "cls.__dict__['prop'].__set__(object(), 1)", + PyTypeError + ); + }); +} diff --git a/tests/ui/invalid_frozen_pyclass_borrow.stderr b/tests/ui/invalid_frozen_pyclass_borrow.stderr index 5bfa9ee0808..b30079031ef 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.stderr +++ b/tests/ui/invalid_frozen_pyclass_borrow.stderr @@ -4,24 +4,6 @@ error: cannot use `#[pyo3(set)]` on a `frozen` class 38 | #[pyo3(set)] | ^^^ -error[E0271]: type mismatch resolving `::Frozen == False` - --> tests/ui/invalid_frozen_pyclass_borrow.rs:11:19 - | -11 | fn mut_method(&mut self) {} - | ^ type mismatch resolving `::Frozen == False` - | -note: expected this to be `False` - --> tests/ui/invalid_frozen_pyclass_borrow.rs:3:1 - | - 3 | #[pyclass(frozen)] - | ^^^^^^^^^^^^^^^^^^ -note: required by a bound in `extract_pyclass_ref_mut` - --> src/impl_/extract_argument.rs - | - | pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( - | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` - = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_frozen_pyclass_borrow.rs:9:1 | @@ -33,11 +15,11 @@ note: expected this to be `False` | 3 | #[pyclass(frozen)] | ^^^^^^^^^^^^^^^^^^ -note: required by a bound in `PyClassGuardMut` - --> src/pyclass/guard.rs +note: required by a bound in `extract_pyclass_ref_mut_trusted` + --> src/impl_/extract_argument.rs | - | pub struct PyClassGuardMut<'a, T: PyClass> { - | ^^^^^^^^^^^^^^ required by this bound in `PyClassGuardMut` + | pub unsafe fn extract_pyclass_ref_mut_trusted<'a, 'holder, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut_trusted` = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` diff --git a/tests/ui/invalid_pymethod_enum.stderr b/tests/ui/invalid_pymethod_enum.stderr index 3c574320cb5..e2d7c5a9c84 100644 --- a/tests/ui/invalid_pymethod_enum.stderr +++ b/tests/ui/invalid_pymethod_enum.stderr @@ -1,21 +1,3 @@ -error[E0271]: type mismatch resolving `::Frozen == False` - --> tests/ui/invalid_pymethod_enum.rs:11:24 - | -11 | fn mutate_in_place(&mut self) { - | ^ type mismatch resolving `::Frozen == False` - | -note: expected this to be `False` - --> tests/ui/invalid_pymethod_enum.rs:3:1 - | - 3 | #[pyclass] - | ^^^^^^^^^^ -note: required by a bound in `extract_pyclass_ref_mut` - --> src/impl_/extract_argument.rs - | - | pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( - | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` - = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_pymethod_enum.rs:9:1 | @@ -27,31 +9,13 @@ note: expected this to be `False` | 3 | #[pyclass] | ^^^^^^^^^^ -note: required by a bound in `PyClassGuardMut` - --> src/pyclass/guard.rs +note: required by a bound in `extract_pyclass_ref_mut_trusted` + --> src/impl_/extract_argument.rs | - | pub struct PyClassGuardMut<'a, T: PyClass> { - | ^^^^^^^^^^^^^^ required by this bound in `PyClassGuardMut` + | pub unsafe fn extract_pyclass_ref_mut_trusted<'a, 'holder, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut_trusted` = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0271]: type mismatch resolving `::Frozen == False` - --> tests/ui/invalid_pymethod_enum.rs:27:24 - | -27 | fn mutate_in_place(&mut self) { - | ^ type mismatch resolving `::Frozen == False` - | -note: expected this to be `False` - --> tests/ui/invalid_pymethod_enum.rs:19:1 - | -19 | #[pyclass] - | ^^^^^^^^^^ -note: required by a bound in `extract_pyclass_ref_mut` - --> src/impl_/extract_argument.rs - | - | pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( - | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` - = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_pymethod_enum.rs:25:1 | @@ -63,9 +27,9 @@ note: expected this to be `False` | 19 | #[pyclass] | ^^^^^^^^^^ -note: required by a bound in `PyClassGuardMut` - --> src/pyclass/guard.rs +note: required by a bound in `extract_pyclass_ref_mut_trusted` + --> src/impl_/extract_argument.rs | - | pub struct PyClassGuardMut<'a, T: PyClass> { - | ^^^^^^^^^^^^^^ required by this bound in `PyClassGuardMut` + | pub unsafe fn extract_pyclass_ref_mut_trusted<'a, 'holder, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut_trusted` = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index 0cb4039b186..b7915ee519f 100644 --- a/tests/ui/not_send.stderr +++ b/tests/ui/not_send.stderr @@ -9,9 +9,6 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe = help: within `pyo3::Python<'_>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>` note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` --> $RUST/core/src/marker.rs - | - | pub struct PhantomData; - | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::marker::NotSend` --> src/marker.rs | @@ -19,9 +16,6 @@ note: required because it appears within the type `pyo3::marker::NotSend` | ^^^^^^^ note: required because it appears within the type `PhantomData` --> $RUST/core/src/marker.rs - | - | pub struct PhantomData; - | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::Python<'_>` --> src/marker.rs | diff --git a/tests/ui/not_send2.stderr b/tests/ui/not_send2.stderr index 3d76b5ebc11..678d618d245 100644 --- a/tests/ui/not_send2.stderr +++ b/tests/ui/not_send2.stderr @@ -12,9 +12,6 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe = help: within `pyo3::Bound<'_, PyString>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>` note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` --> $RUST/core/src/marker.rs - | - | pub struct PhantomData; - | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::marker::NotSend` --> src/marker.rs | @@ -22,9 +19,6 @@ note: required because it appears within the type `pyo3::marker::NotSend` | ^^^^^^^ note: required because it appears within the type `PhantomData` --> $RUST/core/src/marker.rs - | - | pub struct PhantomData; - | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::Python<'_>` --> src/marker.rs |