Skip to content

Commit

Permalink
Merge #3120
Browse files Browse the repository at this point in the history
3120: feat: add #[pyo3(from_item_all)] for FromPyObject r=davidhewitt a=BlueGlassBlock

This PR addresses #3112 by implementing ~~`#[pyo3(item_all)]`~~ `#[pyo3(from_item_all)]`.

Co-authored-by: BlueGlassBlock <blueglassblock@outlook.com>
  • Loading branch information
bors[bot] and BlueGlassBlock committed May 4, 2023
2 parents 157e0b4 + f75ea3d commit 6356acb
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 3 deletions.
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)]
| ^^^^^^^^^^^^^

0 comments on commit 6356acb

Please sign in to comment.