-
Notifications
You must be signed in to change notification settings - Fork 906
Implement #[init] method attribute in #[pymethods]
#4951
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
f256dc8 to
6dbd5f4
Compare
This allows to control objects initialization flow in the Rust code in case of inheritance from native Python types.
|
It seems that most of |
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.
My apologies for the delay. Thanks for this, it seems useful 👍
Please add some more tests, for stuff like combinations of attributes:
#[init]
#[classmethod]
#[init]
#[new]
and a pyclass that inherits another pyclass, that init cannot be on functions and so on.
pyo3-macros-backend/src/method.rs
Outdated
| _kwargs: *mut #pyo3_path::ffi::PyObject | ||
| ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { | ||
| use #pyo3_path::impl_::callback::IntoPyCallbackOutput; | ||
| let function = #rust_name; // Shadow the function name to avoid #3017 |
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.
Is this no longer needed?
|
btw I also have basic support for defining
|
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 @ahlinc for the PR. Based on the long gap here without response to the review, I will probably take this over in the coming weeks to unblock #4678. I think that supporting __init__ properly may also have benefits for #5137. Please forgive me if you did intend to return to this 🙏
I think the general implementation here looks good, we can probably drop the #[init] attribute. I also wonder what would happen if users did not define #[new] and just defined __init__, based on the current implementation users would get an error saying the class cannot be instantiated. That might be confusing; I wonder if we should consider other options like allowing users to just define __init__ (and make the type unusable before __init__ is called)?
I think given the possible interplay with inheritance, what I might do next is write up an issue summarising the current state of __new__ and __init__, versus what we might like to have in the future.
Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com>
|
I've refactored this to current main, as well as #5551, for now I'm switching this to draft until that lands. While I was at it, I also implemented most of the review feedback from above. |
4a56a0e to
5e8c8e2
Compare
|
With #5551 landed, I think this is now also in a reviewable state again. |
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.
Implementation looks great, thank you! A few thoughts which might prompt doc adjustments or follow up features.
guide/src/class.md
Outdated
| kwargs: Option<&Bound<'_, PyDict>>, | ||
| ) -> PyResult<()> { | ||
| // call the super types __init__ | ||
| slf.py_super()? |
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.
We might need to double check this and improve the py_super API, IIRC if slf is a subclass of MyDict then this infinitely recurses because of the way py_super gets the type from the object at runtime.
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.
Yep, you are right here, I just tried this. If replaced by PySuper::new(&PyDict::type_object(slf.py()), slf)? it starts working as expected. Maybe this means that py_super should really look like this instead:
fn py_super<T: PyTypeInfo>(&self) -> PyResult<Bound<'py, PySuper>> {
PySuper::new(&T::type_object(self.py()), self)
}This would require specifying the correct base to use.
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 quite possibly, yes. I am also not entirely sure what the interaction with module state (#5600) will be with our "global" type objects, so we may not want to rush to change py_super too fast.
To not block this PR, I guess we can:
- use the direct
PySuper::newcall here - create follow up tasks to:
- document
py_superwith the caveat on how it works - design a better replacement
- document
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.
Sounds good. Do we want to look into making this available on PyPy/GraalPy or defer that to followup as well?
| PyO3 makes it possible for every magic method to be implemented in `#[pymethods]` just as they would be done in a regular Python class, with a few notable differences: | ||
|
|
||
| - `__new__` and `__init__` are replaced by the [`#[new]` attribute](../class.md#constructor). | ||
| - `__new__` is replaced by the [`#[new]` attribute](../class.md#constructor). |
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.
Not a thing for here but I hope one day maybe we can have __new__ work exactly like Python (maybe it already does), the #[new] attribute might just make sense for some special cases or as a shorthand for #[pyo3(name = "__new__")].
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, maybe we can even deprecate and remove it entirely at some point, so that all dunder methods work similarly and the constructor is less special.
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 there are possible edge cases about __new__ probably wanting to always take (cls, *args, **kwargs) signature (not sure?) which might allow #[new] to have some special semantics.
Given the churn downstream my feeling is that deprecating #[new] is unlikely to be justified and documenting it as a supported shorthand is probably 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.
Sure, time will tell. Having it as an optional equivalent to #[pyo3(name = "__new__")] would still be quite nice.
pytests/src/pyclasses.rs
Outdated
| } | ||
| } | ||
|
|
||
| #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] // Can't subclass native types on abi3 yet, PyPy/GraalPy don't support `py_super` |
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.
We might want to make py_super() work by calling the python builtin directly on those interpreters? Maybe a follow up task depending what else we decide regarding that API.
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've exposed it in 70981d7. If you agree I think this is ready to land then.
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.
Looks great, thanks everyone for getting __init__ support implemented!
guide/src/class.md
Outdated
| # #[cfg(not(Py_LIMITED_API))] | ||
| use pyo3::types::{PyDict, PyTuple, PySuper}; | ||
| # #[cfg(not(Py_LIMITED_API))] | ||
| use crate::pyo3::PyTypeInfo; |
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 so nice to solve #1344 one day 😂
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'd be nice to tidy this error message up with a specialized return value somehow, this is totally fine to merge though.
|
|
||
| @final | ||
| class SubClassWithInit: | ||
| def __init__(self, /, *args, **kwargs) -> Any: ... |
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.
Looks like this type hint is wrong, I'll open an issue for this cc @Tpt
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.
Indeed, it should be None. Will have a look at it in a follow-up when this MR is merged.
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.
|
Looks like the PyPy failure is real. I've managed to reproduce it on linux (wsl) but not windows. |
|
Looks like some kind of memory corruption that occurs when we try to run the base initializer: self_.py_super()?.call_method("__init__", args.to_owned(), kwargs)?;Not sure why tho 🤔 |
|
Hmm, I will try to help investigate ASAP, probably Wednesday. This is probably good reason to delay #5646 just in case it needs a patch to the existing code. |
|
|
||
| #[cfg(not(any(Py_LIMITED_API)))] // Can't subclass native types on abi3 yet | ||
| #[pyclass(extends = PyDict)] | ||
| struct SubClassWithInit; |
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.
Small nit: I would move this test with the other subclassing tests in subclassing.rs
Not forgotten about this, have had some family stuff come up this week which is keeping me away from the linux desktop. |
|
No worries. I tried to debug a bit yesterday, but without much success. At least the |
|
From examining valgrind output, it seems to me that we have an issue where the PyPy GC is attempting to interact with the dict subclass after it's been deallocated. Presumably calling I can replace the use of use pyo3::PyTypeInfo;
let mut full_args = vec![self_.clone().into_any()];
for arg in args {
full_args.push(arg);
}
let args = PyTuple::new(self_.py(), full_args)?;
PyDict::type_object(self_.py()).call_method("__init__", args, kwargs)?;I can also trigger the crash more reliably by forcing gc collection after deleting the dict subclass: $ git diff tests
diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py
index e2ef59ef0..2db6db71b 100644
--- a/pytests/tests/test_pyclasses.py
+++ b/pytests/tests/test_pyclasses.py
@@ -1,3 +1,4 @@
+import gc
import platform
import sys
from typing import Type
@@ -177,3 +178,9 @@ def test_class_init_method():
d = SubClassWithInit({"a": 1}, b=2)
assert d == {"__init__": True, "a": 1, "b": 2}
+
+ del d
+ for i in range(10):
+ gc.collect(0)
+ gc.collect(1)
+ gc.collect(2)Not yet sure whether the issue is in our implementation of classes on PyPy, or an upstream PyPy issue. I'll try to minimise the repro. |
|
#5653 fixes 🎉 |
* Implement #[init] method attribute in #[pymethods] This allows to control objects initialization flow in the Rust code in case of inheritance from native Python types. * Apply suggestions from code review Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> * review feedback * expose `PySuper` on PyPy, GraalPy and ABI3 --------- Co-authored-by: David Hewitt <mail@davidhewitt.dev> Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com>
|
💯 Thanks for investigating! I suspected it had something to do with the subclassing, but only manifested itself due to the new |
d848e7c to
b078588
Compare
|
Woohoo 🎉 all green ✅ |
This allows to control objects initialization flow in the Rust code in case of inheritance from native Python types.
A simple example, you implement a class inherited from
PyDictbut want to build a custom constructor interface. With just the#[new]constructor it's not possible and the basePyDict__init__method will be called too with all arguments that were passed to the constructor as a standard objects initialization behavior.