Skip to content

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ahlinc
Copy link
Contributor

@ahlinc ahlinc commented Mar 1, 2025

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 PyDict but want to build a custom constructor interface. With just the #[new] constructor it's not possible and the base PyDict __init__ method will be called too with all arguments that were passed to the constructor as a standard objects initialization behavior.

@ahlinc ahlinc force-pushed the init-method branch 10 times, most recently from f256dc8 to 6dbd5f4 Compare March 2, 2025 20:52
This allows to control objects initialization flow in the Rust code
in case of inheritance from native Python types.
@ahlinc
Copy link
Contributor Author

ahlinc commented Mar 2, 2025

It seems that most of codecov/patch complains are unrelated to this PR changes.

@ahlinc ahlinc mentioned this pull request Mar 7, 2025
Copy link
Member

@mejrs mejrs left a 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.

explicitly like that happens in a regular Python code.

To declare an initializer, you need to define a method and annotate it with the `#[init]`
attribute. An `init` method must have the same input paretemeters signature like
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
attribute. An `init` method must have the same input paretemeters signature like
attribute. An `init` method must have the same input parameters signature like

`__init__` method of `PyDict` been called. In this case by defining an own `init` method
it's possible to stop initialization flow.

If you declare an own `init` method you may need to call a super class `__init__` method
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
If you declare an own `init` method you may need to call a super class `__init__` method
If you declare an own `init` method you may need to call a super class' `__init__` method

@@ -830,7 +842,6 @@ impl<'a> FnSpec<'a> {
_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
Copy link
Member

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?

@@ -104,6 +106,35 @@ impl ClassWithDict {
}
}

#[cfg(Py_3_8)]
Copy link
Member

Choose a reason for hiding this comment

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

Is #[init] only supported on 3.8 and up? Please also document this in the guide.

@@ -181,6 +182,59 @@ created from Rust, but not from Python.

For arguments, see the [`Method arguments`](#method-arguments) section below.

## Initializer
Copy link
Member

Choose a reason for hiding this comment

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

Please also document that it shall return either PyResult<()> or nothing. Ideally the macro would check for this; not sure how feasible that is.

@@ -181,6 +182,59 @@ created from Rust, but not from Python.

For arguments, see the [`Method arguments`](#method-arguments) section below.

## Initializer

An initializer implements Python's `__init__` method.
Copy link
Member

Choose a reason for hiding this comment

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

We should discourage users from using init unless they really need it; they probably should use new instead.


Like in the constructor case the Rust method name isn't important.

```rust,ignore
Copy link
Member

Choose a reason for hiding this comment

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

Why is this ignored? Please write some test code in this example. It can be something simple like a hidden test that just creates and checks it.

@mbway
Copy link

mbway commented Apr 20, 2025

btw I also have basic support for defining __init__ in my PR because it is required for defining a metaclass. I haven't compared the implementations but mine has the limitation of not supporting inheritance properly so this is probably better.

#4678 (comment)

There is a limitation with extending PyType that tp_new cannot be set so only tp_init is available. My workaroud for now is to initialize to Default::default() before calling the user's init function but this doesn't work with multi-level inheritance (currently I catch this case with an assert). I decided not to invest more into this because it might be entirely scrapped in favour of re-purposing #[new] in the case of a metaclass but that's open to discussion. I thought the init approach would at least be simpler to implement for my proof of concept.

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.

3 participants