Skip to content

Implement trusted self conversion for all extension-type method and slot wrappers#1

Merged
MatthieuDartiailh merged 6 commits intodescriptorfrom
copilot/expansion-checklist-trusted-self-conversion
Apr 7, 2026
Merged

Implement trusted self conversion for all extension-type method and slot wrappers#1
MatthieuDartiailh merged 6 commits intodescriptorfrom
copilot/expansion-checklist-trusted-self-conversion

Conversation

Copy link
Copy Markdown

Copilot AI commented Apr 6, 2026

Thank you for contributing to PyO3!

By submitting these contributions you agree for them to be dual-licensed under PyO3's MIT OR Apache-2.0 license.

Extends the trusted (unchecked) self conversion originally introduced for descriptor slots (__get__/__set__/__delete__) to all extension-type method and slot entrypoints, replacing the ad-hoc descriptor_slot_receiver: bool flag with a proper SelfConversionPolicy enum.

Core plumbing (src/impl_/extract_argument.rs)

  • Added extract_pyclass_ref_trusted / extract_pyclass_ref_mut_trusted: unsafe variants that skip the runtime isinstance check via cast_unchecked, valid when CPython's dispatch contract guarantees the receiver type.

Macro backend (pyo3-macros-backend/src/method.rs)

  • Introduced SelfConversionPolicy { Trusted, Checked } enum replacing descriptor_slot_receiver: bool.
  • Added trusted path for SelfType::Receiver (&self, &mut self) in addition to the existing TryFromBoundRef path.
  • get_wrapper_function now takes SelfConversionPolicy explicitly.
  • Updated SelfConversionPolicy documentation to correctly describe that CPython enforces the receiver type for both slot dispatch and tp_methods entries before the C function is reached, and that Checked is specifically needed for number-protocol binary operator fragments (__add__, __radd__, …, __pow__, __rpow__) where CPython's combined slot implementation may swap operands.
  • Lint hygiene: unsafe {} blocks in generated trusted-path code use quote! (not quote_spanned!), keeping Span::call_site() so they don't trigger #![forbid(unsafe_code)] in user crates.

Slot and method wrapper rollout (pyo3-macros-backend/src/pymethod.rs)

  • Added self_conversion: SelfConversionPolicy field to SlotFragmentDef; defaults to Trusted.
  • Binary operator fragments (__add__, __radd__, …, __pow__, __rpow__) remain Checked: CPython's combined nb_add/nb_power slot implementation swaps operands before calling the reflected fragment, so _slf can legally arrive with a non-class type. The existing ExtractErrorMode::NotImplemented behaviour on type mismatch is preserved.
  • All other SlotFragmentDef (richcmp, __set__, __delete__, __setitem__, etc.) and all SlotDef fixed-argument slots use Trusted.
  • tp_call, tp_clear, getter/setter/deleter wrappers → Trusted.
  • tp_methods entries → Trusted: CPython's method-wrapper descriptor enforces the correct receiver type before the C function is reached (verified by descr_check in CPython's descriptor machinery), so the trusted path is valid for regular instance methods too.
  • Checked is now reserved exclusively for standalone #[pyfunction]s (no class receiver) and number-protocol binary operator fragments where CPython does not guarantee the receiver type.

Testing

  • Added tp_methods_receiver_type_checked_by_cpython test in tests/test_methods.rs demonstrating that calling an unbound tp_methods method with a wrong-type receiver from Python raises TypeError.
  • Added tests/test_trusted_self_conversion_safety.rs with comprehensive coverage of all Trusted slot categories, verifying CPython raises TypeError before reaching the Rust wrapper for each:
    • tp_str, tp_repr, tp_hash (unary format/hash slots)
    • tp_richcompare via fragments __lt__, __le__, __eq__, __ne__, __gt__, __ge__
    • tp_iter, tp_iternext
    • mp_length, mp_subscript, mp_ass_subscript, sq_contains
    • tp_setattro via __setattr__, __delattr__ fragments
    • tp_call
    • nb_positive, nb_negative, nb_invert, nb_int, nb_float, nb_index, nb_bool
    • nb_inplace_add, nb_inplace_subtract, nb_inplace_multiply
    • tp_getset getter/setter (via getset_descriptor / descr_check)

UI test snapshots

  • Updated tests/ui/invalid_frozen_pyclass_borrow.stderr and tests/ui/invalid_pymethod_enum.stderr to reflect that frozen-class &mut self errors now point to extract_pyclass_ref_mut_trusted instead of the former extract_pyclass_ref_mut/PyClassGuardMut.
  • Updated tests/ui/not_send.stderr and tests/ui/not_send2.stderr to match current stable compiler output (dropped redundant PhantomData struct-declaration note lines).

Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/0085858a-ae43-4524-a2fb-f0759a630ba0

Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement trusted self conversion for extension-type methods Implement trusted self conversion for all extension-type slot method wrappers Apr 6, 2026
Copilot AI requested a review from MatthieuDartiailh April 6, 2026 16:47
@MatthieuDartiailh
Copy link
Copy Markdown
Owner

MatthieuDartiailh commented Apr 6, 2026

@copilot Can you point to the reference from which you concluded that:
Regular tp_methods entries remain Checked — Python allows calling them as MyClass.method(wrong_type)

Copy link
Copy Markdown
Owner

@MatthieuDartiailh MatthieuDartiailh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach for methods defined in tp_methods is wrong and need to be changed. It is unclear why tests that have been edited needed to be edited and additional tests demonstrating the changes are safe should be added (passing bad types from Python is caught even though Rust does not perform a check).
Please address @copilot

Comment thread pyo3-macros-backend/src/method.rs Outdated
Comment on lines +347 to +348
/// The receiver's type is verified at runtime. Used when the receiver cannot
/// be assumed to be of the correct type (e.g., regular Python-callable methods).
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked on a C++ project. For methods defined in tp_methods, CPython does enforce the proper type is used when the method is called from Python. As a consequence, methods listed in tp_methods can use Trusted.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — I was mistaken. CPython's method-wrapper descriptor (used for tp_methods entries) enforces that the receiver is an instance of the correct type before the C function is reached. Updated impl_py_method_def to use Trusted and corrected the SelfConversionPolicy documentation in commit 733b529.

Comment thread pyo3-macros-backend/src/pymethod.rs Outdated
Comment on lines +381 to +384
// Regular `tp_methods` entries can be called from Python with an arbitrary
// receiver (e.g. `MyClass.method(wrong_type)`), so the receiver type must
// be checked at runtime.
SelfConversionPolicy::Checked,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true. Here we should use Trusted.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 733b529impl_py_method_def now uses SelfConversionPolicy::Trusted.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did those tests were touched ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were accidentally modified when I ran TRYBUILD=overwrite to investigate a test failure. They're unrelated to the actual code changes. Reverted in commit 733b529.

Comment thread tests/ui/invalid_pyclass_args.stderr Outdated
Comment on lines +459 to +434
| ^^^ the trait `pyo3::PyClass` is not implemented for `Box<dyn std::error::Error + Send + Sync>`
| ^^^ the trait `PyClass` is not implemented for `Box<dyn std::error::Error + Send + Sync>`
|
= note: implement `FromPyObject` to enable using `Box<dyn std::error::Error + Send + Sync>` as a function argument
= note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]`
= help: the following other types implement trait `pyo3::PyClass`:
= help: the following other types implement trait `PyClass`:
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did those test change ?

Comment thread tests/ui/not_send.stderr
Comment on lines -12 to -24
|
| pub struct PhantomData<T: PointeeSized>;
| ^^^^^^^^^^^
note: required because it appears within the type `pyo3::marker::NotSend`
--> src/marker.rs
|
| struct NotSend(PhantomData<*mut Python<'static>>);
| ^^^^^^^
note: required because it appears within the type `PhantomData<pyo3::marker::NotSend>`
--> $RUST/core/src/marker.rs
|
| pub struct PhantomData<T: PointeeSized>;
| ^^^^^^^^^^^
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did those tests change ?

Comment thread tests/ui/not_send2.stderr
Comment on lines -15 to 22
|
| pub struct PhantomData<T: PointeeSized>;
| ^^^^^^^^^^^
note: required because it appears within the type `pyo3::marker::NotSend`
--> src/marker.rs
|
| struct NotSend(PhantomData<*mut Python<'static>>);
| ^^^^^^^
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did those tests change ?

Comment thread tests/ui/not_send2.stderr
Comment on lines -15 to 22
|
| pub struct PhantomData<T: PointeeSized>;
| ^^^^^^^^^^^
note: required because it appears within the type `pyo3::marker::NotSend`
--> src/marker.rs
|
| struct NotSend(PhantomData<*mut Python<'static>>);
| ^^^^^^^
note: required because it appears within the type `PhantomData<pyo3::marker::NotSend>`
--> $RUST/core/src/marker.rs
|
| pub struct PhantomData<T: PointeeSized>;
| ^^^^^^^^^^^
note: required because it appears within the type `pyo3::Python<'_>`
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did those change ?

Comment on lines -195 to 196
error[E0277]: `Box<dyn std::error::Error + Send + Sync>` cannot be used as a Python function argument
--> tests/ui/invalid_pyclass_args.rs:205:12
|
205 | field: Box<dyn std::error::Error + Send + Sync>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyFunctionArgument<'_, '_, '_, false>` is not implemented for `Box<dyn std::error::Error + Send + Sync>`
|
= note: implement `FromPyObject` to enable using `Box<dyn std::error::Error + Send + Sync>` as a function argument
= note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]`
help: the following other types implement trait `PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>`
--> src/impl_/extract_argument.rs
|
| / impl<'a, 'holder, 'py, T: 'a + 'py> PyFunctionArgument<'a, 'holder, 'py, false>
| | for &'holder Bound<'py, T>
| | where
| | T: PyTypeCheck,
| |___________________^ `&'holder pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, 'holder, 'py, false>`
...
| / impl<'a, 'holder, 'py, T> PyFunctionArgument<'a, 'holder, 'py, false> for Option<T>
| | where
| | T: PyFunctionArgument<'a, 'holder, 'py, false>,
| |___________________________________________________^ `Option<T>` implements `PyFunctionArgument<'a, 'holder, 'py, false>`
...
| impl<'a, 'holder, T: PyClass> PyFunctionArgument<'a, 'holder, '_, false> for &'holder T {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `&'holder T` implements `PyFunctionArgument<'a, 'holder, '_, false>`
...
| / impl<'a, 'holder, T: PyClass<Frozen = False>> PyFunctionArgument<'a, 'holder, '_, false>
| | for &'holder mut T
| |______________________^ `&'holder mut T` implements `PyFunctionArgument<'a, 'holder, '_, false>`

error[E0592]: duplicate definitions with name `__pymethod___richcmp____`
--> tests/ui/invalid_pyclass_args.rs:37:1
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did those change ?

…derr files; add test

Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/2bbf1372-87cb-4ff2-9dc6-100ee046f83f

Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Apr 7, 2026

The approach for methods defined in tp_methods is wrong and need to be changed. It is unclear why tests that have been edited needed to be edited and additional tests demonstrating the changes are sa...

Fixed in commit 733b529:

  • impl_py_method_def now uses SelfConversionPolicy::Trusted (CPython's method-wrapper descriptor enforces the receiver type before the C function is reached)
  • Unrelated .stderr snapshot files have been reverted
  • Added tp_methods_receiver_type_checked_by_cpython test in tests/test_methods.rs that calls an unbound tp_methods method with a wrong-type receiver from Python and asserts TypeError is raised — demonstrating CPython's enforcement before our Rust wrapper is invoked

@copilot Can you point to the reference from which you concluded that:
Regular tp_methods entries remain Checked — Python allows calling them as MyClass.method(wrong_type)

That claim was incorrect — I was assuming unbound method calls could bypass the type check, but CPython's method-wrapper descriptor enforces the correct receiver type before reaching the C function regardless of how the method is called from Python. The comment and code have been updated in commit 733b529 to correctly use Trusted for tp_methods entries.

Copilot AI changed the title Implement trusted self conversion for all extension-type slot method wrappers Implement trusted self conversion for all extension-type method and slot wrappers Apr 7, 2026
Copilot AI requested a review from MatthieuDartiailh April 7, 2026 08:16
Copy link
Copy Markdown
Owner

@MatthieuDartiailh MatthieuDartiailh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address review comments.

Comment thread pyo3-macros-backend/src/method.rs Outdated
Comment on lines +340 to +342
/// `Checked` should be used for standalone `#[pyfunction]`s and other contexts
/// where no class receiver contract exists (e.g., free functions passed as
/// arguments to `tp_methods` when `cls` is `None`).
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked also apply to number protocols methods (__add__) this should be mentionned.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the SelfConversionPolicy doc in commit dfa09ca to explicitly list number-protocol binary operator fragments (__add__, __radd__, …, __pow__, __rpow__) as the case where Checked is required.

Comment thread tests/test_methods.rs
Comment on lines +47 to +67
/// 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::<InstanceMethod>();
// 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
);
});
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such tests should exist for all slots that used Trusted conversion method to ensure we did not miss a corner case.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests/test_trusted_self_conversion_safety.rs in commit dfa09ca with 9 test functions covering all Trusted slot categories: tp_str/tp_repr/tp_hash, richcmp fragments, tp_iter/tp_iternext, container slots (__len__, __getitem__, __setitem__, __delitem__, __contains__), tp_setattro fragments, tp_call, all numeric unary slots (__pos__, __neg__, __invert__, __int__, __float__, __index__, __bool__), inplace operator slots (__iadd__, __isub__, __imul__), and tp_getset getter/setter. Each test verifies that passing a wrong-type receiver from Python raises TypeError before our Rust wrapper is invoked.

…rehensive Trusted slot safety tests

Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/d914329e-af68-4d7b-9602-e8cd9f359681

Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Apr 7, 2026

@copilot address review comments.

Addressed in commit dfa09ca:

  • Updated SelfConversionPolicy::Checked documentation to explicitly mention number-protocol binary operator fragments as the use case
  • Added tests/test_trusted_self_conversion_safety.rs with 9 test functions covering all Trusted slot categories

Copilot AI requested a review from MatthieuDartiailh April 7, 2026 08:55
@MatthieuDartiailh
Copy link
Copy Markdown
Owner

@copilot fix rust formatting issue

Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/5ae8bd2c-e6ce-45f8-8d83-49ddc8b4c84c

Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Apr 7, 2026

@copilot fix rust formatting issue

Fixed in commit f3b177a — ran cargo fmt to resolve the line-length formatting issues in method.rs and pyfunction.rs.

Copilot AI requested a review from MatthieuDartiailh April 7, 2026 09:13
@MatthieuDartiailh MatthieuDartiailh marked this pull request as ready for review April 7, 2026 09:17
@MatthieuDartiailh
Copy link
Copy Markdown
Owner

@copilot please bless the UI tests so that the CI can make progress.

Agent-Logs-Url: https://github.com/MatthieuDartiailh/pyo3/sessions/55109f72-332d-44e0-8dda-d02f8e11fbed

Co-authored-by: MatthieuDartiailh <4481740+MatthieuDartiailh@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Apr 7, 2026

@copilot please bless the UI tests so that the CI can make progress.

Done in commit d1b38a7. Updated four snapshot files:

  • invalid_frozen_pyclass_borrow.stderr and invalid_pymethod_enum.stderr: now reference extract_pyclass_ref_mut_trusted (instead of the old extract_pyclass_ref_mut/PyClassGuardMut) as a result of the trusted conversion changes.
  • not_send.stderr and not_send2.stderr: dropped PhantomData struct declaration note lines that the current stable compiler no longer emits.

@MatthieuDartiailh MatthieuDartiailh merged commit 732cf48 into descriptor Apr 7, 2026
29 of 43 checks passed
@MatthieuDartiailh MatthieuDartiailh deleted the copilot/expansion-checklist-trusted-self-conversion branch April 7, 2026 10:03
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.

2 participants