Skip to content

Commit

Permalink
Merge pull request #3384 from DataTriny/pyclass_rename_variants
Browse files Browse the repository at this point in the history
Add  `rename_all` attribute to `#[pyclass]`
  • Loading branch information
adamreichold committed Aug 16, 2023
2 parents 0375d57 + 6c70db1 commit 9363491
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 20 deletions.
1 change: 1 addition & 0 deletions guide/pyclass_parameters.md
Expand Up @@ -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. |
| <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. |
| `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. |
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3384.added.md
@@ -0,0 +1 @@
`#[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"`.
1 change: 1 addition & 0 deletions pyo3-macros-backend/Cargo.toml
Expand Up @@ -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"
Expand Down
51 changes: 51 additions & 0 deletions pyo3-macros-backend/src/attributes.rs
Expand Up @@ -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);
Expand Down Expand Up @@ -82,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<Self> {
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 {
Expand Down Expand Up @@ -121,6 +171,7 @@ 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 TextSignatureAttribute = KeywordAttribute<kw::text_signature, TextSignatureAttributeValue>;

impl<K: Parse + std::fmt::Debug, V: Parse> Parse for KeywordAttribute<K, V> {
Expand Down
37 changes: 29 additions & 8 deletions pyo3-macros-backend/src/pyclass.rs
Expand Up @@ -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};
Expand All @@ -14,9 +14,9 @@ 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 proc_macro2::{Span, TokenStream};
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
Expand Down Expand Up @@ -66,6 +66,7 @@ pub struct PyClassPyO3Options {
pub mapping: Option<kw::mapping>,
pub module: Option<ModuleAttribute>,
pub name: Option<NameAttribute>,
pub rename_all: Option<RenameAllAttribute>,
pub sequence: Option<kw::sequence>,
pub set_all: Option<kw::set_all>,
pub subclass: Option<kw::subclass>,
Expand All @@ -86,6 +87,7 @@ enum PyClassPyO3Option {
Mapping(kw::mapping),
Module(ModuleAttribute),
Name(NameAttribute),
RenameAll(RenameAllAttribute),
Sequence(kw::sequence),
SetAll(kw::set_all),
Subclass(kw::subclass),
Expand Down Expand Up @@ -115,6 +117,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(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) {
Expand Down Expand Up @@ -173,6 +177,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),
Expand Down Expand Up @@ -356,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)
Expand All @@ -379,12 +389,20 @@ struct PyClassEnumVariant<'a> {
}

impl<'a> PyClassEnumVariant<'a> {
fn python_name(&self) -> Cow<'_, syn::Ident> {
fn python_name(&self, args: &PyClassArgs) -> Cow<'_, syn::Ident> {
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(|| {
let name = self.ident.unraw();
if let Some(attr) = &args.options.rename_all {
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)
}
})
}
}

Expand Down Expand Up @@ -515,7 +533,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, }
});
Expand Down Expand Up @@ -597,7 +615,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)
Expand Down Expand Up @@ -675,6 +693,7 @@ fn extract_variant_data(variant: &mut syn::Variant) -> syn::Result<PyClassEnumVa

fn descriptors_to_items(
cls: &syn::Ident,
rename_all: Option<&RenameAllAttribute>,
frozen: Option<frozen>,
field_options: Vec<(&syn::Field, FieldPyO3Options)>,
) -> syn::Result<Vec<MethodAndMethodDef>> {
Expand All @@ -697,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);
Expand All @@ -710,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);
Expand Down
17 changes: 14 additions & 3 deletions 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::{
Expand Down Expand Up @@ -724,6 +724,7 @@ pub enum PropertyType<'a> {
field_index: usize,
field: &'a syn::Field,
python_name: Option<&'a NameAttribute>,
renaming_rule: Option<RenamingRule>,
},
Function {
self_type: &'a SelfType,
Expand All @@ -736,11 +737,21 @@ impl PropertyType<'_> {
fn null_terminated_python_name(&self) -> Result<syn::LitStr> {
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 mut name = field_name.unraw().to_string();
if let Some(rule) = renaming_rule {
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`");
}
Expand Down
17 changes: 16 additions & 1 deletion pyo3-macros-backend/src/utils.rs
Expand Up @@ -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 {
Expand Down Expand Up @@ -161,3 +161,18 @@ pub(crate) fn get_pyo3_crate(attr: &Option<CrateAttribute>) -> 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(),
}
}

0 comments on commit 9363491

Please sign in to comment.