Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add #[pyo3(from_item_all)] for FromPyObject #3120

Merged
merged 8 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 29 additions & 0 deletions guide/src/conversions/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,35 @@ from a mapping with the key `"key"`. The arguments for `attribute` are restricte
non-empty string literals while `item` can take any valid literal that implements
`ToBorrowedObject`.

You can use `#[pyo3(from_item_all)]` on a struct to extract every field with `get_item` method.
In this case, you can't use `#[pyo3(attribute)]` or barely use `#[pyo3(item)]` on any field.
However, using `#[pyo3(item("key"))]` to specify the key for a field is still allowed.

```rust
use pyo3::prelude::*;

#[derive(FromPyObject)]
#[pyo3(from_item_all)]
struct RustyStruct {
foo: String,
bar: String,
#[pyo3(item("foobar"))]
baz: String,
}
#
# fn main() -> PyResult<()> {
# Python::with_gil(|py| -> PyResult<()> {
# let py_dict = py.eval("{'foo': 'foo', 'bar': 'bar', 'foobar': 'foobar'}", None, None)?;
# let rustystruct: RustyStruct = py_dict.extract()?;
# assert_eq!(rustystruct.foo, "foo");
# assert_eq!(rustystruct.bar, "bar");
# assert_eq!(rustystruct.baz, "foobar");
#
# Ok(())
# })
# }
```

#### Deriving [`FromPyObject`] for tuple structs

Tuple structs are also supported but do not allow customizing the extraction. The input is
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3120.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow using `#[pyo3(from_item_all)]` when deriving `FromPyObject` to specify `get_item` as getter for all fields.
1 change: 1 addition & 0 deletions pyo3-macros-backend/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod kw {
syn::custom_keyword!(get);
syn::custom_keyword!(get_all);
syn::custom_keyword!(item);
syn::custom_keyword!(from_item_all);
syn::custom_keyword!(mapping);
syn::custom_keyword!(module);
syn::custom_keyword!(name);
Expand Down
33 changes: 31 additions & 2 deletions pyo3-macros-backend/src/frompyobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,22 @@ impl<'a> Container<'a> {
.ident
.as_ref()
.expect("Named fields should have identifiers");
let attrs = FieldPyO3Attributes::from_attrs(&field.attrs)?;
let mut attrs = FieldPyO3Attributes::from_attrs(&field.attrs)?;

if let Some(ref from_item_all) = options.from_item_all {
if let Some(replaced) = attrs.getter.replace(FieldGetter::GetItem(None))
{
match replaced {
FieldGetter::GetItem(Some(item_name)) => {
attrs.getter = Some(FieldGetter::GetItem(Some(item_name)));
}
FieldGetter::GetItem(None) => bail_spanned!(from_item_all.span() => "Useless `item` - the struct is already annotated with `from_item_all`"),
FieldGetter::GetAttr(_) => bail_spanned!(
from_item_all.span() => "The struct is already annotated with `from_item_all`, `attribute` is not allowed"
),
}
}
}

Ok(NamedStructField {
ident,
Expand Down Expand Up @@ -343,6 +358,8 @@ impl<'a> Container<'a> {
struct ContainerOptions {
/// Treat the Container as a Wrapper, directly extract its fields from the input object.
transparent: bool,
/// Force every field to be extracted from item of source Python object.
from_item_all: Option<attributes::kw::from_item_all>,
/// Change the name of an enum variant in the generated error message.
annotation: Option<syn::LitStr>,
/// Change the path for the pyo3 crate
Expand All @@ -353,6 +370,8 @@ struct ContainerOptions {
enum ContainerPyO3Attribute {
/// Treat the Container as a Wrapper, directly extract its fields from the input object.
Transparent(attributes::kw::transparent),
/// Force every field to be extracted from item of source Python object.
ItemAll(attributes::kw::from_item_all),
/// Change the name of an enum variant in the generated error message.
ErrorAnnotation(LitStr),
/// Change the path for the pyo3 crate
Expand All @@ -365,6 +384,9 @@ impl Parse for ContainerPyO3Attribute {
if lookahead.peek(attributes::kw::transparent) {
let kw: attributes::kw::transparent = input.parse()?;
Ok(ContainerPyO3Attribute::Transparent(kw))
} else if lookahead.peek(attributes::kw::from_item_all) {
let kw: attributes::kw::from_item_all = input.parse()?;
Ok(ContainerPyO3Attribute::ItemAll(kw))
} else if lookahead.peek(attributes::kw::annotation) {
let _: attributes::kw::annotation = input.parse()?;
let _: Token![=] = input.parse()?;
Expand Down Expand Up @@ -392,6 +414,13 @@ impl ContainerOptions {
);
options.transparent = true;
}
ContainerPyO3Attribute::ItemAll(kw) => {
ensure_spanned!(
matches!(options.from_item_all, None),
kw.span() => "`from_item_all` may only be provided once"
);
options.from_item_all = Some(kw);
}
ContainerPyO3Attribute::ErrorAnnotation(lit_str) => {
ensure_spanned!(
options.annotation.is_none(),
Expand Down Expand Up @@ -494,7 +523,7 @@ impl FieldPyO3Attributes {
getter.is_none(),
attr.span() => "only one of `attribute` or `item` can be provided"
);
getter = Some(field_getter)
getter = Some(field_getter);
}
FieldPyO3Attribute::FromPyWith(from_py_with_attr) => {
ensure_spanned!(
Expand Down
38 changes: 38 additions & 0 deletions tests/ui/invalid_frompy_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,42 @@ struct InvalidTransparentWithGetter {
field: String,
}

#[derive(FromPyObject)]
#[pyo3(from_item_all)]
struct FromItemAllOnTuple(String);

#[derive(FromPyObject)]
#[pyo3(from_item_all)]
#[pyo3(transparent)]
struct FromItemAllWithTransparent {
field: String,
}

#[derive(FromPyObject)]
#[pyo3(from_item_all, from_item_all)]
struct MultipleFromItemAll {
field: String,
}

#[derive(FromPyObject)]
#[pyo3(from_item_all)]
struct UselessItemAttr {
#[pyo3(item)]
field: String,
}

#[derive(FromPyObject)]
#[pyo3(from_item_all)]
struct FromItemAllConflictAttr {
#[pyo3(attribute)]
field: String,
}

#[derive(FromPyObject)]
#[pyo3(from_item_all)]
struct FromItemAllConflictAttrWithArgs {
#[pyo3(attribute("f"))]
field: String,
}

fn main() {}
32 changes: 31 additions & 1 deletion tests/ui/invalid_frompy_derive.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ error: only one of `attribute` or `item` can be provided
118 | #[pyo3(item, attribute)]
| ^

error: expected one of: `transparent`, `annotation`, `crate`
error: expected one of: `transparent`, `from_item_all`, `annotation`, `crate`
--> tests/ui/invalid_frompy_derive.rs:123:8
|
123 | #[pyo3(unknown = "should not work")]
Expand Down Expand Up @@ -193,3 +193,33 @@ error: `transparent` structs may not have a `getter` for the inner field
|
175 | field: String,
| ^^^^^

error: `transparent` structs may not have a `getter` for the inner field
--> tests/ui/invalid_frompy_derive.rs:186:5
|
186 | field: String,
| ^^^^^

error: `from_item_all` may only be provided once
--> tests/ui/invalid_frompy_derive.rs:190:23
|
190 | #[pyo3(from_item_all, from_item_all)]
| ^^^^^^^^^^^^^

error: Useless `item` - the struct is already annotated with `from_item_all`
--> tests/ui/invalid_frompy_derive.rs:196:8
|
196 | #[pyo3(from_item_all)]
| ^^^^^^^^^^^^^

error: The struct is already annotated with `from_item_all`, `attribute` is not allowed
--> tests/ui/invalid_frompy_derive.rs:203:8
|
203 | #[pyo3(from_item_all)]
| ^^^^^^^^^^^^^

error: The struct is already annotated with `from_item_all`, `attribute` is not allowed
--> tests/ui/invalid_frompy_derive.rs:210:8
|
210 | #[pyo3(from_item_all)]
| ^^^^^^^^^^^^^