From d011467e637d2df17ce9ee8533480e22f9eb71f6 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Fri, 4 Jun 2021 11:16:25 +0100 Subject: [PATCH] pyclass: allow `#[pyo3(get, set, name = "foo")]` --- guide/src/class.md | 2 +- pyo3-macros-backend/src/attributes.rs | 19 +- pyo3-macros-backend/src/from_pyobject.rs | 6 +- pyo3-macros-backend/src/konst.rs | 6 +- pyo3-macros-backend/src/method.rs | 5 +- pyo3-macros-backend/src/module.rs | 2 +- pyo3-macros-backend/src/pyclass.rs | 214 ++++++++++++++--------- pyo3-macros-backend/src/pyfunction.rs | 12 +- pyo3-macros-backend/src/pymethod.rs | 159 +++++++++++------ tests/test_class_basics.rs | 15 +- tests/test_inheritance.rs | 8 +- tests/ui/invalid_property_args.rs | 14 +- tests/ui/invalid_property_args.stderr | 32 +++- 13 files changed, 327 insertions(+), 167 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 1d024f4f..269db256 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -349,7 +349,7 @@ struct MyClass { } ``` -The above would make the `num` property available for reading and writing from Python code as `self.num`. +The above would make the `num` field available for reading and writing as a `self.num` Python property. To expose the property with a different name to the field, specify this alongside the rest of the options, e.g. `#[pyo3(get, set, name = "custom_name")]`. Properties can be readonly or writeonly by using just `#[pyo3(get)]` or `#[pyo3(set)]` respectively. diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 709d13a4..d10f2f52 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -12,9 +12,11 @@ pub mod kw { syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); syn::custom_keyword!(from_py_with); + syn::custom_keyword!(get); syn::custom_keyword!(item); syn::custom_keyword!(pass_module); syn::custom_keyword!(name); + syn::custom_keyword!(set); syn::custom_keyword!(signature); syn::custom_keyword!(transparent); } @@ -43,9 +45,7 @@ impl Parse for NameAttribute { } } -pub fn get_pyo3_attributes( - attr: &syn::Attribute, -) -> Result>> { +pub fn get_pyo3_options(attr: &syn::Attribute) -> Result>> { if is_attribute_ident(attr, "pyo3") { attr.parse_args_with(Punctuated::parse_terminated).map(Some) } else { @@ -83,6 +83,19 @@ pub fn take_attributes( Ok(()) } +pub fn take_pyo3_options(attrs: &mut Vec) -> Result> { + let mut out = Vec::new(); + take_attributes(attrs, |attr| { + if let Some(options) = get_pyo3_options(attr)? { + out.extend(options.into_iter()); + Ok(true) + } else { + Ok(false) + } + })?; + Ok(out) +} + pub fn get_deprecated_name_attribute( attr: &syn::Attribute, deprecations: &mut Deprecations, diff --git a/pyo3-macros-backend/src/from_pyobject.rs b/pyo3-macros-backend/src/from_pyobject.rs index ed0c6387..e28b10d4 100644 --- a/pyo3-macros-backend/src/from_pyobject.rs +++ b/pyo3-macros-backend/src/from_pyobject.rs @@ -1,4 +1,4 @@ -use crate::attributes::{self, get_pyo3_attributes, FromPyWithAttribute}; +use crate::attributes::{self, get_pyo3_options, FromPyWithAttribute}; use proc_macro2::TokenStream; use quote::quote; use syn::{ @@ -290,7 +290,7 @@ impl ContainerOptions { annotation: None, }; for attr in attrs { - if let Some(pyo3_attrs) = get_pyo3_attributes(attr)? { + if let Some(pyo3_attrs) = get_pyo3_options(attr)? { for pyo3_attr in pyo3_attrs { match pyo3_attr { ContainerPyO3Attribute::Transparent(kw) => { @@ -388,7 +388,7 @@ impl FieldPyO3Attributes { let mut from_py_with = None; for attr in attrs { - if let Some(pyo3_attrs) = get_pyo3_attributes(attr)? { + if let Some(pyo3_attrs) = get_pyo3_options(attr)? { for pyo3_attr in pyo3_attrs { match pyo3_attr { FieldPyO3Attribute::Getter(field_getter) => { diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index 1e9203bc..ddece4f0 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -1,7 +1,7 @@ use crate::{ attributes::{ - self, get_deprecated_name_attribute, get_pyo3_attributes, is_attribute_ident, - take_attributes, NameAttribute, + self, get_deprecated_name_attribute, get_pyo3_options, is_attribute_ident, take_attributes, + NameAttribute, }, deprecations::Deprecations, }; @@ -69,7 +69,7 @@ impl ConstAttributes { ); attributes.is_class_attr = true; Ok(true) - } else if let Some(pyo3_attributes) = get_pyo3_attributes(attr)? { + } else if let Some(pyo3_attributes) = get_pyo3_options(attr)? { for pyo3_attr in pyo3_attributes { match pyo3_attr { PyO3ConstAttribute::Name(name) => attributes.set_name(name)?, diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 36dbfc5f..56e96f43 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -220,9 +220,8 @@ impl<'a> FnSpec<'a> { }) } - pub fn null_terminated_python_name(&self) -> TokenStream { - let name = format!("{}\0", self.python_name); - quote!({#name}) + pub fn null_terminated_python_name(&self) -> syn::LitStr { + syn::LitStr::new(&format!("{}\0", self.python_name), self.python_name.span()) } fn parse_text_signature( diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 911436a3..04f6694d 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -114,7 +114,7 @@ fn get_pyfn_attr(attrs: &mut Vec) -> syn::Result syn::parse::Result { + fn parse(input: ParseStream) -> Result { let mut slf = PyClassArgs::default(); let vars = Punctuated::::parse_terminated(input)?; @@ -57,7 +57,7 @@ impl Default for PyClassArgs { impl PyClassArgs { /// Adda single expression from the comma separated list in the attribute, which is /// either a single word or an assignment expression - fn add_expr(&mut self, expr: &Expr) -> syn::parse::Result<()> { + fn add_expr(&mut self, expr: &Expr) -> Result<()> { match expr { syn::Expr::Path(exp) if exp.path.segments.len() == 1 => self.add_path(exp), syn::Expr::Assign(assign) => self.add_assign(assign), @@ -172,63 +172,102 @@ pub fn build_py_class( &get_class_python_name(&class.ident, attr), )?; let doc = utils::get_doc(&class.attrs, text_signature, true)?; - let mut descriptors = Vec::new(); ensure_spanned!( class.generics.params.is_empty(), class.generics.span() => "#[pyclass] cannot have generic parameters" ); - match &mut class.fields { - syn::Fields::Named(fields) => { - for field in fields.named.iter_mut() { - let field_descs = parse_descriptors(field)?; - if !field_descs.is_empty() { - descriptors.push((field.clone(), field_descs)); - } - } + let field_options = match &mut class.fields { + syn::Fields::Named(fields) => fields + .named + .iter_mut() + .map(|field| { + FieldPyO3Options::take_pyo3_options(&mut field.attrs) + .map(move |options| (&*field, options)) + }) + .collect::>()?, + syn::Fields::Unnamed(fields) => fields + .unnamed + .iter_mut() + .map(|field| { + FieldPyO3Options::take_pyo3_options(&mut field.attrs) + .map(move |options| (&*field, options)) + }) + .collect::>()?, + syn::Fields::Unit => { + // No fields for unit struct + Vec::new() } - syn::Fields::Unnamed(fields) => { - for field in fields.unnamed.iter_mut() { - let field_descs = parse_descriptors(field)?; - if !field_descs.is_empty() { - descriptors.push((field.clone(), field_descs)); - } - } - } - syn::Fields::Unit => { /* No fields for unit struct */ } - } + }; - impl_class(&class.ident, &attr, doc, descriptors, methods_type) + impl_class(&class.ident, &attr, doc, field_options, methods_type) } -/// Parses `#[pyo3(get, set)]` -fn parse_descriptors(item: &mut syn::Field) -> syn::Result> { - let mut descs = Vec::new(); - let mut new_attrs = Vec::new(); - for attr in item.attrs.drain(..) { - if let Ok(syn::Meta::List(list)) = attr.parse_meta() { - if list.path.is_ident("pyo3") { - for meta in list.nested.iter() { - if let syn::NestedMeta::Meta(metaitem) = meta { - if metaitem.path().is_ident("get") { - descs.push(FnType::Getter(SelfType::Receiver { mutable: false })); - } else if metaitem.path().is_ident("set") { - descs.push(FnType::Setter(SelfType::Receiver { mutable: true })); - } else { - bail_spanned!(metaitem.span() => "only get and set are supported"); - } - } - } - } else { - new_attrs.push(attr) - } +/// `#[pyo3()]` options for pyclass fields +struct FieldPyO3Options { + get: bool, + set: bool, + name: Option, +} + +enum FieldPyO3Option { + Get(attributes::kw::get), + Set(attributes::kw::set), + Name(NameAttribute), +} + +impl Parse for FieldPyO3Option { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::get) { + input.parse().map(FieldPyO3Option::Get) + } else if lookahead.peek(attributes::kw::set) { + input.parse().map(FieldPyO3Option::Set) + } else if lookahead.peek(attributes::kw::name) { + input.parse().map(FieldPyO3Option::Name) } else { - new_attrs.push(attr); + Err(lookahead.error()) } } - item.attrs = new_attrs; - Ok(descs) +} + +impl FieldPyO3Options { + fn take_pyo3_options(attrs: &mut Vec) -> Result { + let mut options = FieldPyO3Options { + get: false, + set: false, + 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; + } + FieldPyO3Option::Set(kw) => { + ensure_spanned!( + !options.set, + kw.span() => "`set` may only be specified once" + ); + options.set = true; + } + FieldPyO3Option::Name(name) => { + ensure_spanned!( + options.name.is_none(), + name.0.span() => "`name` may only be specified once" + ); + options.name = Some(name); + } + } + } + + Ok(options) + } } /// To allow multiple #[pymethods] block, we define inventory types. @@ -267,12 +306,12 @@ fn impl_class( cls: &syn::Ident, attr: &PyClassArgs, doc: syn::LitStr, - descriptors: Vec<(syn::Field, Vec)>, + field_options: Vec<(&syn::Field, FieldPyO3Options)>, methods_type: PyClassMethodsType, ) -> syn::Result { let cls_name = get_class_python_name(cls, attr).to_string(); - let extra = { + let alloc = { if let Some(freelist) = &attr.freelist { quote! { impl pyo3::freelist::PyClassWithFreeList for #cls { @@ -296,17 +335,7 @@ fn impl_class( } }; - let extra = if !descriptors.is_empty() { - let path = syn::Path::from(syn::PathSegment::from(cls.clone())); - let ty = syn::Type::from(syn::TypePath { path, qself: None }); - let desc_impls = impl_descriptors(&ty, descriptors)?; - quote! { - #desc_impls - #extra - } - } else { - extra - }; + let descriptors = impl_descriptors(cls, field_options)?; // insert space for weak ref let weakref = if attr.has_weaklist { @@ -481,39 +510,50 @@ fn impl_class( } } - #extra + #alloc + + #descriptors #gc_impl }) } fn impl_descriptors( - cls: &syn::Type, - descriptors: Vec<(syn::Field, Vec)>, + cls: &syn::Ident, + field_options: Vec<(&syn::Field, FieldPyO3Options)>, ) -> syn::Result { - let py_methods: Vec = descriptors - .iter() - .flat_map(|(field, fns)| { - fns.iter() - .map(|desc| { - let doc = utils::get_doc(&field.attrs, None, true) - .unwrap_or_else(|_| syn::LitStr::new("", Span::call_site())); - let property_type = PropertyType::Descriptor( - field.ident.as_ref().ok_or_else( - || err_spanned!(field.span() => "`#[pyo3(get, set)]` is not supported on tuple struct fields") - )? - ); - match desc { - FnType::Getter(self_ty) => { - impl_py_getter_def(cls, property_type, self_ty, &doc, &Default::default()) - } - FnType::Setter(self_ty) => { - impl_py_setter_def(cls, property_type, self_ty, &doc, &Default::default()) - } - _ => unreachable!(), - } - }) - .collect::>>() + let ty = syn::parse_quote!(#cls); + let py_methods: Vec = 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().0.span() => "`name` is useless without `get` or `set`"))) + } else { + None + }; + + let getter = if options.get { + Some(impl_py_getter_def(&ty, PropertyType::Descriptor { + field_index, + field, + python_name: options.name.as_ref() + })) + } else { + None + }; + + let setter = if options.set { + Some(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::>()?; diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index c1851e69..a6a90c6b 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -2,7 +2,7 @@ use crate::{ attributes::{ - self, get_deprecated_name_attribute, get_pyo3_attributes, take_attributes, + self, get_deprecated_name_attribute, get_pyo3_options, take_attributes, FromPyWithAttribute, NameAttribute, }, deprecations::Deprecations, @@ -62,7 +62,7 @@ impl PyFunctionArgPyO3Attributes { pub fn from_attrs(attrs: &mut Vec) -> syn::Result { let mut attributes = PyFunctionArgPyO3Attributes { from_py_with: None }; take_attributes(attrs, |attr| { - if let Some(pyo3_attrs) = get_pyo3_attributes(attr)? { + if let Some(pyo3_attrs) = get_pyo3_options(attr)? { for attr in pyo3_attrs { match attr { PyFunctionArgPyO3Attribute::FromPyWith(from_py_with) => { @@ -270,13 +270,13 @@ impl Parse for PyFunctionOption { impl PyFunctionOptions { pub fn from_attrs(attrs: &mut Vec) -> syn::Result { let mut options = PyFunctionOptions::default(); - options.take_pyo3_attributes(attrs)?; + options.take_pyo3_options(attrs)?; Ok(options) } - pub fn take_pyo3_attributes(&mut self, attrs: &mut Vec) -> syn::Result<()> { + pub fn take_pyo3_options(&mut self, attrs: &mut Vec) -> syn::Result<()> { take_attributes(attrs, |attr| { - if let Some(pyo3_attributes) = get_pyo3_attributes(attr)? { + if let Some(pyo3_attributes) = get_pyo3_options(attr)? { self.add_attributes(pyo3_attributes)?; Ok(true) } else if let Some(name) = get_deprecated_name_attribute(attr, &mut self.deprecations)? @@ -332,7 +332,7 @@ pub fn build_py_function( ast: &mut syn::ItemFn, mut options: PyFunctionOptions, ) -> syn::Result { - options.take_pyo3_attributes(&mut ast.attrs)?; + options.take_pyo3_options(&mut ast.attrs)?; Ok(impl_wrap_pyfunction(ast, options)?.1) } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 5748d01c..d5626c7a 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1,3 +1,6 @@ +use std::borrow::Cow; + +use crate::attributes::NameAttribute; use crate::utils::ensure_not_async_fn; // Copyright (c) 2017-present PyO3 Project and Contributors use crate::{attributes::FromPyWithAttribute, konst::ConstSpec}; @@ -10,12 +13,6 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned}; use syn::{ext::IdentExt, spanned::Spanned, Result}; -#[derive(Clone, Copy)] -pub enum PropertyType<'a> { - Descriptor(&'a syn::Ident), - Function(&'a FnSpec<'a>), -} - pub enum GeneratedPyMethod { Method(TokenStream), New(TokenStream), @@ -45,19 +42,19 @@ pub fn gen_py_method( FnType::ClassAttribute => { GeneratedPyMethod::Method(impl_py_method_class_attribute(cls, &spec)) } - FnType::Getter(self_ty) => GeneratedPyMethod::Method(impl_py_getter_def( + FnType::Getter(self_type) => GeneratedPyMethod::Method(impl_py_getter_def( cls, - PropertyType::Function(&spec), - self_ty, - &spec.doc, - &spec.deprecations, + PropertyType::Function { + self_type, + spec: &spec, + }, )?), - FnType::Setter(self_ty) => GeneratedPyMethod::Method(impl_py_setter_def( + FnType::Setter(self_type) => GeneratedPyMethod::Method(impl_py_setter_def( cls, - PropertyType::Function(&spec), - self_ty, - &spec.doc, - &spec.deprecations, + PropertyType::Function { + self_type, + spec: &spec, + }, )?), }) } @@ -260,17 +257,30 @@ fn impl_call_getter(cls: &syn::Type, spec: &FnSpec) -> syn::Result /// Generate a function wrapper called `__wrap` for a property getter pub(crate) fn impl_wrap_getter( cls: &syn::Type, - property_type: PropertyType, - self_ty: &SelfType, + property_type: &PropertyType, ) -> syn::Result { - let getter_impl = match &property_type { - PropertyType::Descriptor(ident) => { + let getter_impl = match property_type { + PropertyType::Descriptor { + field: syn::Field { + ident: Some(ident), .. + }, + .. + } => { + // named struct field quote!(_slf.#ident.clone()) } - PropertyType::Function(spec) => impl_call_getter(cls, spec)?, + PropertyType::Descriptor { field_index, .. } => { + // tuple struct field + let index = syn::Index::from(*field_index); + quote!(_slf.#index.clone()) + } + PropertyType::Function { spec, .. } => impl_call_getter(cls, spec)?, }; - let slf = self_ty.receiver(cls); + let slf = match property_type { + PropertyType::Descriptor { .. } => SelfType::Receiver { mutable: false }.receiver(cls), + PropertyType::Function { self_type, .. } => self_type.receiver(cls), + }; Ok(quote! {{ unsafe extern "C" fn __wrap( _slf: *mut pyo3::ffi::PyObject, _: *mut std::os::raw::c_void) -> *mut pyo3::ffi::PyObject @@ -309,17 +319,30 @@ fn impl_call_setter(cls: &syn::Type, spec: &FnSpec) -> syn::Result /// Generate a function wrapper called `__wrap` for a property setter pub(crate) fn impl_wrap_setter( cls: &syn::Type, - property_type: PropertyType, - self_ty: &SelfType, + property_type: &PropertyType, ) -> syn::Result { - let setter_impl = match &property_type { - PropertyType::Descriptor(ident) => { + let setter_impl = match property_type { + PropertyType::Descriptor { + field: syn::Field { + ident: Some(ident), .. + }, + .. + } => { + // named struct field quote!({ _slf.#ident = _val; }) } - PropertyType::Function(spec) => impl_call_setter(cls, spec)?, + PropertyType::Descriptor { field_index, .. } => { + // tuple struct field + let index = syn::Index::from(*field_index); + quote!({ _slf.#index = _val; }) + } + PropertyType::Function { spec, .. } => impl_call_setter(cls, spec)?, }; - let slf = self_ty.receiver(cls); + let slf = match property_type { + PropertyType::Descriptor { .. } => SelfType::Receiver { mutable: true }.receiver(cls), + PropertyType::Function { self_type, .. } => self_type.receiver(cls), + }; Ok(quote! {{ #[allow(unused_mut)] unsafe extern "C" fn __wrap( @@ -707,18 +730,11 @@ pub fn impl_py_method_def_call( pub(crate) fn impl_py_setter_def( cls: &syn::Type, property_type: PropertyType, - self_ty: &SelfType, - doc: &syn::LitStr, - deprecations: &Deprecations, ) -> Result { - let python_name = match property_type { - PropertyType::Descriptor(ident) => { - let formatted_name = format!("{}\0", ident.unraw()); - quote!(#formatted_name) - } - PropertyType::Function(spec) => spec.null_terminated_python_name(), - }; - let wrapper = impl_wrap_setter(cls, property_type, self_ty)?; + let python_name = property_type.null_terminated_python_name()?; + let deprecations = property_type.deprecations(); + let doc = property_type.doc(); + let wrapper = impl_wrap_setter(cls, &property_type)?; Ok(quote! { pyo3::class::PyMethodDefType::Setter({ #deprecations @@ -734,18 +750,11 @@ pub(crate) fn impl_py_setter_def( pub(crate) fn impl_py_getter_def( cls: &syn::Type, property_type: PropertyType, - self_ty: &SelfType, - doc: &syn::LitStr, - deprecations: &Deprecations, ) -> Result { - let python_name = match property_type { - PropertyType::Descriptor(ident) => { - let formatted_name = format!("{}\0", ident.unraw()); - quote!(#formatted_name) - } - PropertyType::Function(spec) => spec.null_terminated_python_name(), - }; - let wrapper = impl_wrap_getter(cls, property_type, self_ty)?; + let python_name = property_type.null_terminated_python_name()?; + let deprecations = property_type.deprecations(); + let doc = property_type.doc(); + let wrapper = impl_wrap_getter(cls, &property_type)?; Ok(quote! { pyo3::class::PyMethodDefType::Getter({ #deprecations @@ -770,3 +779,53 @@ fn split_off_python_arg<'a>(args: &'a [FnArg<'a>]) -> (Option<&FnArg>, &[FnArg]) (None, args) } } + +pub enum PropertyType<'a> { + Descriptor { + field_index: usize, + field: &'a syn::Field, + python_name: Option<&'a NameAttribute>, + }, + Function { + self_type: &'a SelfType, + spec: &'a FnSpec<'a>, + }, +} + +impl PropertyType<'_> { + fn null_terminated_python_name(&self) -> Result { + match self { + PropertyType::Descriptor { + field, python_name, .. + } => { + let name = match (python_name, &field.ident) { + (Some(name), _) => name.0.to_string(), + (None, Some(field_name)) => format!("{}\0", field_name.unraw()), + (None, None) => { + bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`") + } + }; + Ok(syn::LitStr::new(&name, field.span())) + } + PropertyType::Function { spec, .. } => Ok(spec.null_terminated_python_name()), + } + } + + fn deprecations(&self) -> Option<&Deprecations> { + match self { + PropertyType::Descriptor { .. } => None, + PropertyType::Function { spec, .. } => Some(&spec.deprecations), + } + } + + fn doc(&self) -> Cow { + match self { + PropertyType::Descriptor { field, .. } => { + let doc = utils::get_doc(&field.attrs, None, true) + .unwrap_or_else(|_| syn::LitStr::new("", Span::call_site())); + Cow::Owned(doc) + } + PropertyType::Function { spec, .. } => Cow::Borrowed(&spec.doc), + } + } +} diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 21d7990e..193a216b 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -317,7 +317,7 @@ fn test_pymethods_from_py_with() { } #[pyclass] -struct TupleClass(i32); +struct TupleClass(#[pyo3(get, set, name = "value")] i32); #[test] fn test_tuple_struct_class() { @@ -326,5 +326,18 @@ fn test_tuple_struct_class() { assert!(typeobj.call((), None).is_err()); py_assert!(py, typeobj, "typeobj.__name__ == 'TupleClass'"); + + let instance = Py::new(py, TupleClass(5)).unwrap(); + py_run!( + py, + instance, + r#" + assert instance.value == 5; + instance.value = 1234; + assert instance.value == 1234; + "# + ); + + assert_eq!(instance.borrow(py).0, 1234); }); } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index c9d1df2b..1e8a7fbb 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -159,7 +159,7 @@ mod inheriting_native_type { #[pyclass(extends=PySet)] #[derive(Debug)] struct SetWithName { - #[pyo3(get(name))] + #[pyo3(get, name = "name")] _name: &'static str, } @@ -179,14 +179,14 @@ mod inheriting_native_type { py_run!( py, set_sub, - r#"set_sub.add(10); assert list(set_sub) == [10]; assert set_sub._name == "Hello :)""# + r#"set_sub.add(10); assert list(set_sub) == [10]; assert set_sub.name == "Hello :)""# ); } #[pyclass(extends=PyDict)] #[derive(Debug)] struct DictWithName { - #[pyo3(get(name))] + #[pyo3(get, name = "name")] _name: &'static str, } @@ -206,7 +206,7 @@ mod inheriting_native_type { py_run!( py, dict_sub, - r#"dict_sub[0] = 1; assert dict_sub[0] == 1; assert dict_sub._name == "Hello :)""# + r#"dict_sub[0] = 1; assert dict_sub[0] == 1; assert dict_sub.name == "Hello :)""# ); } diff --git a/tests/ui/invalid_property_args.rs b/tests/ui/invalid_property_args.rs index 20dc1218..c1049946 100644 --- a/tests/ui/invalid_property_args.rs +++ b/tests/ui/invalid_property_args.rs @@ -25,6 +25,18 @@ impl ClassWithSetter { } #[pyclass] -struct TupleGetterSetter(#[pyo3(get, set)] i32); +struct TupleGetterSetterNoName(#[pyo3(get, set)] i32); + +#[pyclass] +struct MultipleGet(#[pyo3(get, get)] i32); + +#[pyclass] +struct MultipleSet(#[pyo3(set, set)] i32); + +#[pyclass] +struct MultipleName(#[pyo3(name = "foo", name = "bar")] i32); + +#[pyclass] +struct NameWithoutGetSet(#[pyo3(name = "value")] i32); fn main() {} diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index 15ad11e8..5e2b40cc 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -16,8 +16,32 @@ error: setter function can have at most two arguments ([pyo3::Python,] and value 24 | fn setter_with_too_many_args(&mut self, py: Python, foo: u32, bar: u32) {} | ^^^ -error: `#[pyo3(get, set)]` is not supported on tuple struct fields - --> $DIR/invalid_property_args.rs:28:44 +error: `get` and `set` with tuple struct fields require `name` + --> $DIR/invalid_property_args.rs:28:50 | -28 | struct TupleGetterSetter(#[pyo3(get, set)] i32); - | ^^^ +28 | struct TupleGetterSetterNoName(#[pyo3(get, set)] i32); + | ^^^ + +error: `get` may only be specified once + --> $DIR/invalid_property_args.rs:31:32 + | +31 | struct MultipleGet(#[pyo3(get, get)] i32); + | ^^^ + +error: `set` may only be specified once + --> $DIR/invalid_property_args.rs:34:32 + | +34 | struct MultipleSet(#[pyo3(set, set)] i32); + | ^^^ + +error: `name` may only be specified once + --> $DIR/invalid_property_args.rs:37:49 + | +37 | struct MultipleName(#[pyo3(name = "foo", name = "bar")] i32); + | ^^^^^ + +error: `name` is useless without `get` or `set` + --> $DIR/invalid_property_args.rs:40:40 + | +40 | struct NameWithoutGetSet(#[pyo3(name = "value")] i32); + | ^^^^^^^