From a9e9ec628d697c2f1c9470487459c7884b6af47a Mon Sep 17 00:00:00 2001 From: DataTriny Date: Sun, 13 Aug 2023 18:25:07 +0200 Subject: [PATCH 1/5] Add attribute to --- guide/pyclass_parameters.md | 1 + newsfragments/3384.added.md | 1 + pyo3-macros-backend/Cargo.toml | 1 + pyo3-macros-backend/src/attributes.rs | 1 + pyo3-macros-backend/src/pyclass.rs | 32 ++++++++++++++++++++++----- 5 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 newsfragments/3384.added.md diff --git a/guide/pyclass_parameters.md b/guide/pyclass_parameters.md index 0ed15be6315..20a43b2b622 100644 --- a/guide/pyclass_parameters.md +++ b/guide/pyclass_parameters.md @@ -11,6 +11,7 @@ | `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. | +| `rename_all` | Renames all variants of the `enum` so that `MyVariant` becomes `MY_VARIANT`. | | `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. | diff --git a/newsfragments/3384.added.md b/newsfragments/3384.added.md new file mode 100644 index 00000000000..7de84fc183b --- /dev/null +++ b/newsfragments/3384.added.md @@ -0,0 +1 @@ +`#[pyclass]` now accepts `rename_all`: this allows renaming all variants of an enum so that `MyVariant` becomes `MY_VARIANT`. \ No newline at end of file diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 8cd1116f921..8a699af2c75 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -16,6 +16,7 @@ edition = "2021" [dependencies] quote = { version = "1", default-features = false } proc-macro2 = { version = "1", default-features = false } +heck = "0.4" [dependencies.syn] version = "2" diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index e51a5a043d3..4b34fad368c 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -26,6 +26,7 @@ pub mod kw { syn::custom_keyword!(module); syn::custom_keyword!(name); syn::custom_keyword!(pass_module); + syn::custom_keyword!(rename_all); syn::custom_keyword!(sequence); syn::custom_keyword!(set); syn::custom_keyword!(set_all); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 2494f562a44..006918a349d 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -16,7 +16,8 @@ use crate::pymethod::{ }; use crate::utils::{self, get_pyo3_crate, PythonDoc}; use crate::PyFunctionOptions; -use proc_macro2::{Span, TokenStream}; +use heck::ToShoutySnakeCase; +use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; @@ -66,6 +67,7 @@ pub struct PyClassPyO3Options { pub mapping: Option, pub module: Option, pub name: Option, + pub rename_all: Option, pub sequence: Option, pub set_all: Option, pub subclass: Option, @@ -86,6 +88,7 @@ enum PyClassPyO3Option { Mapping(kw::mapping), Module(ModuleAttribute), Name(NameAttribute), + RenameAll(kw::rename_all), Sequence(kw::sequence), SetAll(kw::set_all), Subclass(kw::subclass), @@ -115,6 +118,8 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Module) } else if lookahead.peek(kw::name) { input.parse().map(PyClassPyO3Option::Name) + } else if lookahead.peek(attributes::kw::rename_all) { + input.parse().map(PyClassPyO3Option::RenameAll) } else if lookahead.peek(attributes::kw::sequence) { input.parse().map(PyClassPyO3Option::Sequence) } else if lookahead.peek(attributes::kw::set_all) { @@ -173,6 +178,7 @@ impl PyClassPyO3Options { PyClassPyO3Option::Mapping(mapping) => set_option!(mapping), PyClassPyO3Option::Module(module) => set_option!(module), PyClassPyO3Option::Name(name) => set_option!(name), + PyClassPyO3Option::RenameAll(rename_all) => set_option!(rename_all), PyClassPyO3Option::Sequence(sequence) => set_option!(sequence), PyClassPyO3Option::SetAll(set_all) => set_option!(set_all), PyClassPyO3Option::Subclass(subclass) => set_option!(subclass), @@ -203,6 +209,11 @@ pub fn build_py_class( "#[pyclass] cannot have lifetime parameters. \ For an explanation, see https://pyo3.rs/latest/class.html#no-lifetime-parameters" ); + } else if let Some(rename_all) = args.options.rename_all { + bail_spanned!( + rename_all.span() => + "rename_all should only be applied to enums." + ); } ensure_spanned!( @@ -379,12 +390,21 @@ struct PyClassEnumVariant<'a> { } impl<'a> PyClassEnumVariant<'a> { - fn python_name(&self) -> Cow<'_, syn::Ident> { - self.options + fn python_name(&self, args: &PyClassArgs) -> Cow<'_, syn::Ident> { + let name = self + .options .name .as_ref() .map(|name_attr| Cow::Borrowed(&name_attr.value.0)) - .unwrap_or_else(|| Cow::Owned(self.ident.unraw())) + .unwrap_or_else(|| Cow::Owned(self.ident.unraw())); + if args.options.rename_all.is_some() { + Cow::Owned(Ident::new( + &format!("{}", name).to_shouty_snake_case(), + Span::call_site(), + )) + } else { + name + } } } @@ -515,7 +535,7 @@ fn impl_enum( let repr = format!( "{}.{}", get_class_python_name(cls, args), - variant.python_name(), + variant.python_name(args), ); quote! { #cls::#variant_name => #repr, } }); @@ -597,7 +617,7 @@ fn impl_enum( cls, args, methods_type, - enum_default_methods(cls, variants.iter().map(|v| (v.ident, v.python_name()))), + enum_default_methods(cls, variants.iter().map(|v| (v.ident, v.python_name(args)))), default_slots, ) .doc(doc) From 57505cb1a1b0a09509ff9fd005d0f21c57897e54 Mon Sep 17 00:00:00 2001 From: DataTriny Date: Sun, 13 Aug 2023 18:54:55 +0200 Subject: [PATCH 2/5] Fix tests --- tests/ui/invalid_pyclass_args.stderr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 1f45715fe90..48a7961eb08 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`, `get_all`, `mapping`, `module`, `name`, `sequence`, `set_all`, `subclass`, `text_signature`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `text_signature`, `unsendable`, `weakref` --> 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`, `get_all`, `mapping`, `module`, `name`, `sequence`, `set_all`, `subclass`, `text_signature`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `text_signature`, `unsendable`, `weakref` --> tests/ui/invalid_pyclass_args.rs:21:11 | 21 | #[pyclass(weakrev)] From f02fe9478d082bc493d9fcaba6ea7f0dce2a0da3 Mon Sep 17 00:00:00 2001 From: DataTriny Date: Mon, 14 Aug 2023 23:29:44 +0200 Subject: [PATCH 3/5] Make rename_all accept a renaming rule, allow applying it to classes as well --- guide/pyclass_parameters.md | 2 +- newsfragments/3384.added.md | 2 +- pyo3-macros-backend/src/attributes.rs | 50 +++++++++++++++++++++++++++ pyo3-macros-backend/src/pyclass.rs | 47 +++++++++++++------------ pyo3-macros-backend/src/pymethod.rs | 17 +++++++-- pyo3-macros-backend/src/utils.rs | 17 ++++++++- tests/test_class_attributes.rs | 39 +++++++++++++++++++++ tests/test_enum.rs | 23 ++++++++++++ tests/ui/invalid_pyclass_args.rs | 6 ++++ tests/ui/invalid_pyclass_args.stderr | 24 +++++++++---- 10 files changed, 192 insertions(+), 35 deletions(-) diff --git a/guide/pyclass_parameters.md b/guide/pyclass_parameters.md index 20a43b2b622..edc69eb3cf5 100644 --- a/guide/pyclass_parameters.md +++ b/guide/pyclass_parameters.md @@ -11,7 +11,7 @@ | `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. | -| `rename_all` | Renames all variants of the `enum` so that `MyVariant` becomes `MY_VARIANT`. | +| `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. | | `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. | diff --git a/newsfragments/3384.added.md b/newsfragments/3384.added.md index 7de84fc183b..d4732d5d5b3 100644 --- a/newsfragments/3384.added.md +++ b/newsfragments/3384.added.md @@ -1 +1 @@ -`#[pyclass]` now accepts `rename_all`: this allows renaming all variants of an enum so that `MyVariant` becomes `MY_VARIANT`. \ No newline at end of file +`#[pyclass]` now accepts `rename_all = "renaming_rule"`: this allows renaming all getters and setters of a struct, or all variants of an enum. Available renaming rules are: `"camelCase"`, `"kebab-case"`, `"lowercase"`, `"PascalCase"`, `"SCREAMING-KEBAB-CASE"`, `"SCREAMING_SNAKE_CASE"`, `"snake_case"`, `"UPPERCASE"`. diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 4b34fad368c..e5e91cef42c 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -83,6 +83,55 @@ impl ToTokens for NameLitStr { } } +/// Available renaming rules +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RenamingRule { + CamelCase, + KebabCase, + Lowercase, + PascalCase, + ScreamingKebabCase, + ScreamingSnakeCase, + SnakeCase, + Uppercase, +} + +/// A helper type which parses a renaming rule via a literal string +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RenamingRuleLitStr { + pub lit: LitStr, + pub rule: RenamingRule, +} + +impl Parse for RenamingRuleLitStr { + fn parse(input: ParseStream<'_>) -> Result { + let string_literal: LitStr = input.parse()?; + let rule = match string_literal.value().as_ref() { + "camelCase" => RenamingRule::CamelCase, + "kebab-case" => RenamingRule::KebabCase, + "lowercase" => RenamingRule::Lowercase, + "PascalCase" => RenamingRule::PascalCase, + "SCREAMING-KEBAB-CASE" => RenamingRule::ScreamingKebabCase, + "SCREAMING_SNAKE_CASE" => RenamingRule::ScreamingSnakeCase, + "snake_case" => RenamingRule::SnakeCase, + "UPPERCASE" => RenamingRule::Uppercase, + _ => { + bail_spanned!(string_literal.span() => "expected a valid renaming rule, possible values are: \"camelCase\", \"kebab-case\", \"lowercase\", \"PascalCase\", \"SCREAMING-KEBAB-CASE\", \"SCREAMING_SNAKE_CASE\", \"snake_case\", \"UPPERCASE\"") + } + }; + Ok(Self { + lit: string_literal, + rule, + }) + } +} + +impl ToTokens for RenamingRuleLitStr { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.lit.to_tokens(tokens) + } +} + /// Text signatue can be either a literal string or opt-in/out #[derive(Clone, Debug, PartialEq, Eq)] pub enum TextSignatureAttributeValue { @@ -122,6 +171,7 @@ pub type ExtendsAttribute = KeywordAttribute; pub type FreelistAttribute = KeywordAttribute>; pub type ModuleAttribute = KeywordAttribute; pub type NameAttribute = KeywordAttribute; +pub type RenameAllAttribute = KeywordAttribute; pub type TextSignatureAttribute = KeywordAttribute; impl Parse for KeywordAttribute { diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 006918a349d..fe2bc41e196 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use crate::attributes::kw::frozen; use crate::attributes::{ self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, - ModuleAttribute, NameAttribute, NameLitStr, TextSignatureAttribute, + ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, TextSignatureAttribute, TextSignatureAttributeValue, }; use crate::deprecations::{Deprecation, Deprecations}; @@ -14,9 +14,8 @@ use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, SlotDef, __INT__, __REPR__, __RICHCMP__, }; -use crate::utils::{self, get_pyo3_crate, PythonDoc}; +use crate::utils::{self, apply_renaming_rule, get_pyo3_crate, PythonDoc}; use crate::PyFunctionOptions; -use heck::ToShoutySnakeCase; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::ext::IdentExt; @@ -67,7 +66,7 @@ pub struct PyClassPyO3Options { pub mapping: Option, pub module: Option, pub name: Option, - pub rename_all: Option, + pub rename_all: Option, pub sequence: Option, pub set_all: Option, pub subclass: Option, @@ -88,7 +87,7 @@ enum PyClassPyO3Option { Mapping(kw::mapping), Module(ModuleAttribute), Name(NameAttribute), - RenameAll(kw::rename_all), + RenameAll(RenameAllAttribute), Sequence(kw::sequence), SetAll(kw::set_all), Subclass(kw::subclass), @@ -118,7 +117,7 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Module) } else if lookahead.peek(kw::name) { input.parse().map(PyClassPyO3Option::Name) - } else if lookahead.peek(attributes::kw::rename_all) { + } else if lookahead.peek(kw::rename_all) { input.parse().map(PyClassPyO3Option::RenameAll) } else if lookahead.peek(attributes::kw::sequence) { input.parse().map(PyClassPyO3Option::Sequence) @@ -209,11 +208,6 @@ pub fn build_py_class( "#[pyclass] cannot have lifetime parameters. \ For an explanation, see https://pyo3.rs/latest/class.html#no-lifetime-parameters" ); - } else if let Some(rename_all) = args.options.rename_all { - bail_spanned!( - rename_all.span() => - "rename_all should only be applied to enums." - ); } ensure_spanned!( @@ -367,7 +361,12 @@ fn impl_class( cls, args, methods_type, - descriptors_to_items(cls, args.options.frozen, field_options)?, + descriptors_to_items( + cls, + args.options.rename_all.as_ref(), + args.options.frozen, + field_options, + )?, vec![], ) .doc(doc) @@ -391,20 +390,19 @@ struct PyClassEnumVariant<'a> { impl<'a> PyClassEnumVariant<'a> { fn python_name(&self, args: &PyClassArgs) -> Cow<'_, syn::Ident> { - let name = self - .options + self.options .name .as_ref() .map(|name_attr| Cow::Borrowed(&name_attr.value.0)) - .unwrap_or_else(|| Cow::Owned(self.ident.unraw())); - if args.options.rename_all.is_some() { - Cow::Owned(Ident::new( - &format!("{}", name).to_shouty_snake_case(), - Span::call_site(), - )) - } else { - name - } + .unwrap_or_else(|| { + let name = self.ident.unraw(); + if let Some(attr) = &args.options.rename_all { + let new_name = apply_renaming_rule(attr.value.rule, &format!("{}", name)); + Cow::Owned(Ident::new(&new_name, Span::call_site())) + } else { + Cow::Owned(name) + } + }) } } @@ -695,6 +693,7 @@ fn extract_variant_data(variant: &mut syn::Variant) -> syn::Result, frozen: Option, field_options: Vec<(&syn::Field, FieldPyO3Options)>, ) -> syn::Result> { @@ -717,6 +716,7 @@ fn descriptors_to_items( field_index, field, python_name: options.name.as_ref(), + renaming_rule: rename_all.map(|rename_all| rename_all.value.rule), }, )?; items.push(getter); @@ -730,6 +730,7 @@ fn descriptors_to_items( field_index, field, python_name: options.name.as_ref(), + renaming_rule: rename_all.map(|rename_all| rename_all.value.rule), }, )?; items.push(setter); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 0e2dc1006dd..b46f2ab71b8 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use crate::attributes::NameAttribute; +use crate::attributes::{NameAttribute, RenamingRule}; use crate::method::{CallingConvention, ExtractErrorMode}; use crate::utils::{ensure_not_async_fn, PythonDoc}; use crate::{ @@ -724,6 +724,7 @@ pub enum PropertyType<'a> { field_index: usize, field: &'a syn::Field, python_name: Option<&'a NameAttribute>, + renaming_rule: Option, }, Function { self_type: &'a SelfType, @@ -736,11 +737,21 @@ impl PropertyType<'_> { fn null_terminated_python_name(&self) -> Result { match self { PropertyType::Descriptor { - field, python_name, .. + field, + python_name, + renaming_rule, + .. } => { let name = match (python_name, &field.ident) { (Some(name), _) => name.value.0.to_string(), - (None, Some(field_name)) => format!("{}\0", field_name.unraw()), + (None, Some(field_name)) => { + let name = format!("{}\0", field_name.unraw()); + if let Some(rule) = renaming_rule { + utils::apply_renaming_rule(*rule, &name) + } else { + name + } + } (None, None) => { bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`"); } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 7bb8d240fd2..65da11f28e1 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -2,7 +2,7 @@ use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use syn::{punctuated::Punctuated, spanned::Spanned, Token}; -use crate::attributes::CrateAttribute; +use crate::attributes::{CrateAttribute, RenamingRule}; /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. macro_rules! err_spanned { @@ -161,3 +161,18 @@ pub(crate) fn get_pyo3_crate(attr: &Option) -> syn::Path { .map(|p| p.value.0.clone()) .unwrap_or_else(|| syn::parse_str("::pyo3").unwrap()) } + +pub fn apply_renaming_rule(rule: RenamingRule, name: &str) -> String { + use heck::*; + + match rule { + RenamingRule::CamelCase => name.to_lower_camel_case(), + RenamingRule::KebabCase => name.to_kebab_case(), + RenamingRule::Lowercase => name.to_lowercase(), + RenamingRule::PascalCase => name.to_upper_camel_case(), + RenamingRule::ScreamingKebabCase => name.to_shouty_kebab_case(), + RenamingRule::ScreamingSnakeCase => name.to_shouty_snake_case(), + RenamingRule::SnakeCase => name.to_snake_case(), + RenamingRule::Uppercase => name.to_uppercase(), + } +} diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index 781cc799cf9..ea6768da051 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -160,3 +160,42 @@ RuntimeError: An error occurred while initializing class BrokenClass" ) }); } + +#[pyclass(get_all, set_all, rename_all = "camelCase")] +struct StructWithRenamedFields { + first_field: bool, + second_field: u8, + #[pyo3(name = "third_field")] + fourth_field: bool, +} + +#[pymethods] +impl StructWithRenamedFields { + #[new] + fn new() -> Self { + Self { + first_field: true, + second_field: 5, + fourth_field: false, + } + } +} + +#[test] +fn test_renaming_all_struct_fields() { + use pyo3::types::PyBool; + + Python::with_gil(|py| { + let struct_class = py.get_type::(); + let struct_obj = struct_class.call0().unwrap(); + assert!(struct_obj + .setattr("firstField", PyBool::new(py, false)) + .is_ok()); + py_assert!(py, struct_obj, "struct_obj.firstField == False"); + py_assert!(py, struct_obj, "struct_obj.secondField == 5"); + assert!(struct_obj + .setattr("third_field", PyBool::new(py, true)) + .is_ok()); + py_assert!(py, struct_obj, "struct_obj.third_field == True"); + }); +} diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 994fc36e83e..a1b3a974523 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -190,3 +190,26 @@ fn test_rename_variant_repr_correct() { py_assert!(py, var1, "repr(var1) == 'RenameVariantEnum.VARIANT'"); }) } + +#[pyclass(rename_all = "SCREAMING_SNAKE_CASE")] +#[allow(clippy::enum_variant_names)] +enum RenameAllVariantsEnum { + VariantOne, + VariantTwo, + #[pyo3(name = "VariantThree")] + VariantFour, +} + +#[test] +fn test_renaming_all_enum_variants() { + Python::with_gil(|py| { + let enum_obj = py.get_type::(); + py_assert!(py, enum_obj, "enum_obj.VARIANT_ONE == enum_obj.VARIANT_ONE"); + py_assert!(py, enum_obj, "enum_obj.VARIANT_TWO == enum_obj.VARIANT_TWO"); + py_assert!( + py, + enum_obj, + "enum_obj.VariantThree == enum_obj.VariantThree" + ); + }); +} diff --git a/tests/ui/invalid_pyclass_args.rs b/tests/ui/invalid_pyclass_args.rs index c2ff187aadf..fac21db078c 100644 --- a/tests/ui/invalid_pyclass_args.rs +++ b/tests/ui/invalid_pyclass_args.rs @@ -15,6 +15,12 @@ struct InvalidName2 {} #[pyclass(name = CustomName)] struct DeprecatedName {} +#[pyclass(rename_all = camelCase)] +struct InvalidRenamingRule {} + +#[pyclass(rename_all = "Camel-Case")] +struct InvalidRenamingRule2 {} + #[pyclass(module = my_module)] struct InvalidModule {} diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 48a7961eb08..859f0f409d3 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -29,19 +29,31 @@ error: expected string literal | ^^^^^^^^^^ error: expected string literal - --> tests/ui/invalid_pyclass_args.rs:18:20 + --> tests/ui/invalid_pyclass_args.rs:18:24 | -18 | #[pyclass(module = my_module)] +18 | #[pyclass(rename_all = camelCase)] + | ^^^^^^^^^ + +error: expected a valid renaming rule, possible values are: "camelCase", "kebab-case", "lowercase", "PascalCase", "SCREAMING-KEBAB-CASE", "SCREAMING_SNAKE_CASE", "snake_case", "UPPERCASE" + --> tests/ui/invalid_pyclass_args.rs:21:24 + | +21 | #[pyclass(rename_all = "Camel-Case")] + | ^^^^^^^^^^^^ + +error: expected string literal + --> tests/ui/invalid_pyclass_args.rs:24:20 + | +24 | #[pyclass(module = my_module)] | ^^^^^^^^^ error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `text_signature`, `unsendable`, `weakref` - --> tests/ui/invalid_pyclass_args.rs:21:11 + --> tests/ui/invalid_pyclass_args.rs:27:11 | -21 | #[pyclass(weakrev)] +27 | #[pyclass(weakrev)] | ^^^^^^^ error: a `#[pyclass]` cannot be both a `mapping` and a `sequence` - --> tests/ui/invalid_pyclass_args.rs:25:8 + --> tests/ui/invalid_pyclass_args.rs:31:8 | -25 | struct CannotBeMappingAndSequence {} +31 | struct CannotBeMappingAndSequence {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^ From 1a73ce6e60bfdefb80f6b5321b62b1e6f5f7789a Mon Sep 17 00:00:00 2001 From: DataTriny Date: Tue, 15 Aug 2023 20:56:13 +0200 Subject: [PATCH 4/5] Address review comments --- pyo3-macros-backend/src/pyclass.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index fe2bc41e196..c2cb66d76dd 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -397,7 +397,7 @@ impl<'a> PyClassEnumVariant<'a> { .unwrap_or_else(|| { let name = self.ident.unraw(); if let Some(attr) = &args.options.rename_all { - let new_name = apply_renaming_rule(attr.value.rule, &format!("{}", name)); + let new_name = apply_renaming_rule(attr.value.rule, &name.to_string()); Cow::Owned(Ident::new(&new_name, Span::call_site())) } else { Cow::Owned(name) diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index b46f2ab71b8..5fd74ec2168 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -745,12 +745,12 @@ impl PropertyType<'_> { let name = match (python_name, &field.ident) { (Some(name), _) => name.value.0.to_string(), (None, Some(field_name)) => { - let name = format!("{}\0", field_name.unraw()); + let mut name = field_name.unraw().to_string(); if let Some(rule) = renaming_rule { - utils::apply_renaming_rule(*rule, &name) - } else { - name + name = utils::apply_renaming_rule(*rule, &name); } + name.push('\0'); + name } (None, None) => { bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`"); From 6c70db1e0bdb929221572e82c5236d3b5c04f2bb Mon Sep 17 00:00:00 2001 From: DataTriny Date: Wed, 16 Aug 2023 18:24:19 +0200 Subject: [PATCH 5/5] Test renaming rules --- tests/test_class_attributes.rs | 86 ++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index ea6768da051..bb88e81c898 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -199,3 +199,89 @@ fn test_renaming_all_struct_fields() { py_assert!(py, struct_obj, "struct_obj.third_field == True"); }); } + +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)] + #[allow(non_snake_case)] + struct $struct_name { + $field_name: u8, + } + #[pymethods] + impl $struct_name { + #[new] + fn new() -> Self { + Self { $field_name: 0 } + } + } + #[test] + fn $test_name() { + //use pyo3::types::PyInt; + + Python::with_gil(|py| { + let struct_class = py.get_type::<$struct_name>(); + let struct_obj = struct_class.call0().unwrap(); + assert!(struct_obj.setattr($renamed_field_name, 2).is_ok()); + let attr = struct_obj.getattr($renamed_field_name).unwrap(); + assert_eq!(2, PyAny::extract::(attr).unwrap()); + }); + } + }; +} + +test_case!( + LowercaseTest, + "lowercase", + fieldOne, + "fieldone", + test_rename_all_lowercase +); +test_case!( + CamelCaseTest, + "camelCase", + field_one, + "fieldOne", + test_rename_all_camel_case +); +test_case!( + KebabCaseTest, + "kebab-case", + field_one, + "field-one", + test_rename_all_kebab_case +); +test_case!( + PascalCaseTest, + "PascalCase", + field_one, + "FieldOne", + test_rename_all_pascal_case +); +test_case!( + ScreamingSnakeCaseTest, + "SCREAMING_SNAKE_CASE", + field_one, + "FIELD_ONE", + test_rename_all_screaming_snake_case +); +test_case!( + ScreamingKebabCaseTest, + "SCREAMING-KEBAB-CASE", + field_one, + "FIELD-ONE", + test_rename_all_screaming_kebab_case +); +test_case!( + SnakeCaseTest, + "snake_case", + fieldOne, + "field_one", + test_rename_all_snake_case +); +test_case!( + UppercaseTest, + "UPPERCASE", + fieldOne, + "FIELDONE", + test_rename_all_uppercase +);