Skip to content

Commit a06a2ad

Browse files
committed
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.
1 parent 31bb2f4 commit a06a2ad

File tree

12 files changed

+245
-25
lines changed

12 files changed

+245
-25
lines changed

guide/src/class.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This chapter will discuss the functionality and configuration these attributes o
1010
- [`#[pyo3(get, set)]`](#object-properties-using-pyo3get-set)
1111
- [`#[pymethods]`](#instance-methods)
1212
- [`#[new]`](#constructor)
13+
- [`#[init]`](#initializer)
1314
- [`#[getter]`](#object-properties-using-getter-and-setter)
1415
- [`#[setter]`](#object-properties-using-getter-and-setter)
1516
- [`#[staticmethod]`](#static-methods)
@@ -131,7 +132,7 @@ For now, don't worry about these requirements; simple classes will already be th
131132

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

136137
```rust
137138
# #![allow(dead_code)]
@@ -181,6 +182,59 @@ created from Rust, but not from Python.
181182

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

185+
## Initializer
186+
187+
An initializer implements Python's `__init__` method.
188+
189+
It may be required when it's needed to control an object initalization flow on the Rust code.
190+
For example, you define a class that extends `PyDict` and don't want that the original
191+
`__init__` method of `PyDict` been called. In this case by defining an own `init` method
192+
it's possible to stop initialization flow.
193+
194+
If you declare an own `init` method you may need to call a super class `__init__` method
195+
explicitly like that happens in a regular Python code.
196+
197+
To declare an initializer, you need to define a method and annotate it with the `#[init]`
198+
attribute. An `init` method must have the same input paretemeters signature like
199+
in the constructor method.
200+
201+
Like in the constructor case the Rust method name isn't important.
202+
203+
```rust,ignore
204+
# #![allow(dead_code)]
205+
# use pyo3::prelude::*;
206+
use pyo3::types::{PyDict, PyTuple};
207+
208+
#[pyclass(extends = PyDict)]
209+
struct MyDict;
210+
211+
#[pymethods]
212+
impl MyDict {
213+
#[new]
214+
# #[allow(unused_variables)]
215+
#[pyo3(signature = (*args, **kwargs))]
216+
fn __new__(
217+
args: &Bound<'_, PyTuple>,
218+
kwargs: Option<&Bound<'_, PyDict>>,
219+
) -> PyResult<Self> {
220+
Ok(Self)
221+
}
222+
223+
#[init]
224+
#[pyo3(signature = (*args, **kwargs))]
225+
fn __init__(
226+
self_: &Bound<'_, Self>,
227+
args: &Bound<'_, PyTuple>,
228+
kwargs: Option<&Bound<'_, PyDict>>,
229+
) -> PyResult<()> {
230+
self_
231+
.py_super()?
232+
.call_method("__init__", args.to_owned(), kwargs)?;
233+
Ok(())
234+
}
235+
}
236+
```
237+
184238
## Adding the class to a module
185239

186240
The next step is to create the module initializer and add our class to it:

guide/src/class/protocols.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
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.
44

55
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:
6-
- `__new__` and `__init__` are replaced by the [`#[new]` attribute](../class.md#constructor).
76
- `__del__` is not yet supported, but may be in the future.
87
- `__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.
98
- PyO3 adds [`__traverse__` and `__clear__`](#garbage-collector-integration) methods for controlling garbage collection.

newsfragments/4951.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added `#[init]` method attribute in `#[pymethods]`.

pyo3-macros-backend/src/method.rs

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ pub enum FnType {
204204
FnNew,
205205
/// Represents a pymethod annotated with both `#[new]` and `#[classmethod]` (in either order)
206206
FnNewClass(Span),
207+
/// Represents a pymethod annotated with `#[init]`, i.e. the `__init__` dunder.
208+
FnInit(SelfType),
207209
/// Represents a pymethod annotated with `#[classmethod]`, like a `@classmethod`
208210
FnClass(Span),
209211
/// Represents a pyfunction or a pymethod annotated with `#[staticmethod]`, like a `@staticmethod`
@@ -220,6 +222,7 @@ impl FnType {
220222
FnType::Getter(_)
221223
| FnType::Setter(_)
222224
| FnType::Fn(_)
225+
| FnType::FnInit(_)
223226
| FnType::FnClass(_)
224227
| FnType::FnNewClass(_)
225228
| FnType::FnModule(_) => true,
@@ -231,6 +234,7 @@ impl FnType {
231234
match self {
232235
FnType::Fn(_)
233236
| FnType::FnNew
237+
| FnType::FnInit(_)
234238
| FnType::FnStatic
235239
| FnType::FnClass(_)
236240
| FnType::FnNewClass(_)
@@ -250,7 +254,7 @@ impl FnType {
250254
) -> Option<TokenStream> {
251255
let Ctx { pyo3_path, .. } = ctx;
252256
match self {
253-
FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) => {
257+
FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) | FnType::FnInit(st) => {
254258
let mut receiver = st.receiver(
255259
cls.expect("no class given for Fn with a \"self\" receiver"),
256260
error_mode,
@@ -378,6 +382,7 @@ pub enum CallingConvention {
378382
Varargs, // METH_VARARGS | METH_KEYWORDS
379383
Fastcall, // METH_FASTCALL | METH_KEYWORDS (not compatible with `abi3` feature before 3.10)
380384
TpNew, // special convention for tp_new
385+
TpInit, // special convention for tp_init
381386
}
382387

383388
impl CallingConvention {
@@ -476,10 +481,10 @@ impl<'a> FnSpec<'a> {
476481
FunctionSignature::from_arguments(arguments)
477482
};
478483

479-
let convention = if matches!(fn_type, FnType::FnNew | FnType::FnNewClass(_)) {
480-
CallingConvention::TpNew
481-
} else {
482-
CallingConvention::from_signature(&signature)
484+
let convention = match fn_type {
485+
FnType::FnNew | FnType::FnNewClass(_) => CallingConvention::TpNew,
486+
FnType::FnInit(_) => CallingConvention::TpInit,
487+
_ => CallingConvention::from_signature(&signature),
483488
};
484489

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

527-
let mut set_name_to_new = || {
528-
if let Some(name) = &python_name {
529-
bail_spanned!(name.span() => "`name` not allowed with `#[new]`");
532+
let mut set_fn_name = |name| {
533+
if let Some(ident) = python_name {
534+
bail_spanned!(ident.span() => format!("`name` not allowed with `#[{name}]`"));
530535
}
531-
*python_name = Some(syn::Ident::new("__new__", Span::call_site()));
536+
*python_name = Some(syn::Ident::new(
537+
format!("__{name}__").as_str(),
538+
Span::call_site(),
539+
));
532540
Ok(())
533541
};
534542

@@ -539,14 +547,18 @@ impl<'a> FnSpec<'a> {
539547
[MethodTypeAttribute::StaticMethod(_)] => FnType::FnStatic,
540548
[MethodTypeAttribute::ClassAttribute(_)] => FnType::ClassAttribute,
541549
[MethodTypeAttribute::New(_)] => {
542-
set_name_to_new()?;
550+
set_fn_name("new")?;
543551
FnType::FnNew
544552
}
545553
[MethodTypeAttribute::New(_), MethodTypeAttribute::ClassMethod(span)]
546554
| [MethodTypeAttribute::ClassMethod(span), MethodTypeAttribute::New(_)] => {
547-
set_name_to_new()?;
555+
set_fn_name("new")?;
548556
FnType::FnNewClass(*span)
549557
}
558+
[MethodTypeAttribute::Init(_)] => {
559+
set_fn_name("init")?;
560+
FnType::FnInit(parse_receiver("expected receiver for `#[init]`")?)
561+
}
550562
[MethodTypeAttribute::ClassMethod(_)] => {
551563
// Add a helpful hint if the classmethod doesn't look like a classmethod
552564
let span = match sig.inputs.first() {
@@ -830,7 +842,6 @@ impl<'a> FnSpec<'a> {
830842
_kwargs: *mut #pyo3_path::ffi::PyObject
831843
) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> {
832844
use #pyo3_path::impl_::callback::IntoPyCallbackOutput;
833-
let function = #rust_name; // Shadow the function name to avoid #3017
834845
#arg_convert
835846
#init_holders
836847
let result = #call;
@@ -839,6 +850,29 @@ impl<'a> FnSpec<'a> {
839850
}
840851
}
841852
}
853+
CallingConvention::TpInit => {
854+
let mut holders = Holders::new();
855+
let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx);
856+
let self_arg = self
857+
.tp
858+
.self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx);
859+
let call = quote_spanned! {*output_span=> #rust_name(#self_arg #(#args),*) };
860+
let init_holders = holders.init_holders(ctx);
861+
quote! {
862+
unsafe fn #ident(
863+
py: #pyo3_path::Python<'_>,
864+
_slf: *mut #pyo3_path::ffi::PyObject,
865+
_args: *mut #pyo3_path::ffi::PyObject,
866+
_kwargs: *mut #pyo3_path::ffi::PyObject
867+
) -> #pyo3_path::PyResult<::std::os::raw::c_int> {
868+
use #pyo3_path::impl_::callback::IntoPyCallbackOutput;
869+
#arg_convert
870+
#init_holders
871+
#call?;
872+
Ok(0)
873+
}
874+
}
875+
}
842876
})
843877
}
844878

@@ -917,6 +951,7 @@ impl<'a> FnSpec<'a> {
917951
)
918952
},
919953
CallingConvention::TpNew => unreachable!("tp_new cannot get a methoddef"),
954+
CallingConvention::TpInit => unreachable!("tp_init cannot get a methoddef"),
920955
}
921956
}
922957

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

951986
enum MethodTypeAttribute {
952987
New(Span),
988+
Init(Span),
953989
ClassMethod(Span),
954990
StaticMethod(Span),
955991
Getter(Span, Option<Ident>),
@@ -961,6 +997,7 @@ impl MethodTypeAttribute {
961997
fn span(&self) -> Span {
962998
match self {
963999
MethodTypeAttribute::New(span)
1000+
| MethodTypeAttribute::Init(span)
9641001
| MethodTypeAttribute::ClassMethod(span)
9651002
| MethodTypeAttribute::StaticMethod(span)
9661003
| MethodTypeAttribute::Getter(span, _)
@@ -1018,6 +1055,9 @@ impl MethodTypeAttribute {
10181055
if path.is_ident("new") {
10191056
ensure_no_arguments(meta, "new")?;
10201057
Ok(Some(MethodTypeAttribute::New(path.span())))
1058+
} else if path.is_ident("init") {
1059+
ensure_no_arguments(meta, "init")?;
1060+
Ok(Some(MethodTypeAttribute::Init(path.span())))
10211061
} else if path.is_ident("classmethod") {
10221062
ensure_no_arguments(meta, "classmethod")?;
10231063
Ok(Some(MethodTypeAttribute::ClassMethod(path.span())))
@@ -1043,6 +1083,7 @@ impl Display for MethodTypeAttribute {
10431083
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10441084
match self {
10451085
MethodTypeAttribute::New(_) => "#[new]".fmt(f),
1086+
MethodTypeAttribute::Init(_) => "#[init]".fmt(f),
10461087
MethodTypeAttribute::ClassMethod(_) => "#[classmethod]".fmt(f),
10471088
MethodTypeAttribute::StaticMethod(_) => "#[staticmethod]".fmt(f),
10481089
MethodTypeAttribute::Getter(_, _) => "#[getter]".fmt(f),

pyo3-macros-backend/src/pymethod.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ pub fn gen_py_method(
252252
(_, FnType::FnNew) | (_, FnType::FnNewClass(_)) => {
253253
GeneratedPyMethod::Proto(impl_py_method_def_new(cls, spec, ctx)?)
254254
}
255+
(_, FnType::FnInit(_)) => {
256+
GeneratedPyMethod::Proto(impl_py_method_def_init(cls, spec, ctx)?)
257+
}
255258

256259
(_, FnType::Getter(self_type)) => GeneratedPyMethod::Method(impl_py_getter_def(
257260
cls,
@@ -393,6 +396,56 @@ pub fn impl_py_method_def_new(
393396
})
394397
}
395398

399+
pub fn impl_py_method_def_init(
400+
cls: &syn::Type,
401+
spec: &FnSpec<'_>,
402+
ctx: &Ctx,
403+
) -> Result<MethodAndSlotDef> {
404+
let Ctx { pyo3_path, .. } = ctx;
405+
let wrapper_ident = syn::Ident::new("__pymethod___init____", Span::call_site());
406+
let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls), ctx)?;
407+
// Use just the text_signature_call_signature() because the class' Python name
408+
// isn't known to `#[pymethods]` - that has to be attached at runtime from the PyClassImpl
409+
// trait implementation created by `#[pyclass]`.
410+
let text_signature_body = spec.text_signature_call_signature().map_or_else(
411+
|| quote!(::std::option::Option::None),
412+
|text_signature| quote!(::std::option::Option::Some(#text_signature)),
413+
);
414+
let slot_def = quote! {
415+
#pyo3_path::ffi::PyType_Slot {
416+
slot: #pyo3_path::ffi::Py_tp_init,
417+
pfunc: {
418+
unsafe extern "C" fn trampoline(
419+
slf: *mut #pyo3_path::ffi::PyObject,
420+
args: *mut #pyo3_path::ffi::PyObject,
421+
kwargs: *mut #pyo3_path::ffi::PyObject,
422+
) -> ::std::os::raw::c_int {
423+
use #pyo3_path::impl_::pyclass::*;
424+
#[allow(unknown_lints, non_local_definitions)]
425+
impl PyClassInitTextSignature<#cls> for PyClassImplCollector<#cls> {
426+
#[inline]
427+
fn init_text_signature(self) -> ::std::option::Option<&'static str> {
428+
#text_signature_body
429+
}
430+
}
431+
432+
#pyo3_path::impl_::trampoline::initproc(
433+
slf,
434+
args,
435+
kwargs,
436+
#cls::#wrapper_ident
437+
)
438+
}
439+
trampoline
440+
} as #pyo3_path::ffi::initproc as _
441+
}
442+
};
443+
Ok(MethodAndSlotDef {
444+
associated_method,
445+
slot_def,
446+
})
447+
}
448+
396449
fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec<'_>, ctx: &Ctx) -> Result<MethodAndSlotDef> {
397450
let Ctx { pyo3_path, .. } = ctx;
398451

pyo3-macros/src/lib.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,7 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
112112
#[doc = concat!("[11]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#object-properties-using-pyo3get-set")]
113113
#[proc_macro_attribute]
114114
pub fn pymethods(attr: TokenStream, input: TokenStream) -> TokenStream {
115-
let methods_type = if cfg!(feature = "multiple-pymethods") {
116-
PyClassMethodsType::Inventory
117-
} else {
118-
PyClassMethodsType::Specialization
119-
};
120-
pymethods_impl(attr, input, methods_type)
115+
pymethods_impl(attr, input, methods_type())
121116
}
122117

123118
/// A proc macro used to expose Rust functions to Python.

0 commit comments

Comments
 (0)