Skip to content

Commit a39d8f1

Browse files
committed
implement auto_new on pyclasses
1 parent a4a202d commit a39d8f1

File tree

4 files changed

+90
-0
lines changed

4 files changed

+90
-0
lines changed

guide/pyclass-parameters.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
| `rename_all = "renaming_rule"` | Applies renaming rules to every getters and setters of a struct, or every variants of an enum. Possible values are: "camelCase", "kebab-case", "lowercase", "PascalCase", "SCREAMING-KEBAB-CASE", "SCREAMING_SNAKE_CASE", "snake_case", "UPPERCASE". |
2222
| `sequence` | Inform PyO3 that this class is a [`Sequence`][params-sequence], and so leave its C-API mapping length slot empty. |
2323
| `set_all` | Generates setters for all fields of the pyclass. |
24+
| `auto_new` | Generates a default `__new__` constructor, must be used with `set_all` |
2425
| `str` | Implements `__str__` using the `Display` implementation of the underlying Rust datatype or by passing an optional format string `str="<format string>"`. *Note: The optional format string is only allowed for structs. `name` and `rename_all` are incompatible with the optional format string. Additional details can be found in the discussion on this [PR](https://github.com/PyO3/pyo3/pull/4233).* |
2526
| `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. |
2627
| `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a thread-safe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread. Also note the Python's GC is multi-threaded and while unsendable classes will not be traversed on foreign threads to avoid UB, this can lead to memory leaks. |

pyo3-macros-backend/src/attributes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub mod kw {
4040
syn::custom_keyword!(sequence);
4141
syn::custom_keyword!(set);
4242
syn::custom_keyword!(set_all);
43+
syn::custom_keyword!(auto_new);
4344
syn::custom_keyword!(signature);
4445
syn::custom_keyword!(str);
4546
syn::custom_keyword!(subclass);

pyo3-macros-backend/src/pyclass.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use syn::parse::{Parse, ParseStream};
88
use syn::punctuated::Punctuated;
99
use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, ImplItemFn, Result, Token};
1010

11+
use crate::PyFunctionOptions;
1112
use crate::attributes::kw::frozen;
1213
use crate::attributes::{
1314
self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute,
@@ -85,6 +86,7 @@ pub struct PyClassPyO3Options {
8586
pub rename_all: Option<RenameAllAttribute>,
8687
pub sequence: Option<kw::sequence>,
8788
pub set_all: Option<kw::set_all>,
89+
pub auto_new: Option<kw::auto_new>,
8890
pub str: Option<StrFormatterAttribute>,
8991
pub subclass: Option<kw::subclass>,
9092
pub unsendable: Option<kw::unsendable>,
@@ -110,6 +112,7 @@ pub enum PyClassPyO3Option {
110112
RenameAll(RenameAllAttribute),
111113
Sequence(kw::sequence),
112114
SetAll(kw::set_all),
115+
AutoNew(kw::auto_new),
113116
Str(StrFormatterAttribute),
114117
Subclass(kw::subclass),
115118
Unsendable(kw::unsendable),
@@ -154,6 +157,8 @@ impl Parse for PyClassPyO3Option {
154157
input.parse().map(PyClassPyO3Option::Sequence)
155158
} else if lookahead.peek(attributes::kw::set_all) {
156159
input.parse().map(PyClassPyO3Option::SetAll)
160+
} else if lookahead.peek(attributes::kw::auto_new) {
161+
input.parse().map(PyClassPyO3Option::AutoNew)
157162
} else if lookahead.peek(attributes::kw::str) {
158163
input.parse().map(PyClassPyO3Option::Str)
159164
} else if lookahead.peek(attributes::kw::subclass) {
@@ -232,6 +237,7 @@ impl PyClassPyO3Options {
232237
PyClassPyO3Option::RenameAll(rename_all) => set_option!(rename_all),
233238
PyClassPyO3Option::Sequence(sequence) => set_option!(sequence),
234239
PyClassPyO3Option::SetAll(set_all) => set_option!(set_all),
240+
PyClassPyO3Option::AutoNew(auto_new) => set_option!(auto_new),
235241
PyClassPyO3Option::Str(str) => set_option!(str),
236242
PyClassPyO3Option::Subclass(subclass) => set_option!(subclass),
237243
PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable),
@@ -444,6 +450,14 @@ fn impl_class(
444450
}
445451
}
446452

453+
let auto_new = pyclass_auto_new(
454+
&args.options,
455+
cls,
456+
field_options.iter().map(|(f, _)| f),
457+
methods_type,
458+
ctx,
459+
)?;
460+
447461
let mut default_methods = descriptors_to_items(
448462
cls,
449463
args.options.rename_all.as_ref(),
@@ -484,6 +498,8 @@ fn impl_class(
484498

485499
#py_class_impl
486500

501+
#auto_new
502+
487503
#[doc(hidden)]
488504
#[allow(non_snake_case)]
489505
impl #cls {
@@ -2203,6 +2219,58 @@ fn pyclass_hash(
22032219
}
22042220
}
22052221

2222+
fn pyclass_auto_new<'a>(
2223+
options: &PyClassPyO3Options,
2224+
cls: &syn::Ident,
2225+
fields: impl Iterator<Item = &'a &'a syn::Field>,
2226+
methods_type: PyClassMethodsType,
2227+
ctx: &Ctx,
2228+
) -> Result<Option<syn::ItemImpl>> {
2229+
if options.auto_new.is_some() {
2230+
ensure_spanned!(
2231+
options.set_all.is_some(), options.hash.span() => "The `auto_new` option requires the `set_all` option.";
2232+
);
2233+
}
2234+
match options.auto_new {
2235+
Some(opt) => {
2236+
if matches!(methods_type, PyClassMethodsType::Specialization) {
2237+
bail_spanned!(opt.span() => "`auto_new` requires the `multiple-pymethods` feature.");
2238+
}
2239+
2240+
let autonew_impl = {
2241+
let Ctx { pyo3_path, .. } = ctx;
2242+
let mut field_idents = vec![];
2243+
let mut field_types = vec![];
2244+
for (idx, field) in fields.enumerate() {
2245+
field_idents.push(
2246+
field
2247+
.ident
2248+
.clone()
2249+
.unwrap_or_else(|| format_ident!("_{}", idx)),
2250+
);
2251+
field_types.push(&field.ty);
2252+
}
2253+
2254+
parse_quote_spanned! { opt.span() =>
2255+
#[#pyo3_path::pymethods]
2256+
impl #cls {
2257+
#[new]
2258+
fn _pyo3_generated_new( #( #field_idents : #field_types ),* ) -> Self {
2259+
Self {
2260+
#( #field_idents, )*
2261+
}
2262+
}
2263+
}
2264+
2265+
}
2266+
};
2267+
2268+
Ok(Some(autonew_impl))
2269+
}
2270+
None => Ok(None),
2271+
}
2272+
}
2273+
22062274
fn pyclass_class_geitem(
22072275
options: &PyClassPyO3Options,
22082276
cls: &syn::Type,

tests/test_multiple_pymethods.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,23 @@ fn test_class_with_multiple_pymethods() {
7373
py_assert!(py, cls, "cls.CLASS_ATTRIBUTE == 'CLASS_ATTRIBUTE'");
7474
})
7575
}
76+
77+
#[pyclass(get_all, set_all, auto_new)]
78+
struct AutoNewCls {
79+
a: i32,
80+
b: String,
81+
c: Option<f64>,
82+
}
83+
84+
#[test]
85+
fn auto_new() {
86+
Python::attach(|py| {
87+
// python should be able to do AutoNewCls(1, "two", 3.0)
88+
let cls = py.get_type::<AutoNewCls>();
89+
pyo3::py_run!(
90+
py,
91+
cls,
92+
"inst = cls(1, 'two', 3.0); assert inst.a == 1; assert inst.b == 'two'; assert inst.c == 3.0"
93+
);
94+
});
95+
}

0 commit comments

Comments
 (0)