-
Notifications
You must be signed in to change notification settings - Fork 779
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
Add IntoPyObject
conversion trait
#4060
Conversation
CodSpeed Performance ReportMerging #4060 will not alter performanceComparing 🎉 Hooray!
|
I've played around with this some more, and added more conversions for experimentation. (I will remove most of the impls again from here, once we reached some consensus, since many of them warrant their own PRs) I have the feeling that an associated type is the more appropriate choice:
For these points I switched the generic for an associated type. The only type that's a bit special is |
I just wanted to comment that although I haven't managed to actually reply or give thought in detail here, I'm really excited for this and want to circle back to it ASAP. I think at this point it might be that this is too big of a change for landing in 0.22, but I'd be keen to see it land in 0.23 if we can make it happen! |
I'm excited too! I hope this can both enable new use cases, that we previously just didn't support, while also reducing the complexity to implement Python conversions. Having a single trait in each direction is hopefully making it much clearer how everything is wired together.
Yes, I agree. Given how close we are to releasing 0.22 I think it makes sense to target 0.23 here 🤞I think there is also some more work following this, I included a lot of new impls in here for demontration purposes, but in general I think it would make sense to land them seperately, so we can discuss them individually and make reviewing easier. So no need to rush 😄 |
I just rebased this again, now that 0.22 is shipped. It seems like that the deref specialization used here prevents the nicer error message that we introduced in #4220 for the missing conversion trait case (or I haven't found the correct place yet). We should also think again about whether and how we want to integrate the ideas from #4182 here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just rebased this again, now that 0.22 is shipped. It seems like that the deref specialization used here prevents the nicer error message that we introduced in #4220 for the missing conversion trait case (or I haven't found the correct place yet).
We should also think again about whether and how we want to integrate the ideas from here.
Awesome! I am heavily in favour of moving forward with this trait for 0.23, and I think this PR is probably the correct thing to land as a first step more or less in this form. We can then iterate towards the 0.23 release.
For this PR:
- The conflict with Improve the span and message for return types of pymethod/functions #4220 - do you want me to pull this branch and see if I can figure it out? I also wonder if we should remove the GIL refs deprecations from the macros to simplify the problem.
- I'll seek to give a full review at earliest availability.
Follow-ups:
- I think we will need to solve the migration story for how to re-implement
ToPyObject
andIntoPy<PyObject>
(plus other forms) in terms of this new API, and figure out how we expect users to need to adjust code. - Ideas from [RFC] standardize PyBytes <-> Vec<u8> or &[u8] or Cow<[u8]> #4182 - yes I think we should do these. It might be worth first solving the above bullet so that
ToPyObject
andIntoPy
behave consistently with whatever we add to this trait. - I wonder if we should also adjust
FromPyObject
to have thetype Error
associated type, for consistency (and possible optimisations).
I will give this another try later today or tomorrow, but since there is now more that one trait involved, and we are intentionally vague about which one the compiler should use, I feel like there is no clear choice for the compiler to say "This one is not implemented" instead we now have a set of traits from which any implementation would be fine. Therefore the error message is more generic in that case (or at least that would be my explanation). I can also prepare a PR removing the gil-ref deprecations from the macros to see if that helps.
Awesome, much appreciated! Are you fine with me leaving all the trait implementations in for the review, or should I remove them here we land them separately?
I will probably look at that next 👍 |
I think all implementations are fine to be in this PR 👍 |
45968b9
to
4b52b58
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks like it's shaping up nicely and I'm optimistic that we've got a good and flexible enough formulation here. The trait definition is now a fair bit more complex than ToPyObject
or IntoPy
, but I think that can be handled by good documentation and examples.
Reading through more and more of the implementations I find myself tempted to scrutinise each one, so I'm beginning to wonder how we can land this in chunks without making this PR get stuck in review hell. I think answering that question will probably give us an incremental pathway that also works for users, so I'll keep musing on that point.
I haven't yet looked too hard at the specialization in the macros, I will try to study that soon.
dict.set_item( | ||
k.into_pyobject(py)?.into_bound(), | ||
v.into_pyobject(py)?.into_bound(), | ||
)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here it would be nice to just have dict.set_item(k, v)
and ideally the fact that K
and V
implement IntoPyObject
would be enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I guess this ties into how do the migration from IntoPy
/ToPyObject
. Maybe this is something to solve in a followup once we have put some thoughts into how that can and should work? This implementation is kind of workaround for that missing piece.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I think a followup makes sense.
@@ -51,6 +54,29 @@ where | |||
} | |||
} | |||
|
|||
impl<'py, K, V, H> IntoPyObject<'py> for hashbrown::HashMap<K, V, H> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess there's a question here, should we implement this for &HashMap
too? (Subject to &K
and &V
implementing IntoPyObject
.)
Similar for a lot of other containers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think it would be desirable to provide these, since there is no real reason why one should not be able to continue using such a container after the conversion to Python.
I vaguely remember that I tried to add them at some point but ran into lots of HRTBs, I just tried again and it worked like I would have expected so either the implementation evolved and made it simpler, or I just did it wrong last time 🤷.
Since this is already quite a lot I suggest to land them separately.
where | ||
T: IntoPyObject<'py>, | ||
{ | ||
type Target = PyList; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess there's a relevant discussion point here if [u8; N]
should be treated as a byte slice and become PyBytes
(in which case this here might be PyAny
, unless we can find a way to do a type-level specialization on this associated type).
impl<'py, T: PyClass> IntoPyObject<'py> for PyRef<'py, T> { | ||
type Target = T; | ||
type Output = Bound<'py, T>; | ||
type Error = Infallible; | ||
|
||
fn into_pyobject(self, _py: Python<'py>) -> Result<Self::Output, Self::Error> { | ||
Ok(self.inner.clone()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if somehow we can manually decrement the borrow count and then just return self.inner
to avoid the clone here (same for the PyRefMut
case).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, these are suboptimal... I haven't figured out a way to move the inner
because of the Drop
impl. But with the complete remove of the gil-refs now, maybe we can also now store a Borrowed
in PyRef
/PyRefMut
, which should solve that issue I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reflecting on Borrowed
in PyRef
/ PyRefMut
, it creates the downside we already saw with types like Vec<&'a str>
where they can't easily be extracted. (Because a PyRef<'a, 'py, T>
containing a borrowed won't be able to be stored by iterating a list, for example.)
But I think on balance that might actually be a good thing, because it helps prevent locking objects to read/write for longer than desired. (The migration solution would be encouraging users to extract Vec<Bound<'py, T>>
and then .borrow()
for just the needed span of code.)
I've been thinking hard about the migration options, in particular what we want users to have to do when upgrading. I think if we finish the change to I have been thinking hard about a blanket implementation of
The third option is to adopt no blanket implementation and just switch all PyO3 APIs to use I think on balance my currently opinion is that we should take this third option and add neither blanket. This means that code again gets duplicated, similar to the We can at least reduce the amount of duplication by having a (public?) helper macro to create an |
I agree, we want to change the APIs to use the new
Yeah I think this is important so that we have a solid foundation for the future.
For the proc-macros this is what I implemented here already (or did you mean something different?) As for generic code, I will give this a thought, but my initial feeling says the same as you.
Maybe yes, will explore that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementations all look good. I think we should proceed to merge this so that we can then iterate in smaller PRs.
I will briefly have a go now at the missing_intopy
error regression...
A::Item: IntoPyObject<'py>, | ||
PyErr: From<<A::Item as IntoPyObject<'py>>::Error>, | ||
{ | ||
type Target = PyList; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible question if this will become PyAny
for SmallVec<u8>
creating bytes.
@Icxolu I pushed a commit which adjusted the autoderef specialization mechanism to make it a bit easier for me to reason about and managed to add fallback cases which means that the diagnostic actually emits hints that It's a little janky and this branch might need fixups / rebase, but otherwise please feel free to merge this (I'm out of time for today). |
allow error type to be convertible into `PyErr` instead of requiring `PyErr`
Thanks for the review! Your autoderef specialization mechanism is much easier to read than what I originally implemented, I really like that 🚀 . Also keeping the more descriptive error message is really nice 👍 . I rebased this again, fixed the clippy warnings and added a missing implementation for |
IntoPyObject
conversion trait
This implements a proof of concept of a new fallible conversion trait.
Design
IntoPyObject
This introduces a new trait
IntoPyObject
to perform the conversion of implemented type into Python.Currently the trait is generic so that the same type can have multiple conversions to different Python types. In this version I used it to provide an additional typed conversion for theI changed the generic to an associated type as outlined in #4060 (comment)chrono
types when the unlimited API is enabled. There might be more situations where this could be useful.The
Error
is specified as an associated type, since every conversion should have a single applicable error (orInfallible
if it can't fail).The return type is specified to
Bound<T>
to guarantee that we convert to a Python type.IntoPyObjectExtAdditionally to
IntoPyObject
this also addsIntoPyObjectExt
. The sole purpose of this trait is to allow specifying the desired output type using the turbo fish syntax:This makes it much nicer to work with types that implement multiple conversions and need type hinting. This would probably be the API most users would interact with.This is only beneficial in the case of a generic trait.Macros
To use the new conversion on return values of
#[pyfunction]
and friends without breaking current code, a variant ofautorefderef specialization is implemented. It prefers the implementation ofIntoPyObject
overIntoPy<PyObject>
. (Similar to the current approach the conversion to) This means there should be no changes required by users. The macros will pick up the falliblePyAny
needs to be available.IntoPyObject
implementation automatically, if available. For the migration we could then just deprecateIntoPy
(andToPyObject
) and advise to implementIntoPyObject
instead, removing the old conversions in a future release. Additionally there is another layer of specialization for the()
return type in#[pymethods]
and#[pyfunctions]
. This allows converting them intoPyNone
as part of the macro code, because that's the Python equivalent of a function returning "nothing", while still usingPyTuple
as theIntoPyObject::Target
.Open Questions
IntoPyObject
be generic, or would an associated type also work? Maybe implementing it on some more types will give us more insight whether the generic is needed.From my initial implementation I have the feeling that the generic version introduces a lot of complexity while not really providing a big benefit.
IntoPy
andToPyObject
? I assume we need to introduce new variants that are generic overIntoPyObject
, similar to what we did with the_bound
constructors.There are probably some edge cases that don't work correctly, but I thought I'll gather some feedback first whether this goes into the right direction before I chase those down.
TODOs
AnyBound
, possible candidates:BoundObject
BoundType
IntoPyObjectOutput
BoundMethods
Either
implFollowups
&HashMap
,&BTreeSet
or&Vec
PyRef
/PyRefMut
, possibly by storing aBorrowed
in themIntoPy<PyObject>
andToPyObject
PyBytes
specialization ([RFC] standardize PyBytes <-> Vec<u8> or &[u8] or Cow<[u8]> #4182)BoundObject
to the prelude?Xref #4041