Skip to content

Commit 10175ac

Browse files
committed
update new impl macro
1 parent 9f652c0 commit 10175ac

File tree

6 files changed

+116
-67
lines changed

6 files changed

+116
-67
lines changed

guide/pyclass-parameters.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
| `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". |
2323
| `sequence` | Inform PyO3 that this class is a [`Sequence`][params-sequence], and so leave its C-API mapping length slot empty. |
2424
| `set_all` | Generates setters for all fields of the pyclass. |
25-
| `auto_new` | Generates a default `__new__` constructor, must be used with `set_all` |
25+
| `new = "from_fields"` | Generates a default `__new__` constructor with all fields as parameters in the `new()` method. |
2626
| `skip_from_py_object` | Prevents this PyClass from participating in the `FromPyObject: PyClass + Clone` blanket implementation. This allows a custom `FromPyObject` impl, even if `self` is `Clone`. |
2727
| `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).* |
2828
| `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. |

newsfragments/5421.added.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Implement `auto_new` attribute for `#[pyclass]`
1+
Implement `new = "from_fields"` attribute for `#[pyclass]`

pyo3-macros-backend/src/attributes.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +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);
43+
syn::custom_keyword!(new);
4444
syn::custom_keyword!(signature);
4545
syn::custom_keyword!(str);
4646
syn::custom_keyword!(subclass);
@@ -312,13 +312,41 @@ impl ToTokens for TextSignatureAttributeValue {
312312
}
313313
}
314314

315+
#[derive(Clone, Debug, PartialEq, Eq)]
316+
pub enum NewImplTypeAttributeValue {
317+
FromFields,
318+
// Future variant for 'default' should go here
319+
}
320+
321+
impl Parse for NewImplTypeAttributeValue {
322+
fn parse(input: ParseStream<'_>) -> Result<Self> {
323+
let string_literal: LitStr = input.parse()?;
324+
if string_literal.value().as_str() == "from_fields" {
325+
Ok(NewImplTypeAttributeValue::FromFields)
326+
} else {
327+
bail_spanned!(string_literal.span() => "expected \"from_fields\"")
328+
}
329+
}
330+
}
331+
332+
impl ToTokens for NewImplTypeAttributeValue {
333+
fn to_tokens(&self, tokens: &mut TokenStream) {
334+
match self {
335+
NewImplTypeAttributeValue::FromFields => {
336+
tokens.extend(quote! { "from_fields" });
337+
}
338+
}
339+
}
340+
}
341+
315342
pub type ExtendsAttribute = KeywordAttribute<kw::extends, Path>;
316343
pub type FreelistAttribute = KeywordAttribute<kw::freelist, Box<Expr>>;
317344
pub type ModuleAttribute = KeywordAttribute<kw::module, LitStr>;
318345
pub type NameAttribute = KeywordAttribute<kw::name, NameLitStr>;
319346
pub type RenameAllAttribute = KeywordAttribute<kw::rename_all, RenamingRuleLitStr>;
320347
pub type StrFormatterAttribute = OptionalKeywordAttribute<kw::str, StringFormatter>;
321348
pub type TextSignatureAttribute = KeywordAttribute<kw::text_signature, TextSignatureAttributeValue>;
349+
pub type NewImplTypeAttribute = KeywordAttribute<kw::new, NewImplTypeAttributeValue>;
322350
pub type SubmoduleAttribute = kw::submodule;
323351
pub type GILUsedAttribute = KeywordAttribute<kw::gil_used, LitBool>;
324352

pyo3-macros-backend/src/pyclass.rs

Lines changed: 65 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, ImplItemFn, Result
1111
use crate::attributes::kw::frozen;
1212
use crate::attributes::{
1313
self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute,
14-
ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, StrFormatterAttribute,
14+
ModuleAttribute, NameAttribute, NameLitStr, NewImplTypeAttribute, NewImplTypeAttributeValue,
15+
RenameAllAttribute, StrFormatterAttribute,
1516
};
1617
use crate::combine_errors::CombineErrors;
1718
#[cfg(feature = "experimental-inspect")]
@@ -85,7 +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>,
88-
pub auto_new: Option<kw::auto_new>,
89+
pub new: Option<NewImplTypeAttribute>,
8990
pub str: Option<StrFormatterAttribute>,
9091
pub subclass: Option<kw::subclass>,
9192
pub unsendable: Option<kw::unsendable>,
@@ -113,7 +114,7 @@ pub enum PyClassPyO3Option {
113114
RenameAll(RenameAllAttribute),
114115
Sequence(kw::sequence),
115116
SetAll(kw::set_all),
116-
AutoNew(kw::auto_new),
117+
New(NewImplTypeAttribute),
117118
Str(StrFormatterAttribute),
118119
Subclass(kw::subclass),
119120
Unsendable(kw::unsendable),
@@ -160,8 +161,8 @@ impl Parse for PyClassPyO3Option {
160161
input.parse().map(PyClassPyO3Option::Sequence)
161162
} else if lookahead.peek(attributes::kw::set_all) {
162163
input.parse().map(PyClassPyO3Option::SetAll)
163-
} else if lookahead.peek(attributes::kw::auto_new) {
164-
input.parse().map(PyClassPyO3Option::AutoNew)
164+
} else if lookahead.peek(attributes::kw::new) {
165+
input.parse().map(PyClassPyO3Option::New)
165166
} else if lookahead.peek(attributes::kw::str) {
166167
input.parse().map(PyClassPyO3Option::Str)
167168
} else if lookahead.peek(attributes::kw::subclass) {
@@ -244,7 +245,7 @@ impl PyClassPyO3Options {
244245
PyClassPyO3Option::RenameAll(rename_all) => set_option!(rename_all),
245246
PyClassPyO3Option::Sequence(sequence) => set_option!(sequence),
246247
PyClassPyO3Option::SetAll(set_all) => set_option!(set_all),
247-
PyClassPyO3Option::AutoNew(auto_new) => set_option!(auto_new),
248+
PyClassPyO3Option::New(new) => set_option!(new),
248249
PyClassPyO3Option::Str(str) => set_option!(str),
249250
PyClassPyO3Option::Subclass(subclass) => set_option!(subclass),
250251
PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable),
@@ -473,11 +474,10 @@ fn impl_class(
473474
}
474475
}
475476

476-
let auto_new = pyclass_auto_new(
477+
let (default_new, default_new_slot) = pyclass_new_impl(
477478
&args.options,
478-
cls,
479+
&syn::parse_quote!(#cls),
479480
field_options.iter().map(|(f, _)| f),
480-
methods_type,
481481
ctx,
482482
)?;
483483

@@ -509,6 +509,7 @@ fn impl_class(
509509
slots.extend(default_richcmp_slot);
510510
slots.extend(default_hash_slot);
511511
slots.extend(default_str_slot);
512+
slots.extend(default_new_slot);
512513

513514
let py_class_impl = PyClassImplsBuilder::new(cls, args, methods_type, default_methods, slots)
514515
.doc(doc)
@@ -521,14 +522,13 @@ fn impl_class(
521522

522523
#py_class_impl
523524

524-
#auto_new
525-
526525
#[doc(hidden)]
527526
#[allow(non_snake_case)]
528527
impl #cls {
529528
#default_richcmp
530529
#default_hash
531530
#default_str
531+
#default_new
532532
#default_class_getitem
533533
}
534534
})
@@ -2241,56 +2241,77 @@ fn pyclass_hash(
22412241
}
22422242
}
22432243

2244-
fn pyclass_auto_new<'a>(
2244+
fn pyclass_new_impl<'a>(
22452245
options: &PyClassPyO3Options,
2246-
cls: &syn::Ident,
2246+
ty: &syn::Type,
22472247
fields: impl Iterator<Item = &'a &'a syn::Field>,
2248-
methods_type: PyClassMethodsType,
22492248
ctx: &Ctx,
2250-
) -> Result<Option<syn::ItemImpl>> {
2251-
if options.auto_new.is_some() {
2249+
) -> Result<(Option<ImplItemFn>, Option<MethodAndSlotDef>)> {
2250+
if options
2251+
.new
2252+
.as_ref()
2253+
.is_some_and(|o| matches!(o.value, NewImplTypeAttributeValue::FromFields))
2254+
{
22522255
ensure_spanned!(
2253-
options.extends.is_none(), options.hash.span() => "The `auto_new` option cannot be used with `extends`.";
2256+
options.extends.is_none(), options.new.span() => "The `new=\"from_fields\"` option cannot be used with `extends`.";
22542257
);
22552258
}
22562259

2257-
match options.auto_new {
2260+
match &options.new {
22582261
Some(opt) => {
2259-
if matches!(methods_type, PyClassMethodsType::Specialization) {
2260-
bail_spanned!(opt.span() => "`auto_new` requires the `multiple-pymethods` feature.");
2262+
let mut field_idents = vec![];
2263+
let mut field_types = vec![];
2264+
for (idx, field) in fields.enumerate() {
2265+
field_idents.push(
2266+
field
2267+
.ident
2268+
.clone()
2269+
.unwrap_or_else(|| format_ident!("_{}", idx)),
2270+
);
2271+
field_types.push(&field.ty);
22612272
}
22622273

2263-
let autonew_impl = {
2264-
let Ctx { pyo3_path, .. } = ctx;
2265-
let mut field_idents = vec![];
2266-
let mut field_types = vec![];
2267-
for (idx, field) in fields.enumerate() {
2268-
field_idents.push(
2269-
field
2270-
.ident
2271-
.clone()
2272-
.unwrap_or_else(|| format_ident!("_{}", idx)),
2273-
);
2274-
field_types.push(&field.ty);
2275-
}
2276-
2274+
let mut new_impl = {
22772275
parse_quote_spanned! { opt.span() =>
2278-
#[#pyo3_path::pymethods]
2279-
impl #cls {
2280-
#[new]
2281-
fn _pyo3_generated_new( #( #field_idents : #field_types ),* ) -> Self {
2282-
Self {
2283-
#( #field_idents, )*
2284-
}
2276+
fn __pyo3_generated____new__( #( #field_idents : #field_types ),* ) -> Self {
2277+
Self {
2278+
#( #field_idents, )*
22852279
}
22862280
}
2287-
22882281
}
22892282
};
22902283

2291-
Ok(Some(autonew_impl))
2284+
let new_slot = generate_protocol_slot(
2285+
ty,
2286+
&mut new_impl,
2287+
&__NEW__,
2288+
"__new__",
2289+
#[cfg(feature = "experimental-inspect")]
2290+
FunctionIntrospectionData {
2291+
names: &["__new__"],
2292+
arguments: field_idents
2293+
.iter()
2294+
.zip(field_types.iter())
2295+
.map(|(ident, ty)| {
2296+
FnArg::Regular(RegularArg {
2297+
name: Cow::Owned(ident.clone()),
2298+
ty,
2299+
from_py_with: None,
2300+
default_value: None,
2301+
option_wrapped_type: None,
2302+
annotation: None,
2303+
})
2304+
})
2305+
.collect(),
2306+
returns: ty.clone(),
2307+
},
2308+
ctx,
2309+
)
2310+
.unwrap();
2311+
2312+
Ok((Some(new_impl), Some(new_slot)))
22922313
}
2293-
None => Ok(None),
2314+
None => Ok((None, None)),
22942315
}
22952316
}
22962317

tests/test_class_attributes.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,26 @@ fn test_renaming_all_struct_fields() {
235235
});
236236
}
237237

238+
#[pyclass(get_all, set_all, new = "from_fields")]
239+
struct AutoNewCls {
240+
a: i32,
241+
b: String,
242+
c: Option<f64>,
243+
}
244+
245+
#[test]
246+
fn new_impl() {
247+
Python::attach(|py| {
248+
// python should be able to do AutoNewCls(1, "two", 3.0)
249+
let cls = py.get_type::<AutoNewCls>();
250+
pyo3::py_run!(
251+
py,
252+
cls,
253+
"inst = cls(1, 'two', 3.0); assert inst.a == 1; assert inst.b == 'two'; assert inst.c == 3.0"
254+
);
255+
});
256+
}
257+
238258
macro_rules! test_case {
239259
($struct_name: ident, $rule: literal, $field_name: ident, $renamed_field_name: literal, $test_name: ident) => {
240260
#[pyclass(get_all, set_all, rename_all = $rule)]

tests/test_multiple_pymethods.rs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,23 +73,3 @@ 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)