diff --git a/guide/pyclass_parameters.md b/guide/pyclass_parameters.md
index c5006890fbb..7826960e6c1 100644
--- a/guide/pyclass_parameters.md
+++ b/guide/pyclass_parameters.md
@@ -7,10 +7,12 @@
| `extends = BaseType` | Use a custom baseclass. Defaults to [`PyAny`][params-1] |
| `freelist = N` | Implements a [free list][params-2] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. |
| `frozen` | Declares that your pyclass is immutable. It removes the borrowchecker overhead when retrieving a shared reference to the Rust struct, but disables the ability to get a mutable reference. |
+| `get_all` | Generates getters for all fields of the pyclass. |
| `mapping` | Inform PyO3 that this class is a [`Mapping`][params-mapping], and so leave its implementation of sequence C-API slots empty. |
| `module = "module_name"` | Python code will see the class as being defined in this module. Defaults to `builtins`. |
| `name = "python_name"` | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. |
| `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. |
| `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. |
| `text_signature = "(arg1, arg2, ...)"` | Sets the text signature for the Python class' `__new__` method. |
| `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread.|
diff --git a/newsfragments/2692.added.md b/newsfragments/2692.added.md
new file mode 100644
index 00000000000..d6e45677907
--- /dev/null
+++ b/newsfragments/2692.added.md
@@ -0,0 +1 @@
+The `#[pyclass]` macro can now take `get_all` and `set_all` to create getters and setters for every field.
\ No newline at end of file
diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs
index b4d45390dbb..cf35f20a156 100644
--- a/pyo3-macros-backend/src/attributes.rs
+++ b/pyo3-macros-backend/src/attributes.rs
@@ -18,6 +18,7 @@ pub mod kw {
syn::custom_keyword!(frozen);
syn::custom_keyword!(gc);
syn::custom_keyword!(get);
+ syn::custom_keyword!(get_all);
syn::custom_keyword!(item);
syn::custom_keyword!(mapping);
syn::custom_keyword!(module);
@@ -25,6 +26,7 @@ pub mod kw {
syn::custom_keyword!(pass_module);
syn::custom_keyword!(sequence);
syn::custom_keyword!(set);
+ syn::custom_keyword!(set_all);
syn::custom_keyword!(signature);
syn::custom_keyword!(subclass);
syn::custom_keyword!(text_signature);
diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs
index d318b5a156f..e15900b14ed 100644
--- a/pyo3-macros-backend/src/pyclass.rs
+++ b/pyo3-macros-backend/src/pyclass.rs
@@ -60,12 +60,14 @@ pub struct PyClassPyO3Options {
pub krate: Option,
pub dict: Option,
pub extends: Option,
+ pub get_all: Option,
pub freelist: Option,
pub frozen: Option,
pub mapping: Option,
pub module: Option,
pub name: Option,
pub sequence: Option,
+ pub set_all: Option,
pub subclass: Option,
pub text_signature: Option,
pub unsendable: Option,
@@ -80,10 +82,12 @@ enum PyClassPyO3Option {
Extends(ExtendsAttribute),
Freelist(FreelistAttribute),
Frozen(kw::frozen),
+ GetAll(kw::get_all),
Mapping(kw::mapping),
Module(ModuleAttribute),
Name(NameAttribute),
Sequence(kw::sequence),
+ SetAll(kw::set_all),
Subclass(kw::subclass),
TextSignature(TextSignatureAttribute),
Unsendable(kw::unsendable),
@@ -105,6 +109,8 @@ impl Parse for PyClassPyO3Option {
input.parse().map(PyClassPyO3Option::Freelist)
} else if lookahead.peek(attributes::kw::frozen) {
input.parse().map(PyClassPyO3Option::Frozen)
+ } else if lookahead.peek(attributes::kw::get_all) {
+ input.parse().map(PyClassPyO3Option::GetAll)
} else if lookahead.peek(attributes::kw::mapping) {
input.parse().map(PyClassPyO3Option::Mapping)
} else if lookahead.peek(attributes::kw::module) {
@@ -113,6 +119,8 @@ impl Parse for PyClassPyO3Option {
input.parse().map(PyClassPyO3Option::Name)
} else if lookahead.peek(attributes::kw::sequence) {
input.parse().map(PyClassPyO3Option::Sequence)
+ } else if lookahead.peek(attributes::kw::set_all) {
+ input.parse().map(PyClassPyO3Option::SetAll)
} else if lookahead.peek(attributes::kw::subclass) {
input.parse().map(PyClassPyO3Option::Subclass)
} else if lookahead.peek(attributes::kw::text_signature) {
@@ -165,10 +173,12 @@ impl PyClassPyO3Options {
PyClassPyO3Option::Extends(extends) => set_option!(extends),
PyClassPyO3Option::Freelist(freelist) => set_option!(freelist),
PyClassPyO3Option::Frozen(frozen) => set_option!(frozen),
+ PyClassPyO3Option::GetAll(get_all) => set_option!(get_all),
PyClassPyO3Option::Mapping(mapping) => set_option!(mapping),
PyClassPyO3Option::Module(module) => set_option!(module),
PyClassPyO3Option::Name(name) => set_option!(name),
PyClassPyO3Option::Sequence(sequence) => set_option!(sequence),
+ PyClassPyO3Option::SetAll(set_all) => set_option!(set_all),
PyClassPyO3Option::Subclass(subclass) => set_option!(subclass),
PyClassPyO3Option::TextSignature(text_signature) => set_option!(text_signature),
PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable),
@@ -212,7 +222,7 @@ pub fn build_py_class(
For an explanation, see https://pyo3.rs/latest/class.html#no-generic-parameters"
);
- let field_options = match &mut class.fields {
+ let mut field_options: Vec<(&syn::Field, FieldPyO3Options)> = match &mut class.fields {
syn::Fields::Named(fields) => fields
.named
.iter_mut()
@@ -230,18 +240,54 @@ pub fn build_py_class(
})
.collect::>()?,
syn::Fields::Unit => {
+ if let Some(attr) = args.options.set_all {
+ return Err(syn::Error::new_spanned(attr, UNIT_SET));
+ };
+ if let Some(attr) = args.options.get_all {
+ return Err(syn::Error::new_spanned(attr, UNIT_GET));
+ };
// No fields for unit struct
Vec::new()
}
};
+ if let Some(attr) = args.options.get_all {
+ for (_, FieldPyO3Options { get, .. }) in &mut field_options {
+ if let Some(old_get) = get.replace(Annotated::Struct(attr)) {
+ return Err(syn::Error::new(old_get.span(), DUPE_GET));
+ }
+ }
+ }
+
+ if let Some(attr) = args.options.set_all {
+ for (_, FieldPyO3Options { set, .. }) in &mut field_options {
+ if let Some(old_set) = set.replace(Annotated::Struct(attr)) {
+ return Err(syn::Error::new(old_set.span(), DUPE_SET));
+ }
+ }
+ }
+
impl_class(&class.ident, &args, doc, field_options, methods_type, krate)
}
+enum Annotated {
+ Field(X),
+ Struct(Y),
+}
+
+impl Spanned for Annotated {
+ fn span(&self) -> Span {
+ match self {
+ Self::Field(x) => x.span(),
+ Self::Struct(y) => y.span(),
+ }
+ }
+}
+
/// `#[pyo3()]` options for pyclass fields
struct FieldPyO3Options {
- get: bool,
- set: bool,
+ get: Option>,
+ set: Option>,
name: Option,
}
@@ -269,33 +315,27 @@ impl Parse for FieldPyO3Option {
impl FieldPyO3Options {
fn take_pyo3_options(attrs: &mut Vec) -> Result {
let mut options = FieldPyO3Options {
- get: false,
- set: false,
+ get: None,
+ set: None,
name: None,
};
for option in take_pyo3_options(attrs)? {
match option {
FieldPyO3Option::Get(kw) => {
- ensure_spanned!(
- !options.get,
- kw.span() => "`get` may only be specified once"
- );
- options.get = true;
+ if options.get.replace(Annotated::Field(kw)).is_some() {
+ return Err(syn::Error::new(kw.span(), UNIQUE_GET));
+ }
}
FieldPyO3Option::Set(kw) => {
- ensure_spanned!(
- !options.set,
- kw.span() => "`set` may only be specified once"
- );
- options.set = true;
+ if options.set.replace(Annotated::Field(kw)).is_some() {
+ return Err(syn::Error::new(kw.span(), UNIQUE_SET));
+ }
}
FieldPyO3Option::Name(name) => {
- ensure_spanned!(
- options.name.is_none(),
- name.span() => "`name` may only be specified once"
- );
- options.name = Some(name);
+ if options.name.replace(name).is_some() {
+ return Err(syn::Error::new(options.name.span(), UNIQUE_NAME));
+ }
}
}
}
@@ -664,39 +704,42 @@ fn descriptors_to_items(
field_options: Vec<(&syn::Field, FieldPyO3Options)>,
) -> syn::Result> {
let ty = syn::parse_quote!(#cls);
- field_options
- .into_iter()
- .enumerate()
- .flat_map(|(field_index, (field, options))| {
- let name_err = if options.name.is_some() && !options.get && !options.set {
- Some(Err(err_spanned!(options.name.as_ref().unwrap().span() => "`name` is useless without `get` or `set`")))
- } else {
- None
- };
+ let mut items = Vec::new();
+ for (field_index, (field, options)) in field_options.into_iter().enumerate() {
+ if let FieldPyO3Options {
+ name: Some(name),
+ get: None,
+ set: None,
+ } = options
+ {
+ return Err(syn::Error::new_spanned(name, USELESS_NAME));
+ }
- let getter = if options.get {
- Some(impl_py_getter_def(&ty, PropertyType::Descriptor {
+ if options.get.is_some() {
+ let getter = impl_py_getter_def(
+ &ty,
+ PropertyType::Descriptor {
field_index,
field,
- python_name: options.name.as_ref()
- }))
- } else {
- None
- };
+ python_name: options.name.as_ref(),
+ },
+ )?;
+ items.push(getter);
+ }
- let setter = if options.set {
- Some(impl_py_setter_def(&ty, PropertyType::Descriptor {
+ if options.set.is_some() {
+ let setter = impl_py_setter_def(
+ &ty,
+ PropertyType::Descriptor {
field_index,
field,
- python_name: options.name.as_ref()
- }))
- } else {
- None
- };
-
- name_err.into_iter().chain(getter).chain(setter)
- })
- .collect::>()
+ python_name: options.name.as_ref(),
+ },
+ )?;
+ items.push(setter);
+ };
+ }
+ Ok(items)
}
fn impl_pytypeinfo(
@@ -1085,3 +1128,16 @@ fn define_inventory_class(inventory_class_name: &syn::Ident) -> TokenStream {
_pyo3::inventory::collect!(#inventory_class_name);
}
}
+
+const UNIQUE_GET: &str = "`get` may only be specified once";
+const UNIQUE_SET: &str = "`set` may only be specified once";
+const UNIQUE_NAME: &str = "`name` may only be specified once";
+
+const DUPE_SET: &str = "useless `set` - the struct is already annotated with `set_all`";
+const DUPE_GET: &str = "useless `get` - the struct is already annotated with `get_all`";
+const UNIT_GET: &str =
+ "`get_all` on an unit struct does nothing, because unit structs have no fields";
+const UNIT_SET: &str =
+ "`set_all` on an unit struct does nothing, because unit structs have no fields";
+
+const USELESS_NAME: &str = "`name` is useless without `get` or `set`";
diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs
index edd5164f19f..4d5efab7843 100644
--- a/tests/test_compile_error.rs
+++ b/tests/test_compile_error.rs
@@ -117,6 +117,7 @@ fn _test_compile_errors() {
t.compile_fail("tests/ui/not_send.rs");
t.compile_fail("tests/ui/not_send2.rs");
t.compile_fail("tests/ui/not_send3.rs");
+ t.compile_fail("tests/ui/get_set_all.rs");
}
#[rustversion::before(1.63)]
diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs
index 02372995426..0928a8188b8 100644
--- a/tests/test_getter_setter.rs
+++ b/tests/test_getter_setter.rs
@@ -158,3 +158,34 @@ fn tuple_struct_getter_setter() {
py_assert!(py, inst, "inst.num == 20");
});
}
+
+#[pyclass(get_all, set_all)]
+struct All {
+ num: i32,
+}
+
+#[test]
+fn get_set_all() {
+ Python::with_gil(|py| {
+ let inst = Py::new(py, All { num: 10 }).unwrap();
+
+ py_run!(py, inst, "assert inst.num == 10");
+ py_run!(py, inst, "inst.num = 20; assert inst.num == 20");
+ });
+}
+
+#[pyclass(get_all)]
+struct All2 {
+ #[pyo3(set)]
+ num: i32,
+}
+
+#[test]
+fn get_all_and_set() {
+ Python::with_gil(|py| {
+ let inst = Py::new(py, All2 { num: 10 }).unwrap();
+
+ py_run!(py, inst, "assert inst.num == 10");
+ py_run!(py, inst, "inst.num = 20; assert inst.num == 20");
+ });
+}
diff --git a/tests/ui/get_set_all.rs b/tests/ui/get_set_all.rs
new file mode 100644
index 00000000000..20a5eca07bf
--- /dev/null
+++ b/tests/ui/get_set_all.rs
@@ -0,0 +1,21 @@
+use pyo3::prelude::*;
+
+#[pyclass(set_all)]
+struct Foo;
+
+#[pyclass(set_all)]
+struct Foo2{
+ #[pyo3(set)]
+ field: u8,
+}
+
+#[pyclass(get_all)]
+struct Foo3;
+
+#[pyclass(get_all)]
+struct Foo4{
+ #[pyo3(get)]
+ field: u8,
+}
+
+fn main() {}
diff --git a/tests/ui/get_set_all.stderr b/tests/ui/get_set_all.stderr
new file mode 100644
index 00000000000..607d5f96597
--- /dev/null
+++ b/tests/ui/get_set_all.stderr
@@ -0,0 +1,23 @@
+error: `set_all` on an unit struct does nothing, because unit structs have no fields
+ --> tests/ui/get_set_all.rs:3:11
+ |
+3 | #[pyclass(set_all)]
+ | ^^^^^^^
+
+error: useless `set` - the struct is already annotated with `set_all`
+ --> tests/ui/get_set_all.rs:8:12
+ |
+8 | #[pyo3(set)]
+ | ^^^
+
+error: `get_all` on an unit struct does nothing, because unit structs have no fields
+ --> tests/ui/get_set_all.rs:12:11
+ |
+12 | #[pyclass(get_all)]
+ | ^^^^^^^
+
+error: useless `get` - the struct is already annotated with `get_all`
+ --> tests/ui/get_set_all.rs:17:12
+ |
+17 | #[pyo3(get)]
+ | ^^^
diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr
index bf8976a60c0..a41b6c79b3a 100644
--- a/tests/ui/invalid_property_args.stderr
+++ b/tests/ui/invalid_property_args.stderr
@@ -44,4 +44,4 @@ error: `name` is useless without `get` or `set`
--> tests/ui/invalid_property_args.rs:40:33
|
40 | struct NameWithoutGetSet(#[pyo3(name = "value")] i32);
- | ^^^^
+ | ^^^^^^^^^^^^^^
diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr
index c80ae034242..0930123d60b 100644
--- a/tests/ui/invalid_pyclass_args.stderr
+++ b/tests/ui/invalid_pyclass_args.stderr
@@ -1,4 +1,4 @@
-error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `mapping`, `module`, `name`, `sequence`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc`
+error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `sequence`, `set_all`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc`
--> tests/ui/invalid_pyclass_args.rs:3:11
|
3 | #[pyclass(extend=pyo3::types::PyDict)]
@@ -34,7 +34,7 @@ error: expected string literal
18 | #[pyclass(module = my_module)]
| ^^^^^^^^^
-error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `mapping`, `module`, `name`, `sequence`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc`
+error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `sequence`, `set_all`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc`
--> tests/ui/invalid_pyclass_args.rs:21:11
|
21 | #[pyclass(weakrev)]