diff --git a/guide/src/class.md b/guide/src/class.md index 543435a3de9..676e572ed75 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -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) @@ -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)] @@ -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. + +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 +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 +in the constructor method. + +Like in the constructor case the Rust method name isn't important. + +```rust,ignore +# #![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 { + 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: diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 8a361a1442e..c362faf39a2 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -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. diff --git a/newsfragments/4951.added.md b/newsfragments/4951.added.md new file mode 100644 index 00000000000..815e089b740 --- /dev/null +++ b/newsfragments/4951.added.md @@ -0,0 +1 @@ +Added `#[init]` method attribute in `#[pymethods]`. diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index a1d7a95df35..a878d81316a 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -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` @@ -220,6 +222,7 @@ impl FnType { FnType::Getter(_) | FnType::Setter(_) | FnType::Fn(_) + | FnType::FnInit(_) | FnType::FnClass(_) | FnType::FnNewClass(_) | FnType::FnModule(_) => true, @@ -231,6 +234,7 @@ impl FnType { match self { FnType::Fn(_) | FnType::FnNew + | FnType::FnInit(_) | FnType::FnStatic | FnType::FnClass(_) | FnType::FnNewClass(_) @@ -250,7 +254,7 @@ impl FnType { ) -> Option { 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, @@ -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 { @@ -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 { @@ -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(()) }; @@ -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() { @@ -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 #arg_convert #init_holders let result = #call; @@ -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) + } + } + } }) } @@ -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"), } } @@ -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, @@ -950,6 +985,7 @@ impl<'a> FnSpec<'a> { enum MethodTypeAttribute { New(Span), + Init(Span), ClassMethod(Span), StaticMethod(Span), Getter(Span, Option), @@ -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, _) @@ -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()))) @@ -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), diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index a94a6ad67ab..c7755cb12a5 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -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, @@ -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 { + 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 { let Ctx { pyo3_path, .. } = ctx; diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 2621bea4c6e..6686c98f341 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -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. diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 3af08c053cc..1b7e3ff696f 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -3,6 +3,8 @@ use std::{thread, time}; use pyo3::exceptions::{PyStopIteration, PyValueError}; use pyo3::prelude::*; use pyo3::types::PyType; +#[cfg(Py_3_8)] +use pyo3::types::{PyDict, PyTuple}; #[pyclass] struct EmptyClass {} @@ -104,6 +106,35 @@ impl ClassWithDict { } } +#[cfg(Py_3_8)] +#[pyclass(extends = PyDict)] +struct SubClassWithInit; + +#[cfg(Py_3_8)] +#[pymethods] +impl SubClassWithInit { + #[new] + #[pyo3(signature = (*args, **kwargs))] + #[allow(unused_variables)] + fn __new__(args: &Bound<'_, PyTuple>, kwargs: Option<&Bound<'_, PyDict>>) -> Self { + 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)?; + self_.as_super().set_item("__init__", true)?; + Ok(()) + } +} + #[pymodule(gil_used = false)] pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; @@ -113,6 +144,8 @@ pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] m.add_class::()?; + #[cfg(Py_3_8)] + m.add_class::()?; Ok(()) } diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index 9f611b634b6..a9fde6c862b 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -121,3 +121,16 @@ def test_dict(): d.foo = 42 assert d.__dict__ == {"foo": 42} + + +def test_class_init_method(): + try: + SubClassWithInit = pyclasses.SubClassWithInit + except AttributeError: + pytest.skip("not defined using Python < 3.8") + + d = SubClassWithInit() + assert d == {"__init__": True} + + d = SubClassWithInit({"a": 1}, b=2) + assert d == {"__init__": True, "a": 1, "b": 2} diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 06ec83d6ff2..0c46cf29d5a 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1042,6 +1042,18 @@ impl PyClassNewTextSignature for &'_ PyClassImplCollector { } } +// Text signature for __init__ +pub trait PyClassInitTextSignature { + fn init_text_signature(self) -> Option<&'static str>; +} + +impl PyClassInitTextSignature for &'_ PyClassImplCollector { + #[inline] + fn init_text_signature(self) -> Option<&'static str> { + None + } +} + // Thread checkers #[doc(hidden)] diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index 7ffad8abdcd..e43524cc15c 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -101,6 +101,12 @@ trampolines!( kwargs: *mut ffi::PyObject, ) -> *mut ffi::PyObject; + pub fn initproc( + slf: *mut ffi::PyObject, + args: *mut ffi::PyObject, + kwargs: *mut ffi::PyObject, + ) -> c_int; + pub fn objobjproc(slf: *mut ffi::PyObject, arg1: *mut ffi::PyObject) -> c_int; pub fn reprfunc(slf: *mut ffi::PyObject) -> *mut ffi::PyObject; diff --git a/tests/ui/invalid_pymethod_names.rs b/tests/ui/invalid_pymethod_names.rs index 1334bcabde4..2596da59ca5 100644 --- a/tests/ui/invalid_pymethod_names.rs +++ b/tests/ui/invalid_pymethod_names.rs @@ -26,6 +26,13 @@ impl TestClass { fn new(&self) -> Self { Self { num: 0 } } } +#[pymethods] +impl TestClass { + #[pyo3(name = "makeinit")] + #[init] + fn init(&self) {} +} + #[pymethods] impl TestClass { #[getter(1)] diff --git a/tests/ui/invalid_pymethod_names.stderr b/tests/ui/invalid_pymethod_names.stderr index 1e7a6f44065..58810031ca7 100644 --- a/tests/ui/invalid_pymethod_names.stderr +++ b/tests/ui/invalid_pymethod_names.stderr @@ -16,14 +16,20 @@ error: `name` not allowed with `#[new]` 24 | #[pyo3(name = "makenew")] | ^^^^^^^^^ +error: `name` not allowed with `#[init]` + --> tests/ui/invalid_pymethod_names.rs:31:19 + | +31 | #[pyo3(name = "makeinit")] + | ^^^^^^^^^^ + error: expected ident or string literal for property name - --> tests/ui/invalid_pymethod_names.rs:31:14 + --> tests/ui/invalid_pymethod_names.rs:38:14 | -31 | #[getter(1)] +38 | #[getter(1)] | ^ error: expected `#[getter(name)]` to set the name - --> tests/ui/invalid_pymethod_names.rs:37:14 + --> tests/ui/invalid_pymethod_names.rs:44:14 | -37 | #[getter = 1] +44 | #[getter = 1] | ^