-
Notifications
You must be signed in to change notification settings - Fork 904
add second lifetime to FromPyObject
#4390
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
Conversation
|
I propose that we add a |
|
Thanks! I think that's a great idea. It's essentially the same thing, but it's much easier to grasp by giving it a distinct name, plus we can better document it in the API docs. I will adapt this to make use of that. |
c960124 to
a1c0bed
Compare
davidhewitt
left a comment
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.
Thanks for pushing on with this! Overall it looks good to me, and I only have a few docs suggestions.
Before we definitely commit to this, I want to take a brief moment just to reflect on the choice of Borrowed over &Bound. There's at least two good technical reasons, which is the lifetime constraint and performance by avoiding pointer-to-pointer. The codspeed branch does show a few slight perf improvements in the ~4% range on tuple extraction and we have further possibilities like PyRef containing Borrowed once we do this.
That said, we deliberately kept the Borrowed smart pointer out of the way as much as possible in the original 0.21 design to try to keep new concepts down. Is it fine to increase its use? I think the upsides justify this, though we might want to increase the quality of documentation and examples on Borrowed (which I'd left quite minimal so far).
| #[automatically_derived] | ||
| impl #trait_generics #pyo3_path::FromPyObject<#lt_param> for #ident #generics #where_clause { | ||
| fn extract_bound(obj: &#pyo3_path::Bound<#lt_param, #pyo3_path::PyAny>) -> #pyo3_path::PyResult<Self> { | ||
| impl #trait_generics #pyo3_path::FromPyObject<'_, #lt_param> for #ident #generics #where_clause { |
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.
It will be an interesting question for later if we ever support the 'a lifetime in our #[derive(FromPyObject)]. Definitely not for now 😂
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, something to experiment with in the future, but out of scope for now for sure 😅
|
Thanks for the review! You are right, the choice of
I agree, if we put |
mejrs
left a comment
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.
IMO the recently added FromPyObject documentation is too "implementation detail"-y.
I'd prefer if this documentation is kept short and concise, one paragraph max.
For example:
Depending on the python version and implementation some `FromPyObject`
implementations may produce Rust types that point into the Python type.
For example `PyString` and `PyBytes` may convert into `str` and `[u8]` references
that borrow from the original Python object.
Types that do not borrow from the input should use `FromPyObjectOwned` instead.
|
Thanks for the feedback! I took another pass over it and tried to make it a bit more concise. I do however think that it's worth to talk about the individual lifetimes at play here, since this trait is right in the center of PyO3. I moved the paragraph about collections into the Let me know what you think about this. |
9cd0a41 to
91ef5fe
Compare
davidhewitt
left a comment
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.
Thanks very much for keeping this branch alive, I guess let's move forward with this before it gets too painful to keep updating.
I had yet another reflection on &Bound vs Borrowed and I'm feeling reasonably comfortable that Borrowed is the correct choice.
|
|
||
| impl<'py, $($T: FromPyObject<'py>),+> FromPyObject<'py> for ($($T,)+) { | ||
| fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult<Self> | ||
| impl<'a, 'py, $($T: FromPyObject<'a, 'py>),+> FromPyObject<'a, 'py> for ($($T,)+) { |
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.
Just for the record and the decision to stick with Borrowed, the reason is that if we used &'a Bound here for the input argument, then tuple extraction would be forced to restrict elements to FromPyObjectOwned.
That maybe wouldn't be a terrible thing, as this is currently a special case when compared with every other container, which is forced to use FromPyObjectOwned due to their mutability.
But I think it would be a breaking change, so it's probably not a good change overall. (E.g. users would lose the ability to extract tuples containing &str, &Bound and Borrowed)
I think as a secondary effect, by using Borrowed here we might be able to change extract_argument.rs to make more use of Borrowed, which IIRC is a possible performance win.
|
I believe this now also finishes the |
|
Fantastic, thank you so much for your help on pushing that over the line. When I get some time I'll try to write up a list of what we might have left for 0.23 (hopefully not much). |
|
Ah, I just started playing with this locally and found one additional finding: having Is that a problem? I think not, because that's what |
|
Oh, and one more case which comes up in local testing. At the moment |
It feels to me like we might want to consider macro tricks such that users can use both |
That's true, but as you mention below, I actually think this a good thing and makes a clearer use case between
Hmm, good question. I guess that is part of the question about how prominent we want |
|
Yes agreed, it's definitely a follow-up to make changes to expand I wonder, are there any other options we have here? E.g. does it work to have a trait with both I am still feeling a little uneasy about the choice to commit |
|
And (sorry to keep dangling ideas on here), inspired by the new
Both changes would significantly complicate the trait. I wonder, is there a way that we can support a simple trait like All food for thought and I think worth discussing while we're committing to breaking changes here. |
No problem 😄
If we're willing to do more breaking changes here, that sounds like a good idea to me. The only downsides I can see
This would be really cool indeed. I guess we would maybe somehow need to take advantage of method precedence between trait and inherent methods, to make the fall through to |
c865e7f to
c366603
Compare
8332c17 to
42dd0d4
Compare
davidhewitt
left a comment
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.
Thanks for all the work here, I think this is pretty much ready to go! I had a few minor comments, mostly I think documentation related.
I guess there are a few follow-ups we should probably throw into issues so we don't forget to complete for 0.27:
- Better documentation on borrowed
- We should explore if we can make some more error types to delay PyErr creation, e.g. on the set extraction it looks like the error is essentially a downcast error or the key extraction error. I guess there's a tradeoff here of API complexity vs performance.
- I think the functions in
extract_argument.rsprobably can takeBorrowedrather than&Boundbut I think that's probably going to involve macro changes which would preferably not be a part of this diff.
| pub trait FromPyObject<'py>: Sized { | ||
| /// Types that must not borrow from the input can use [`FromPyObjectOwned`] as a restriction. This | ||
| /// is most often the case for collection types. See its documentation for more details. | ||
| /// |
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 we should have a section to this doc "Implementing FromPyObject" which lists the choices available:
#[derive(FromPyObject)]- manual implementation for types without a lifetime (i.e.
impl FromPyObject<'_, '_> for X) - manual implementation for types with a lifetime
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.
Good idea, I added a section with these. Do you have a suggestion for an example type with lifetimes?
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.
Looking great!
For the type with lifetimes, I suggest using either &'a str or Bound<'py, T> and saying "this is roughly how this is implemented in PyO3", I think just choosing one of the two is good 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.
Done 👍
|
Oh, and I think there's a note in the guide about performance of |
|
Thanks for the review ❤️ . I've opened some sub-issues in #5390 to track various followups from here. |
Co-authored-by: David Hewitt <mail@davidhewitt.dev>
2f6fb19 to
52dba29
Compare
davidhewitt
left a comment
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.
Subject to final documentation fixups, let's merge!
Thank you for all the iteration and R&D done here 🚀
| pub trait FromPyObject<'py>: Sized { | ||
| /// Types that must not borrow from the input can use [`FromPyObjectOwned`] as a restriction. This | ||
| /// is most often the case for collection types. See its documentation for more details. | ||
| /// |
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.
Looking great!
For the type with lifetimes, I suggest using either &'a str or Bound<'py, T> and saying "this is roughly how this is implemented in PyO3", I think just choosing one of the two is good enough.
52dba29 to
06c37c9
Compare
|
Great, all green 🎉 I'm going to send this now. We can handle any touchups that are left in followups. |
This adds the second lifetime to
FromPyObjectand removesFromPyObjectBound. I hope I adjusted all the bounds correctly. I believe the equivalent to our current implementation is use HRTB for the input lifetime on any generic bound. But this should only be necessary for containers that create temporary Python references during extraction. For wrapper types it should be possible to just forward the relaxed bound.For easier trait bounds
FromPyObjectOwnedis introduced. This is blanket implemented for anyFromPyObjecttype that does not depend on the input lifetime. It is intended to be used as a trait bound, the idea is inspired byserde(Deserialize<=>DeserializeOwned)I tried to document different cases in the migration guide, but it can probably still be extended.
Changelog entry is still missing, will write that tomorrow.Breaking changes:
extractmethod without defaultextract_boundto make the default work