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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This chapter will discuss the functionality and configuration these attributes o
- [`#[pyo3(get, set)]`](#object-properties-using-pyo3get-set)
- [`#[pymethods]`](#instance-methods)
- [`#[new]`](#constructor)
- [`#[init]`](#initializer)
- [`#[getter]`](#object-properties-using-getter-and-setter)
- [`#[setter]`](#object-properties-using-getter-and-setter)
- [`#[staticmethod]`](#static-methods)
Expand Down Expand Up @@ -131,7 +132,7 @@ For now, don't worry about these requirements; simple classes will already be th

By default, it is not possible to create an instance of a custom class from Python code.
To declare a constructor, you need to define a method and annotate it with the `#[new]`
attribute. Only Python's `__new__` method can be specified, `__init__` is not available.
attribute. A constructor is accessible as Python's `__new__` method.

```rust
# #![allow(dead_code)]
Expand Down Expand Up @@ -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.


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.


It may be required when it's needed to control an object initalization flow on the Rust code.
For example, you define a class that extends `PyDict` and don't want that the original
`__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

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

in the constructor method.

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.

# #![allow(dead_code)]
# use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};

#[pyclass(extends = PyDict)]
struct MyDict;

#[pymethods]
impl MyDict {
#[new]
# #[allow(unused_variables)]
#[pyo3(signature = (*args, **kwargs))]
fn __new__(
args: &Bound<'_, PyTuple>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<Self> {
Ok(Self)
}

#[init]
#[pyo3(signature = (*args, **kwargs))]
fn __init__(
self_: &Bound<'_, Self>,
args: &Bound<'_, PyTuple>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<()> {
self_
.py_super()?
.call_method("__init__", args.to_owned(), kwargs)?;
Ok(())
}
}
```

## Adding the class to a module

The next step is to create the module initializer and add our class to it:
Expand Down
1 change: 0 additions & 1 deletion guide/src/class/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
Python's object model defines several protocols for different object behavior, such as the sequence, mapping, and number protocols. Python classes support these protocols by implementing "magic" methods, such as `__str__` or `__repr__`. Because of the double-underscores surrounding their name, these are also known as "dunder" methods.

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).
- `__del__` is not yet supported, but may be in the future.
- `__buffer__` and `__release_buffer__` are currently not supported and instead PyO3 supports [`__getbuffer__` and `__releasebuffer__`](#buffer-objects) methods (these predate [PEP 688](https://peps.python.org/pep-0688/#python-level-buffer-protocol)), again this may change in the future.
- PyO3 adds [`__traverse__` and `__clear__`](#garbage-collector-integration) methods for controlling garbage collection.
Expand Down
1 change: 1 addition & 0 deletions newsfragments/4951.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `#[init]` method attribute in `#[pymethods]`.
67 changes: 54 additions & 13 deletions pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ pub enum FnType {
FnNew,
/// Represents a pymethod annotated with both `#[new]` and `#[classmethod]` (in either order)
FnNewClass(Span),
/// Represents a pymethod annotated with `#[init]`, i.e. the `__init__` dunder.
FnInit(SelfType),
/// Represents a pymethod annotated with `#[classmethod]`, like a `@classmethod`
FnClass(Span),
/// Represents a pyfunction or a pymethod annotated with `#[staticmethod]`, like a `@staticmethod`
Expand All @@ -220,6 +222,7 @@ impl FnType {
FnType::Getter(_)
| FnType::Setter(_)
| FnType::Fn(_)
| FnType::FnInit(_)
| FnType::FnClass(_)
| FnType::FnNewClass(_)
| FnType::FnModule(_) => true,
Expand All @@ -231,6 +234,7 @@ impl FnType {
match self {
FnType::Fn(_)
| FnType::FnNew
| FnType::FnInit(_)
| FnType::FnStatic
| FnType::FnClass(_)
| FnType::FnNewClass(_)
Expand All @@ -250,7 +254,7 @@ impl FnType {
) -> Option<TokenStream> {
let Ctx { pyo3_path, .. } = ctx;
match self {
FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) => {
FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) | FnType::FnInit(st) => {
let mut receiver = st.receiver(
cls.expect("no class given for Fn with a \"self\" receiver"),
error_mode,
Expand Down Expand Up @@ -378,6 +382,7 @@ pub enum CallingConvention {
Varargs, // METH_VARARGS | METH_KEYWORDS
Fastcall, // METH_FASTCALL | METH_KEYWORDS (not compatible with `abi3` feature before 3.10)
TpNew, // special convention for tp_new
TpInit, // special convention for tp_init
}

impl CallingConvention {
Expand Down Expand Up @@ -476,10 +481,10 @@ impl<'a> FnSpec<'a> {
FunctionSignature::from_arguments(arguments)
};

let convention = if matches!(fn_type, FnType::FnNew | FnType::FnNewClass(_)) {
CallingConvention::TpNew
} else {
CallingConvention::from_signature(&signature)
let convention = match fn_type {
FnType::FnNew | FnType::FnNewClass(_) => CallingConvention::TpNew,
FnType::FnInit(_) => CallingConvention::TpInit,
_ => CallingConvention::from_signature(&signature),
};

Ok(FnSpec {
Expand Down Expand Up @@ -524,11 +529,14 @@ impl<'a> FnSpec<'a> {
.map(|stripped| syn::Ident::new(stripped, name.span()))
};

let mut set_name_to_new = || {
if let Some(name) = &python_name {
bail_spanned!(name.span() => "`name` not allowed with `#[new]`");
let mut set_fn_name = |name| {
if let Some(ident) = python_name {
bail_spanned!(ident.span() => format!("`name` not allowed with `#[{name}]`"));
}
*python_name = Some(syn::Ident::new("__new__", Span::call_site()));
*python_name = Some(syn::Ident::new(
format!("__{name}__").as_str(),
Span::call_site(),
));
Ok(())
};

Expand All @@ -539,14 +547,18 @@ impl<'a> FnSpec<'a> {
[MethodTypeAttribute::StaticMethod(_)] => FnType::FnStatic,
[MethodTypeAttribute::ClassAttribute(_)] => FnType::ClassAttribute,
[MethodTypeAttribute::New(_)] => {
set_name_to_new()?;
set_fn_name("new")?;
FnType::FnNew
}
[MethodTypeAttribute::New(_), MethodTypeAttribute::ClassMethod(span)]
| [MethodTypeAttribute::ClassMethod(span), MethodTypeAttribute::New(_)] => {
set_name_to_new()?;
set_fn_name("new")?;
FnType::FnNewClass(*span)
}
[MethodTypeAttribute::Init(_)] => {
set_fn_name("init")?;
FnType::FnInit(parse_receiver("expected receiver for `#[init]`")?)
}
[MethodTypeAttribute::ClassMethod(_)] => {
// Add a helpful hint if the classmethod doesn't look like a classmethod
let span = match sig.inputs.first() {
Expand Down Expand Up @@ -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?

#arg_convert
#init_holders
let result = #call;
Expand All @@ -839,6 +850,29 @@ impl<'a> FnSpec<'a> {
}
}
}
CallingConvention::TpInit => {
let mut holders = Holders::new();
let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx);
let self_arg = self
.tp
.self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx);
let call = quote_spanned! {*output_span=> #rust_name(#self_arg #(#args),*) };
let init_holders = holders.init_holders(ctx);
quote! {
unsafe fn #ident(
py: #pyo3_path::Python<'_>,
_slf: *mut #pyo3_path::ffi::PyObject,
_args: *mut #pyo3_path::ffi::PyObject,
_kwargs: *mut #pyo3_path::ffi::PyObject
) -> #pyo3_path::PyResult<::std::os::raw::c_int> {
use #pyo3_path::impl_::callback::IntoPyCallbackOutput;
#arg_convert
#init_holders
#call?;
Ok(0)
}
}
}
})
}

Expand Down Expand Up @@ -917,6 +951,7 @@ impl<'a> FnSpec<'a> {
)
},
CallingConvention::TpNew => unreachable!("tp_new cannot get a methoddef"),
CallingConvention::TpInit => unreachable!("tp_init cannot get a methoddef"),
}
}

Expand All @@ -934,7 +969,7 @@ impl<'a> FnSpec<'a> {
let self_argument = match &self.tp {
// Getters / Setters / ClassAttribute are not callables on the Python side
FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => return None,
FnType::Fn(_) => Some("self"),
FnType::Fn(_) | FnType::FnInit(_) => Some("self"),
FnType::FnModule(_) => Some("module"),
FnType::FnClass(_) | FnType::FnNewClass(_) => Some("cls"),
FnType::FnStatic | FnType::FnNew => None,
Expand All @@ -950,6 +985,7 @@ impl<'a> FnSpec<'a> {

enum MethodTypeAttribute {
New(Span),
Init(Span),
ClassMethod(Span),
StaticMethod(Span),
Getter(Span, Option<Ident>),
Expand All @@ -961,6 +997,7 @@ impl MethodTypeAttribute {
fn span(&self) -> Span {
match self {
MethodTypeAttribute::New(span)
| MethodTypeAttribute::Init(span)
| MethodTypeAttribute::ClassMethod(span)
| MethodTypeAttribute::StaticMethod(span)
| MethodTypeAttribute::Getter(span, _)
Expand Down Expand Up @@ -1018,6 +1055,9 @@ impl MethodTypeAttribute {
if path.is_ident("new") {
ensure_no_arguments(meta, "new")?;
Ok(Some(MethodTypeAttribute::New(path.span())))
} else if path.is_ident("init") {
ensure_no_arguments(meta, "init")?;
Ok(Some(MethodTypeAttribute::Init(path.span())))
} else if path.is_ident("classmethod") {
ensure_no_arguments(meta, "classmethod")?;
Ok(Some(MethodTypeAttribute::ClassMethod(path.span())))
Expand All @@ -1043,6 +1083,7 @@ impl Display for MethodTypeAttribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MethodTypeAttribute::New(_) => "#[new]".fmt(f),
MethodTypeAttribute::Init(_) => "#[init]".fmt(f),
MethodTypeAttribute::ClassMethod(_) => "#[classmethod]".fmt(f),
MethodTypeAttribute::StaticMethod(_) => "#[staticmethod]".fmt(f),
MethodTypeAttribute::Getter(_, _) => "#[getter]".fmt(f),
Expand Down
53 changes: 53 additions & 0 deletions pyo3-macros-backend/src/pymethod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ pub fn gen_py_method(
(_, FnType::FnNew) | (_, FnType::FnNewClass(_)) => {
GeneratedPyMethod::Proto(impl_py_method_def_new(cls, spec, ctx)?)
}
(_, FnType::FnInit(_)) => {
GeneratedPyMethod::Proto(impl_py_method_def_init(cls, spec, ctx)?)
}

(_, FnType::Getter(self_type)) => GeneratedPyMethod::Method(impl_py_getter_def(
cls,
Expand Down Expand Up @@ -393,6 +396,56 @@ pub fn impl_py_method_def_new(
})
}

pub fn impl_py_method_def_init(
cls: &syn::Type,
spec: &FnSpec<'_>,
ctx: &Ctx,
) -> Result<MethodAndSlotDef> {
let Ctx { pyo3_path, .. } = ctx;
let wrapper_ident = syn::Ident::new("__pymethod___init____", Span::call_site());
let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls), ctx)?;
// Use just the text_signature_call_signature() because the class' Python name
// isn't known to `#[pymethods]` - that has to be attached at runtime from the PyClassImpl
// trait implementation created by `#[pyclass]`.
let text_signature_body = spec.text_signature_call_signature().map_or_else(
|| quote!(::std::option::Option::None),
|text_signature| quote!(::std::option::Option::Some(#text_signature)),
);
let slot_def = quote! {
#pyo3_path::ffi::PyType_Slot {
slot: #pyo3_path::ffi::Py_tp_init,
pfunc: {
unsafe extern "C" fn trampoline(
slf: *mut #pyo3_path::ffi::PyObject,
args: *mut #pyo3_path::ffi::PyObject,
kwargs: *mut #pyo3_path::ffi::PyObject,
) -> ::std::os::raw::c_int {
use #pyo3_path::impl_::pyclass::*;
#[allow(unknown_lints, non_local_definitions)]
impl PyClassInitTextSignature<#cls> for PyClassImplCollector<#cls> {
#[inline]
fn init_text_signature(self) -> ::std::option::Option<&'static str> {
#text_signature_body
}
}

#pyo3_path::impl_::trampoline::initproc(
slf,
args,
kwargs,
#cls::#wrapper_ident
)
}
trampoline
} as #pyo3_path::ffi::initproc as _
}
};
Ok(MethodAndSlotDef {
associated_method,
slot_def,
})
}

fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec<'_>, ctx: &Ctx) -> Result<MethodAndSlotDef> {
let Ctx { pyo3_path, .. } = ctx;

Expand Down
7 changes: 1 addition & 6 deletions pyo3-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,7 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
#[doc = concat!("[11]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#object-properties-using-pyo3get-set")]
#[proc_macro_attribute]
pub fn pymethods(attr: TokenStream, input: TokenStream) -> TokenStream {
let methods_type = if cfg!(feature = "multiple-pymethods") {
PyClassMethodsType::Inventory
} else {
PyClassMethodsType::Specialization
};
pymethods_impl(attr, input, methods_type)
pymethods_impl(attr, input, methods_type())
}

/// A proc macro used to expose Rust functions to Python.
Expand Down
Loading
Loading