Implement get/set all on pyclass

This commit is contained in:
mejrs 2022-10-17 02:37:43 +02:00
parent c9b26f57cd
commit d254134154
8 changed files with 123 additions and 3 deletions

View file

@ -7,10 +7,12 @@
| <span style="white-space: pre">`extends = BaseType`</span> | Use a custom baseclass. Defaults to [`PyAny`][params-1] |
| <span style="white-space: pre">`freelist = N`</span> | 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. |
| <span style="white-space: pre">`frozen`</span> | 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. |
| <span style="white-space: pre">`module = "module_name"`</span> | Python code will see the class as being defined in this module. Defaults to `builtins`. |
| <span style="white-space: pre">`name = "python_name"`</span> | 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. |
| <span style="white-space: pre">`text_signature = "(arg1, arg2, ...)"`</span> | 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.|

View file

@ -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);

View file

@ -1,6 +1,7 @@
// Copyright (c) 2017-present PyO3 Project and Contributors
use std::borrow::Cow;
use std::mem;
use crate::attributes::{
self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute,
@ -60,12 +61,14 @@ pub struct PyClassPyO3Options {
pub krate: Option<CrateAttribute>,
pub dict: Option<kw::dict>,
pub extends: Option<ExtendsAttribute>,
pub get_all: Option<kw::get_all>,
pub freelist: Option<FreelistAttribute>,
pub frozen: Option<kw::frozen>,
pub mapping: Option<kw::mapping>,
pub module: Option<ModuleAttribute>,
pub name: Option<NameAttribute>,
pub sequence: Option<kw::sequence>,
pub set_all: Option<kw::set_all>,
pub subclass: Option<kw::subclass>,
pub text_signature: Option<TextSignatureAttribute>,
pub unsendable: Option<kw::unsendable>,
@ -80,10 +83,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 +110,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 +120,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 +174,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 +223,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,11 +241,33 @@ pub fn build_py_class(
})
.collect::<Result<_>>()?,
syn::Fields::Unit => {
if let Some(attr) = args.options.set_all {
return Err(syn::Error::new(attr.span(), UNIT_SET));
};
if let Some(attr) = args.options.get_all {
return Err(syn::Error::new(attr.span(), 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 mem::replace(get, true) {
return Err(syn::Error::new(attr.span(), DUPE_GET));
}
}
}
if let Some(attr) = args.options.set_all {
for (_, FieldPyO3Options { set, .. }) in &mut field_options {
if mem::replace(set, true) {
return Err(syn::Error::new(attr.span(), DUPE_SET));
}
}
}
impl_class(&class.ident, &args, doc, field_options, methods_type, krate)
}
@ -1085,3 +1118,10 @@ fn define_inventory_class(inventory_class_name: &syn::Ident) -> TokenStream {
_pyo3::inventory::collect!(#inventory_class_name);
}
}
const DUPE_SET: &str = "duplicate `set` - the struct is already annotated with `set_all`";
const DUPE_GET: &str = "duplicate `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";

View file

@ -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)]

View file

@ -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");
});
}

21
tests/ui/get_set_all.rs Normal file
View file

@ -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() {}

View file

@ -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: duplicate `set` - the struct is already annotated with `set_all`
--> tests/ui/get_set_all.rs:6:11
|
6 | #[pyclass(set_all)]
| ^^^^^^^
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: duplicate `get` - the struct is already annotated with `get_all`
--> tests/ui/get_set_all.rs:15:11
|
15 | #[pyclass(get_all)]
| ^^^^^^^

View file

@ -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)]