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. |
| `new = "from_fields"` | Generates a default `__new__` constructor with all fields as parameters in the `new()` method. |
| `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 `new = "from_fields"` attribute for `#[pyclass]`
29 changes: 29 additions & 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!(new);
syn::custom_keyword!(signature);
syn::custom_keyword!(str);
syn::custom_keyword!(subclass);
Expand Down Expand Up @@ -311,13 +312,41 @@ impl ToTokens for TextSignatureAttributeValue {
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NewImplTypeAttributeValue {
FromFields,
// Future variant for 'default' should go here
}

impl Parse for NewImplTypeAttributeValue {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let string_literal: LitStr = input.parse()?;
if string_literal.value().as_str() == "from_fields" {
Ok(NewImplTypeAttributeValue::FromFields)
} else {
bail_spanned!(string_literal.span() => "expected \"from_fields\"")
}
}
}

impl ToTokens for NewImplTypeAttributeValue {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
NewImplTypeAttributeValue::FromFields => {
tokens.extend(quote! { "from_fields" });
}
}
}
}

pub type ExtendsAttribute = KeywordAttribute<kw::extends, Path>;
pub type FreelistAttribute = KeywordAttribute<kw::freelist, Box<Expr>>;
pub type ModuleAttribute = KeywordAttribute<kw::module, LitStr>;
pub type NameAttribute = KeywordAttribute<kw::name, NameLitStr>;
pub type RenameAllAttribute = KeywordAttribute<kw::rename_all, RenamingRuleLitStr>;
pub type StrFormatterAttribute = OptionalKeywordAttribute<kw::str, StringFormatter>;
pub type TextSignatureAttribute = KeywordAttribute<kw::text_signature, TextSignatureAttributeValue>;
pub type NewImplTypeAttribute = KeywordAttribute<kw::new, NewImplTypeAttributeValue>;
pub type SubmoduleAttribute = kw::submodule;
pub type GILUsedAttribute = KeywordAttribute<kw::gil_used, LitBool>;

Expand Down
91 changes: 90 additions & 1 deletion pyo3-macros-backend/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, ImplItemFn, Result
use crate::attributes::kw::frozen;
use crate::attributes::{
self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute,
ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, StrFormatterAttribute,
ModuleAttribute, NameAttribute, NameLitStr, NewImplTypeAttribute, NewImplTypeAttributeValue,
RenameAllAttribute, StrFormatterAttribute,
};
use crate::combine_errors::CombineErrors;
#[cfg(feature = "experimental-inspect")]
Expand Down Expand Up @@ -85,6 +86,7 @@ pub struct PyClassPyO3Options {
pub rename_all: Option<RenameAllAttribute>,
pub sequence: Option<kw::sequence>,
pub set_all: Option<kw::set_all>,
pub new: Option<NewImplTypeAttribute>,
pub str: Option<StrFormatterAttribute>,
pub subclass: Option<kw::subclass>,
pub unsendable: Option<kw::unsendable>,
Expand Down Expand Up @@ -112,6 +114,7 @@ pub enum PyClassPyO3Option {
RenameAll(RenameAllAttribute),
Sequence(kw::sequence),
SetAll(kw::set_all),
New(NewImplTypeAttribute),
Str(StrFormatterAttribute),
Subclass(kw::subclass),
Unsendable(kw::unsendable),
Expand Down Expand Up @@ -158,6 +161,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::new) {
input.parse().map(PyClassPyO3Option::New)
} 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 +245,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::New(new) => set_option!(new),
PyClassPyO3Option::Str(str) => set_option!(str),
PyClassPyO3Option::Subclass(subclass) => set_option!(subclass),
PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable),
Expand Down Expand Up @@ -468,6 +474,13 @@ fn impl_class(
}
}

let (default_new, default_new_slot) = pyclass_new_impl(
&args.options,
&syn::parse_quote!(#cls),
field_options.iter().map(|(f, _)| f),
ctx,
)?;

let mut default_methods = descriptors_to_items(
cls,
args.options.rename_all.as_ref(),
Expand Down Expand Up @@ -496,6 +509,7 @@ fn impl_class(
slots.extend(default_richcmp_slot);
slots.extend(default_hash_slot);
slots.extend(default_str_slot);
slots.extend(default_new_slot);

let py_class_impl = PyClassImplsBuilder::new(cls, args, methods_type, default_methods, slots)
.doc(doc)
Expand All @@ -514,6 +528,7 @@ fn impl_class(
#default_richcmp
#default_hash
#default_str
#default_new
#default_class_getitem
}
})
Expand Down Expand Up @@ -2226,6 +2241,80 @@ fn pyclass_hash(
}
}

fn pyclass_new_impl<'a>(
options: &PyClassPyO3Options,
ty: &syn::Type,
fields: impl Iterator<Item = &'a &'a syn::Field>,
ctx: &Ctx,
) -> Result<(Option<ImplItemFn>, Option<MethodAndSlotDef>)> {
if options
.new
.as_ref()
.is_some_and(|o| matches!(o.value, NewImplTypeAttributeValue::FromFields))
{
ensure_spanned!(
options.extends.is_none(), options.new.span() => "The `new=\"from_fields\"` option cannot be used with `extends`.";
);
}

match &options.new {
Some(opt) => {
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);
}

let mut new_impl = {
parse_quote_spanned! { opt.span() =>
fn __pyo3_generated____new__( #( #field_idents : #field_types ),* ) -> Self {
Self {
#( #field_idents, )*
}
}
}
};

let new_slot = generate_protocol_slot(
ty,
&mut new_impl,
&__NEW__,
"__new__",
#[cfg(feature = "experimental-inspect")]
FunctionIntrospectionData {
names: &["__new__"],
arguments: field_idents
.iter()
.zip(field_types.iter())
.map(|(ident, ty)| {
FnArg::Regular(RegularArg {
name: Cow::Owned(ident.clone()),
ty,
from_py_with: None,
default_value: None,
option_wrapped_type: None,
annotation: None,
})
})
.collect(),
returns: ty.clone(),
},
ctx,
)
.unwrap();

Ok((Some(new_impl), Some(new_slot)))
}
None => Ok((None, None)),
}
}

fn pyclass_class_getitem(
options: &PyClassPyO3Options,
cls: &syn::Type,
Expand Down
20 changes: 20 additions & 0 deletions tests/test_class_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,26 @@ fn test_renaming_all_struct_fields() {
});
}

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

#[test]
fn new_impl() {
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"
);
});
}

macro_rules! test_case {
($struct_name: ident, $rule: literal, $field_name: ident, $renamed_field_name: literal, $test_name: ident) => {
#[pyclass(get_all, set_all, rename_all = $rule)]
Expand Down
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`, `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`, `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