Skip to content

Commit 3b39386

Browse files
speicialize self conversion for descriptor slots for which CPython does ensure a bad type cannot be passed from Python
1 parent 2ba9cda commit 3b39386

3 files changed

Lines changed: 56 additions & 10 deletions

File tree

pyo3-macros-backend/src/method.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ impl FnType {
263263
&self,
264264
cls: Option<&syn::Type>,
265265
error_mode: ExtractErrorMode,
266+
descriptor_slot_receiver: bool,
266267
holders: &mut Holders,
267268
ctx: &Ctx,
268269
) -> Option<TokenStream> {
@@ -272,6 +273,7 @@ impl FnType {
272273
Some(st.receiver(
273274
cls.expect("no class given for Fn with a \"self\" receiver"),
274275
error_mode,
276+
descriptor_slot_receiver,
275277
holders,
276278
ctx,
277279
))
@@ -346,6 +348,7 @@ impl SelfType {
346348
&self,
347349
cls: &syn::Type,
348350
error_mode: ExtractErrorMode,
351+
descriptor_slot_receiver: bool,
349352
holders: &mut Holders,
350353
ctx: &Ctx,
351354
) -> TokenStream {
@@ -391,10 +394,27 @@ impl SelfType {
391394
quote! { unsafe { #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf) } }
392395
};
393396
let pyo3_path = pyo3_path.to_tokens_spanned(*span);
394-
error_mode.handle_error(
397+
let receiver = if descriptor_slot_receiver {
398+
quote_spanned! { *span =>
399+
// Safety: descriptor slot wrappers are only installed on the descriptor
400+
// type itself. CPython calls those slots with `self` set to the
401+
// descriptor object found during lookup, and explicit Python calls to
402+
// `__get__`, `__set__`, and `__delete__` first pass through CPython's
403+
// slot wrapper, which rejects receivers of the wrong type before
404+
// reaching this generated wrapper.
405+
::std::result::Result::<_, #pyo3_path::PyErr>::Ok(unsafe {
406+
#bound_ref.cast_unchecked::<#cls>()
407+
})
408+
}
409+
} else {
395410
quote_spanned! { *span =>
396411
#bound_ref.cast::<#cls>()
397412
.map_err(::std::convert::Into::<#pyo3_path::PyErr>::into)
413+
}
414+
};
415+
error_mode.handle_error(
416+
quote_spanned! { *span =>
417+
#receiver
398418
.and_then(
399419
#[allow(clippy::unnecessary_fallible_conversions, reason = "anything implementing `TryFrom<BoundRef>` is permitted")]
400420
|bound| ::std::convert::TryFrom::try_from(bound).map_err(::std::convert::Into::into)
@@ -697,7 +717,7 @@ impl<'a> FnSpec<'a> {
697717
let rust_call = |args: Vec<TokenStream>, mut holders: Holders| {
698718
let self_arg = self
699719
.tp
700-
.self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx);
720+
.self_arg(cls, ExtractErrorMode::Raise, false, &mut holders, ctx);
701721
let init_holders = holders.init_holders(ctx);
702722

703723
// We must assign the output_span to the return value of the call,

pyo3-macros-backend/src/pymethod.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ fn impl_clear_slot(cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx) -> syn::Result
473473
_ => bail_spanned!(spec.name.span() => "expected instance method for `__clear__` function"),
474474
};
475475
let mut holders = Holders::new();
476-
let slf = self_type.receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx);
476+
let slf = self_type.receiver(cls, ExtractErrorMode::Raise, false, &mut holders, ctx);
477477

478478
if let [arg, ..] = args {
479479
bail_spanned!(arg.ty().span() => "`__clear__` function expected to have no arguments");
@@ -571,7 +571,7 @@ fn impl_call_setter(
571571
ctx: &Ctx,
572572
) -> syn::Result<TokenStream> {
573573
let (py_arg, args) = split_off_python_arg(&spec.signature.arguments);
574-
let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx);
574+
let slf = self_type.receiver(cls, ExtractErrorMode::Raise, false, holders, ctx);
575575

576576
if args.is_empty() {
577577
bail_spanned!(spec.name.span() => "setter function expected to have one argument");
@@ -611,7 +611,7 @@ pub fn impl_py_setter_def(
611611
span: Span::call_site(),
612612
non_null: true,
613613
}
614-
.receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx);
614+
.receiver(cls, ExtractErrorMode::Raise, false, &mut holders, ctx);
615615
if let Some(ident) = &field.ident {
616616
// named struct field
617617
quote!({ #slf.#ident = _val; })
@@ -757,7 +757,7 @@ fn impl_call_getter(
757757
ctx: &Ctx,
758758
) -> syn::Result<TokenStream> {
759759
let (py_arg, args) = split_off_python_arg(&spec.signature.arguments);
760-
let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx);
760+
let slf = self_type.receiver(cls, ExtractErrorMode::Raise, false, holders, ctx);
761761
ensure_spanned!(
762762
args.is_empty(),
763763
args[0].ty().span() => "getter function can only have one argument (of type pyo3::Python)"
@@ -932,7 +932,7 @@ fn impl_call_deleter(
932932
ctx: &Ctx,
933933
) -> Result<TokenStream> {
934934
let (py_arg, args) = split_off_python_arg(&spec.signature.arguments);
935-
let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx);
935+
let slf = self_type.receiver(cls, ExtractErrorMode::Raise, false, holders, ctx);
936936

937937
if !args.is_empty() {
938938
bail_spanned!(spec.name.span() =>
@@ -1395,6 +1395,10 @@ impl SlotDef {
13951395
spec,
13961396
calling_convention,
13971397
*extract_error_mode,
1398+
matches!(
1399+
calling_convention,
1400+
SlotCallingConvention::FixedArguments(&[Ty::MaybeNullObject, Ty::MaybeNullObject])
1401+
),
13981402
&mut holders,
13991403
return_mode.as_ref(),
14001404
ctx,
@@ -1425,11 +1429,16 @@ impl SlotDef {
14251429
}
14261430
}
14271431

1432+
#[allow(
1433+
clippy::too_many_arguments,
1434+
reason = "slot wrapper generation needs the descriptor fast-path flag"
1435+
)]
14281436
fn generate_method_body(
14291437
cls: &syn::Type,
14301438
spec: &FnSpec<'_>,
14311439
calling_convention: &SlotCallingConvention,
14321440
extract_error_mode: ExtractErrorMode,
1441+
descriptor_slot_receiver: bool,
14331442
holders: &mut Holders,
14341443
// NB ignored if calling_convention is SlotCallingConvention::TpNew, possibly should merge into that enum
14351444
return_mode: Option<&ReturnMode>,
@@ -1439,9 +1448,13 @@ fn generate_method_body(
14391448
pyo3_path,
14401449
output_span,
14411450
} = ctx;
1442-
let self_arg = spec
1443-
.tp
1444-
.self_arg(Some(cls), extract_error_mode, holders, ctx);
1451+
let self_arg = spec.tp.self_arg(
1452+
Some(cls),
1453+
extract_error_mode,
1454+
descriptor_slot_receiver,
1455+
holders,
1456+
ctx,
1457+
);
14451458
let rust_name = spec.name;
14461459
let warnings = spec.warnings.build_py_warning(ctx);
14471460

@@ -1621,6 +1634,7 @@ impl SlotFragmentDef {
16211634
spec,
16221635
&SlotCallingConvention::FixedArguments(arguments),
16231636
*extract_error_mode,
1637+
matches!(*fragment, "__set__" | "__delete__"),
16241638
&mut holders,
16251639
None,
16261640
ctx,

tests/test_proto_methods.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,18 @@ assert c.counter.count == 4
813813
# __delete__
814814
del c.counter
815815
assert c.counter.count == 1
816+
817+
# wrong receiver type should be rejected by CPython slot wrapper
818+
for call in (
819+
lambda: Counter.__get__(object(), Class()),
820+
lambda: Counter.__set__(object(), Class(), Counter()),
821+
lambda: Counter.__delete__(object(), Class()),
822+
):
823+
try:
824+
call()
825+
assert False, "expected TypeError"
826+
except TypeError:
827+
pass
816828
"#
817829
);
818830
let globals = PyModule::import(py, "__main__").unwrap().dict();

0 commit comments

Comments
 (0)