-
Notifications
You must be signed in to change notification settings - Fork 851
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should discourage users from using |
||||||
|
||||||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
in the constructor method. | ||||||
|
||||||
Like in the constructor case the Rust method name isn't important. | ||||||
|
||||||
```rust,ignore | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Added `#[init]` method attribute in `#[pymethods]`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<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, | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this no longer needed? |
||
#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<Ident>), | ||
|
@@ -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), | ||
|
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.
Please also document that it shall return either
PyResult<()>
or nothing. Ideally the macro would check for this; not sure how feasible that is.