Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions guide/pyclass-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
| `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". |
| `sequence` | Inform PyO3 that this class is a [`Sequence`][params-sequence], and so leave its C-API mapping length slot empty. |
| `set_all` | Generates setters for all fields of the pyclass. |
| `auto_new` | Generates a default `__new__` constructor, must be used with `set_all` |
| `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`. |
| `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).* |
| `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. |
Expand Down
1 change: 1 addition & 0 deletions newsfragments/5421.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement `auto_new` attribute for `#[pyclass]`
1 change: 1 addition & 0 deletions pyo3-macros-backend/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub mod kw {
syn::custom_keyword!(sequence);
syn::custom_keyword!(set);
syn::custom_keyword!(set_all);
syn::custom_keyword!(auto_new);
syn::custom_keyword!(signature);
syn::custom_keyword!(str);
syn::custom_keyword!(subclass);
Expand Down
68 changes: 68 additions & 0 deletions pyo3-macros-backend/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ pub struct PyClassPyO3Options {
pub rename_all: Option<RenameAllAttribute>,
pub sequence: Option<kw::sequence>,
pub set_all: Option<kw::set_all>,
pub auto_new: Option<kw::auto_new>,
pub str: Option<StrFormatterAttribute>,
pub subclass: Option<kw::subclass>,
pub unsendable: Option<kw::unsendable>,
Expand Down Expand Up @@ -112,6 +113,7 @@ pub enum PyClassPyO3Option {
RenameAll(RenameAllAttribute),
Sequence(kw::sequence),
SetAll(kw::set_all),
AutoNew(kw::auto_new),
Str(StrFormatterAttribute),
Subclass(kw::subclass),
Unsendable(kw::unsendable),
Expand Down Expand Up @@ -158,6 +160,8 @@ impl Parse for PyClassPyO3Option {
input.parse().map(PyClassPyO3Option::Sequence)
} else if lookahead.peek(attributes::kw::set_all) {
input.parse().map(PyClassPyO3Option::SetAll)
} else if lookahead.peek(attributes::kw::auto_new) {
input.parse().map(PyClassPyO3Option::AutoNew)
} else if lookahead.peek(attributes::kw::str) {
input.parse().map(PyClassPyO3Option::Str)
} else if lookahead.peek(attributes::kw::subclass) {
Expand Down Expand Up @@ -240,6 +244,7 @@ impl PyClassPyO3Options {
PyClassPyO3Option::RenameAll(rename_all) => set_option!(rename_all),
PyClassPyO3Option::Sequence(sequence) => set_option!(sequence),
PyClassPyO3Option::SetAll(set_all) => set_option!(set_all),
PyClassPyO3Option::AutoNew(auto_new) => set_option!(auto_new),
PyClassPyO3Option::Str(str) => set_option!(str),
PyClassPyO3Option::Subclass(subclass) => set_option!(subclass),
PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable),
Expand Down Expand Up @@ -466,6 +471,14 @@ fn impl_class(
}
}

let auto_new = pyclass_auto_new(
&args.options,
cls,
field_options.iter().map(|(f, _)| f),
methods_type,
ctx,
)?;

let mut default_methods = descriptors_to_items(
cls,
args.options.rename_all.as_ref(),
Expand Down Expand Up @@ -506,6 +519,8 @@ fn impl_class(

#py_class_impl

#auto_new

#[doc(hidden)]
#[allow(non_snake_case)]
impl #cls {
Expand Down Expand Up @@ -2233,6 +2248,59 @@ fn pyclass_hash(
}
}

fn pyclass_auto_new<'a>(
options: &PyClassPyO3Options,
cls: &syn::Ident,
fields: impl Iterator<Item = &'a &'a syn::Field>,
methods_type: PyClassMethodsType,
ctx: &Ctx,
) -> Result<Option<syn::ItemImpl>> {
if options.auto_new.is_some() {
ensure_spanned!(
options.extends.is_none(), options.hash.span() => "The `auto_new` option cannot be used with `extends`.";
);
}

match options.auto_new {
Some(opt) => {
if matches!(methods_type, PyClassMethodsType::Specialization) {
bail_spanned!(opt.span() => "`auto_new` requires the `multiple-pymethods` feature.");
}
Comment on lines +2266 to +2268
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this restriction (at the cost of a slightly more complicated implementation) - see generate_protocol_slot and how it's used for __str__.


let autonew_impl = {
let Ctx { pyo3_path, .. } = ctx;
let mut field_idents = vec![];
let mut field_types = vec![];
for (idx, field) in fields.enumerate() {
field_idents.push(
field
.ident
.clone()
.unwrap_or_else(|| format_ident!("_{}", idx)),
);
field_types.push(&field.ty);
}

parse_quote_spanned! { opt.span() =>
#[#pyo3_path::pymethods]
impl #cls {
#[new]
fn _pyo3_generated_new( #( #field_idents : #field_types ),* ) -> Self {
Self {
#( #field_idents, )*
}
}
}

}
};

Ok(Some(autonew_impl))
}
None => Ok(None),
}
}

fn pyclass_class_geitem(
options: &PyClassPyO3Options,
cls: &syn::Type,
Expand Down
20 changes: 20 additions & 0 deletions tests/test_multiple_pymethods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,23 @@ fn test_class_with_multiple_pymethods() {
py_assert!(py, cls, "cls.CLASS_ATTRIBUTE == 'CLASS_ATTRIBUTE'");
})
}

#[pyclass(get_all, set_all, auto_new)]
struct AutoNewCls {
a: i32,
b: String,
c: Option<f64>,
}

#[test]
fn auto_new() {
Python::attach(|py| {
// python should be able to do AutoNewCls(1, "two", 3.0)
let cls = py.get_type::<AutoNewCls>();
pyo3::py_run!(
py,
cls,
"inst = cls(1, 'two', 3.0); assert inst.a == 1; assert inst.b == 'two'; assert inst.c == 3.0"
);
});
}
4 changes: 2 additions & 2 deletions tests/ui/invalid_pyclass_args.stderr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref`, `generic`, `from_py_object`, `skip_from_py_object`
error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `auto_new`, `str`, `subclass`, `unsendable`, `weakref`, `generic`, `from_py_object`, `skip_from_py_object`
--> tests/ui/invalid_pyclass_args.rs:4:11
|
4 | #[pyclass(extend=pyo3::types::PyDict)]
Expand Down Expand Up @@ -46,7 +46,7 @@ error: expected string literal
25 | #[pyclass(module = my_module)]
| ^^^^^^^^^

error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref`, `generic`, `from_py_object`, `skip_from_py_object`
error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `auto_new`, `str`, `subclass`, `unsendable`, `weakref`, `generic`, `from_py_object`, `skip_from_py_object`
--> tests/ui/invalid_pyclass_args.rs:28:11
|
28 | #[pyclass(weakrev)]
Expand Down
Loading