Skip to content

Commit b61dc7f

Browse files
authored
add rename_all support for #[derive(IntoPyObject)] (#5112)
1 parent d74fadc commit b61dc7f

File tree

6 files changed

+129
-3
lines changed

6 files changed

+129
-3
lines changed

newsfragments/5112.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
add `rename_all` support for `#[derive(IntoPyObject)]`

pyo3-macros-backend/src/intopyobject.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use crate::attributes::IntoPyWithAttribute;
1+
use crate::attributes::{IntoPyWithAttribute, RenamingRule};
22
use crate::derive_attributes::{ContainerAttributes, FieldAttributes};
3-
use crate::utils::Ctx;
3+
use crate::utils::{self, Ctx};
44
use proc_macro2::{Span, TokenStream};
55
use quote::{format_ident, quote, quote_spanned, ToTokens};
66
use syn::ext::IdentExt;
@@ -65,6 +65,7 @@ struct Container<'a, const REF: bool> {
6565
path: syn::Path,
6666
receiver: Option<Ident>,
6767
ty: ContainerType<'a>,
68+
rename_rule: Option<RenamingRule>,
6869
}
6970

7071
/// Construct a container based on fields, identifier and attributes.
@@ -79,6 +80,10 @@ impl<'a, const REF: bool> Container<'a, REF> {
7980
) -> Result<Self> {
8081
let style = match fields {
8182
Fields::Unnamed(unnamed) if !unnamed.unnamed.is_empty() => {
83+
ensure_spanned!(
84+
options.rename_all.is_none(),
85+
options.rename_all.span() => "`rename_all` is useless on tuple structs and variants."
86+
);
8287
let mut tuple_fields = unnamed
8388
.unnamed
8489
.iter()
@@ -127,6 +132,10 @@ impl<'a, const REF: bool> Container<'a, REF> {
127132
attrs.getter.is_none(),
128133
attrs.getter.unwrap().span() => "`transparent` structs may not have `item` nor `attribute` for the inner field"
129134
);
135+
ensure_spanned!(
136+
options.rename_all.is_none(),
137+
options.rename_all.span() => "`rename_all` is not permitted on `transparent` structs and variants"
138+
);
130139
ensure_spanned!(
131140
attrs.into_py_with.is_none(),
132141
attrs.into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs or variants"
@@ -169,6 +178,7 @@ impl<'a, const REF: bool> Container<'a, REF> {
169178
path,
170179
receiver,
171180
ty: style,
181+
rename_rule: options.rename_all.map(|v| v.value.rule),
172182
};
173183
Ok(v)
174184
}
@@ -254,7 +264,10 @@ impl<'a, const REF: bool> Container<'a, REF> {
254264
.as_ref()
255265
.and_then(|item| item.0.as_ref())
256266
.map(|item| item.into_token_stream())
257-
.unwrap_or_else(|| f.ident.unraw().to_string().into_token_stream());
267+
.unwrap_or_else(|| {
268+
let name = f.ident.unraw().to_string();
269+
self.rename_rule.map(|rule| utils::apply_renaming_rule(rule, &name)).unwrap_or(name).into_token_stream()
270+
});
258271
let value = Ident::new(&format!("arg{i}"), f.field.ty.span());
259272

260273
if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) {
@@ -459,6 +472,9 @@ pub fn build_derive_into_pyobject<const REF: bool>(tokens: &DeriveInput) -> Resu
459472
if options.transparent.is_some() {
460473
bail_spanned!(tokens.span() => "`transparent` is not supported at top level for enums");
461474
}
475+
if let Some(rename_all) = options.rename_all {
476+
bail_spanned!(rename_all.span() => "`rename_all` is not supported at top level for enums");
477+
}
462478
let en = Enum::<REF>::new(en, &tokens.ident)?;
463479
en.build(ctx)
464480
}

tests/test_frompy_intopy_roundtrip.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,11 @@ pub enum Foo {
225225
TransparentStructVar {
226226
a: Option<String>,
227227
},
228+
#[pyo3(rename_all = "camelCase", from_item_all)]
229+
RenameAll {
230+
long_field_name: [u16; 2],
231+
other_field: Option<String>,
232+
},
228233
}
229234

230235
#[test]
@@ -270,5 +275,15 @@ fn test_enum() {
270275

271276
let foo = transparent_struct_var.clone().into_pyobject(py).unwrap();
272277
assert_eq!(transparent_struct_var, foo.extract::<Foo>().unwrap());
278+
279+
let rename_all_struct_var = Foo::RenameAll {
280+
long_field_name: [1, 2],
281+
other_field: None,
282+
};
283+
let foo = (&rename_all_struct_var).into_pyobject(py).unwrap();
284+
assert_eq!(rename_all_struct_var, foo.extract::<Foo>().unwrap());
285+
286+
let foo = rename_all_struct_var.clone().into_pyobject(py).unwrap();
287+
assert_eq!(rename_all_struct_var, foo.extract::<Foo>().unwrap());
273288
});
274289
}

tests/test_intopyobject.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,38 @@ fn test_into_py_with() {
252252
);
253253
});
254254
}
255+
256+
#[test]
257+
fn test_struct_into_py_rename_all() {
258+
#[derive(IntoPyObject, IntoPyObjectRef)]
259+
#[pyo3(rename_all = "camelCase")]
260+
struct Foo {
261+
foo_bar: String,
262+
#[pyo3(item("BAZ"))]
263+
baz: usize,
264+
#[pyo3(item)]
265+
long_field_name: f32,
266+
}
267+
268+
let foo = Foo {
269+
foo_bar: "foobar".into(),
270+
baz: 42,
271+
long_field_name: 0.0,
272+
};
273+
274+
Python::with_gil(|py| {
275+
let py_foo_ref = (&foo).into_pyobject(py).unwrap();
276+
let py_foo = foo.into_pyobject(py).unwrap();
277+
278+
py_run!(
279+
py,
280+
py_foo_ref,
281+
"assert py_foo_ref == {'fooBar': 'foobar', 'BAZ': 42, 'longFieldName': 0},f'{py_foo_ref}'"
282+
);
283+
py_run!(
284+
py,
285+
py_foo,
286+
"assert py_foo == {'fooBar': 'foobar', 'BAZ': 42, 'longFieldName': 0},f'{py_foo}'"
287+
);
288+
});
289+
}

tests/ui/invalid_intopy_derive.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,33 @@ enum EnumStructIntoPyWith {
138138
},
139139
}
140140

141+
#[derive(IntoPyObject, IntoPyObjectRef)]
142+
#[pyo3(transparent, rename_all = "camelCase")]
143+
struct StructTransparentRenameAll {
144+
foo_bar: String,
145+
}
146+
147+
#[derive(IntoPyObject, IntoPyObjectRef)]
148+
#[pyo3(rename_all = "camelCase")]
149+
struct StructTupleRenameAll(String, usize);
150+
151+
#[derive(IntoPyObject, IntoPyObjectRef)]
152+
enum EnumTransparentVariantRenameAll {
153+
#[pyo3(rename_all = "camelCase")]
154+
#[pyo3(transparent)]
155+
Variant { foo: String },
156+
}
157+
158+
#[derive(IntoPyObject, IntoPyObjectRef)]
159+
enum EnumTupleVariantRenameAll {
160+
#[pyo3(rename_all = "camelCase")]
161+
Variant(String, usize),
162+
}
163+
164+
#[derive(IntoPyObject, IntoPyObjectRef)]
165+
#[pyo3(rename_all = "camelCase")]
166+
enum EnumTopRenameAll {
167+
Variant { foo: String },
168+
}
169+
141170
fn main() {}

tests/ui/invalid_intopy_derive.stderr

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,33 @@ error: `into_py_with` is not permitted on `transparent` structs or variants
149149
|
150150
136 | #[pyo3(into_py_with = into)]
151151
| ^^^^^^^^^^^^
152+
153+
error: `rename_all` is not permitted on `transparent` structs and variants
154+
--> tests/ui/invalid_intopy_derive.rs:142:21
155+
|
156+
142 | #[pyo3(transparent, rename_all = "camelCase")]
157+
| ^^^^^^^^^^
158+
159+
error: `rename_all` is useless on tuple structs and variants.
160+
--> tests/ui/invalid_intopy_derive.rs:148:8
161+
|
162+
148 | #[pyo3(rename_all = "camelCase")]
163+
| ^^^^^^^^^^
164+
165+
error: `rename_all` is not permitted on `transparent` structs and variants
166+
--> tests/ui/invalid_intopy_derive.rs:153:12
167+
|
168+
153 | #[pyo3(rename_all = "camelCase")]
169+
| ^^^^^^^^^^
170+
171+
error: `rename_all` is useless on tuple structs and variants.
172+
--> tests/ui/invalid_intopy_derive.rs:160:12
173+
|
174+
160 | #[pyo3(rename_all = "camelCase")]
175+
| ^^^^^^^^^^
176+
177+
error: `rename_all` is not supported at top level for enums
178+
--> tests/ui/invalid_intopy_derive.rs:165:8
179+
|
180+
165 | #[pyo3(rename_all = "camelCase")]
181+
| ^^^^^^^^^^

0 commit comments

Comments
 (0)