Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix AsRef and Deref impls on Bound<T> #3879

Merged
merged 5 commits into from
Feb 22, 2024
Merged

Conversation

Icxolu
Copy link
Contributor

@Icxolu Icxolu commented Feb 20, 2024

Part of #3684, following the discussion thread starting here: #3867 (comment)

Previously the AsRef and Deref implementations of Bound<T> used T: AsRef<PyAny> as generic bound. This was problematic for two reasons:

  • the bound was actually not used, but just happens to be true for the natives python types that we want these impls to work on,
  • the bound was not satisfied on custom #[pyclass] types, which we would like these impls to also work on as well.

This PR introduces a new marker trait (currently) called HasPyBaseType, which is implemented

  • for all native python types except PyAny though pyobject_native_type_named, which conveniently had exactly this purpose,
  • for all #[pyclass] types, as part of the macro expansion.

The AsRef and Deref bounds where modified to the new marker trait, which allows them to work for#[pyclass] types again.

Questions:

  • Is this how we would like these to be implemented?
  • Should we keep the HasPyBaseType name?
  • Should the trait be public or #[doc(hidden)]

The second commit cleans up some .as_any() calls that needed to be introduced by the missing Deref impl.

@Icxolu Icxolu added the CI-skip-changelog Skip checking changelog entry label Feb 20, 2024
src/types/mod.rs Outdated
/// This marks types that can be upcast into a [`PyAny`] and used in its place.
/// This essentially includes every Python object except [`PyAny`] itself.
///
/// This is used to provide the [`AsRef<Bound<'_, PyAny>`](std::convert::AsRef)
Copy link
Member

Choose a reason for hiding this comment

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

The two inline code sections for AsRef and Deref miss a closing >.

src/types/mod.rs Outdated Show resolved Hide resolved
src/instance.rs Outdated
@@ -380,7 +380,7 @@ where

impl<'py, T> AsRef<Bound<'py, PyAny>> for Bound<'py, T>
where
T: AsRef<PyAny>,
T: HasPyBaseType,
Copy link
Member

Choose a reason for hiding this comment

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

One more fundamental question: Do we need a bound here at all? If Bound::as_any can be called without this as soon as we have an instance of Bound<T> at hand, what does the additional bound here achieve?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe if we don't put a bound on it we'll get infinite recursion while auto-dereferencing, because it would be also available on Bound<PyAny>.

Copy link
Member

Choose a reason for hiding this comment

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

But auto-derefercing happens only as often as necessary until a resolution can be found, so why would it dereference again after having reached Bound<PyAny> as any method that can be will have been found? And a non-existing method will not exist, but I expect the compiler to have safe guards against infinite recursion, either a hard limit or loop detection.

Or does this not compile at all? Did you try just dropping the bound?

Copy link
Member

Choose a reason for hiding this comment

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

error[E0055]: reached the recursion limit while auto-dereferencing `instance::Bound<'_, types::any::PyAny>`
  --> src/types/typeobject.rs:72:14
   |
72 |             .to_owned()
   |              ^^^^^^^^ deref recursion limit reached
   |
   = help: consider increasing the recursion limit by adding a `#![recursion_limit = "256"]` attribute to your crate (`pyo3`)

But auto-derefercing happens only as often as necessary until a resolution can be found, so why would it dereference again after having reached Bound as any method that can be will have been found?

So this appears to be wrong.

And a non-existing method will not exist, but I expect the compiler to have safe guards against infinite recursion, either a hard limit or loop detection.

And this appears to be a hard limit defaulting to 256 steps.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess this hits this one: rust-lang/rust#19509

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So this appears to be required for Deref. I would suggest dropping it for AsRef tough and making a code comment for the Deref impl.

That sounds good to me, I'll update that.

As for the name shed, I would add CoercePyAny following std's CoerceUnsized.

I think I'd be fine with that, but I want to note here that I would consider coercion a distinct mechanism in Rust, which is not really what's happening here. I can see the similarities, but it also could potentially cause confusion. Let's see what other think about that.

I'd suggest keeping the machinery required to implement native types public/documented as downstream crates might need to wrap other native types than those provided in the pyo3 crate, e.g. rust-numpy has PyArray<T, D> for which the existing pyobject_native_type_* macros proved insufficient to handle.

👍

Copy link
Member

Choose a reason for hiding this comment

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

I think I'd be fine with that, but I want to note here that I would consider coercion a distinct mechanism in Rust, which is not really what's happening here. I can see the similarities, but it also could potentially cause confusion. Let's see what other think about that.

Agreed with this, CoercePyAny is reasonable but I'd sort of expect Bound<T> to coerce to Bound<PyAny> as a non-reference too if it was proper coercion. (e.g. in fn foo(any: Bound<PyAny>) pass any Bound and get the coercion.)

Some other names I can think of:

  • IsNotPyAny
  • BoundDerefToPyAny

Copy link
Member

Choose a reason for hiding this comment

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

I would additionally nominate DerefToPyAny then as I don't think this is limited to Bound as the outer wrapper, i.e. we could do this for Borrowed<T> as well (if it did not already Deref to Bound<T>).

(I dislike IsNotPyAny because traits should IMHO encapsulate the required properties and/or operations, not type identities. But then again, the whole bound seems to work around what I'd consider a quality of implementation issue in rustc as linked above.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also don't really like IsNotPyAny for the same reasons. I agree that this is in principle independent of the container type, so I have renamed it to DerefToPyAny for now. I also added a note in the docs about why this is needed and that it will be removed if the compiler gets smart enough to accept this.

Copy link
Member

Choose a reason for hiding this comment

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

DerefToPyAny seems clear enough 👍

Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

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

This looks good to me as a solution to keep the behaviour working like it would for the GIL Refs, we seem to have three of us agree on DerefToPyAny 👍

Thanks!

@davidhewitt davidhewitt added this pull request to the merge queue Feb 22, 2024
Merged via the queue into PyO3:main with commit 4f8ee96 Feb 22, 2024
38 of 39 checks passed
@Icxolu Icxolu deleted the bound-deref branch February 23, 2024 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI-skip-changelog Skip checking changelog entry
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants