More FromPyObject derive suggestions by @davidhewitt

This commit is contained in:
Sebastian Pütz 2020-08-30 19:04:42 +02:00
parent 7a9f4a1633
commit 0f32f886b8
4 changed files with 61 additions and 30 deletions

View File

@ -183,17 +183,28 @@ use pyo3::prelude::*;
struct RustyTuple(String, String); struct RustyTuple(String, String);
``` ```
#### Deriving [`FromPyObject`] for wrapper types Tuple structs with a single field are treated as wrapper types which are described in the
following section. To override this behaviour and ensure that the input is in fact a tuple,
The `pyo3(transparent)` attribute can be used on structs with exactly one field. This results specify the struct as
in extracting directly from the input object, i.e. `obj.extract()`, rather than trying to access
an item or attribute.
``` ```
use pyo3::prelude::*; use pyo3::prelude::*;
#[derive(FromPyObject)] #[derive(FromPyObject)]
#[pyo3(transparent)] struct RustyTuple((String,));
struct RustyTransparentTuple(String); ```
#### Deriving [`FromPyObject`] for wrapper types
The `pyo3(transparent)` attribute can be used on structs with exactly one field. This results
in extracting directly from the input object, i.e. `obj.extract()`, rather than trying to access
an item or attribute. This behaviour is enabled per default for newtype structs and tuple-variants
with a single field.
```
use pyo3::prelude::*;
#[derive(FromPyObject)]
struct RustyTransparentTupleStruct(String);
#[derive(FromPyObject)] #[derive(FromPyObject)]
#[pyo3(transparent)] #[pyo3(transparent)]
@ -217,12 +228,10 @@ use pyo3::prelude::*;
#[derive(FromPyObject)] #[derive(FromPyObject)]
enum RustyEnum<'a> { enum RustyEnum<'a> {
#[pyo3(transparent)]
Int(usize), // input is a positive int Int(usize), // input is a positive int
#[pyo3(transparent)]
String(String), // input is a string String(String), // input is a string
IntTuple(usize, usize), // input is a 2-tuple with positive ints IntTuple(usize, usize), // input is a 2-tuple with positive ints
StringIntTuple(String, usize), // innput is a 2-tuple with String and int StringIntTuple(String, usize), // input is a 2-tuple with String and int
Coordinates3d { // needs to be in front of 2d Coordinates3d { // needs to be in front of 2d
x: usize, x: usize,
y: usize, y: usize,
@ -257,9 +266,28 @@ enum RustyEnum {
``` ```
If the input is neither a string nor an integer, the error message will be: If the input is neither a string nor an integer, the error message will be:
`"Can't convert <INPUT> to str, int"`, where `<INPUT>` is replaced by the type name and `"Can't convert <INPUT> to Union[str, int]"`, where `<INPUT>` is replaced by the type name and
`repr()` of the input object. `repr()` of the input object.
#### `#[derive(FromPyObject)]` Container Attributes
- `pyo3(transparent)`
- extract the field directly from the object as `obj.extract()` instead of `get_item()` or
`getattr()`
- Newtype structs and tuple-variants are treated as transparent per default.
- only supported for single-field structs and enum variants
- `pyo3(annotation = "name")`
- changes the name of the failed variant in the generated error message in case of failure.
- e.g. `pyo3("int")` reports the variant's type as `int`.
- only supported for enum variants
#### `#[derive(FromPyObject)]` Field Attributes
- `pyo3(attribute)`, `pyo3(attribute("name"))`
- retrieve the field from an attribute, possibly with a custom name specified as an argument
- argument must be a string-literal.
- `pyo3(item)`, `pyo3(item("key"))`
- retrieve the field from a mapping, possibly with the custom key specified as an argument.
- can be any literal that implements `ToBorrowedObject`
### `IntoPy<T>` ### `IntoPy<T>`
This trait defines the to-python conversion for a Rust type. It is usually implemented as This trait defines the to-python conversion for a Rust type. It is usually implemented as

View File

@ -65,6 +65,11 @@ impl<'a> Enum<'a> {
error_names.push_str(", "); error_names.push_str(", ");
} }
} }
let error_names = if self.variants.len() > 1 {
format!("Union[{}]", error_names)
} else {
error_names
};
quote!( quote!(
#(#var_extracts)* #(#var_extracts)*
let type_name = obj.get_type().name(); let type_name = obj.get_type().name();
@ -134,7 +139,13 @@ impl<'a> Container<'a> {
} }
let style = match (fields, transparent) { let style = match (fields, transparent) {
(Fields::Unnamed(_), true) => ContainerType::TupleNewtype, (Fields::Unnamed(_), true) => ContainerType::TupleNewtype,
(Fields::Unnamed(unnamed), false) => ContainerType::Tuple(unnamed.unnamed.len()), (Fields::Unnamed(unnamed), false) => {
if unnamed.unnamed.len() == 1 {
ContainerType::TupleNewtype
} else {
ContainerType::Tuple(unnamed.unnamed.len())
}
}
(Fields::Named(named), true) => { (Fields::Named(named), true) => {
let field = named let field = named
.named .named
@ -321,7 +332,8 @@ impl ContainerAttribute {
if let syn::NestedMeta::Meta(metaitem) = &meta { if let syn::NestedMeta::Meta(metaitem) = &meta {
match metaitem { match metaitem {
Meta::Path(p) if p.is_ident("transparent") => { Meta::Path(p) if p.is_ident("transparent") => {
attrs.push(ContainerAttribute::Transparent) attrs.push(ContainerAttribute::Transparent);
continue;
} }
Meta::NameValue(nv) if nv.path.is_ident("annotation") => { Meta::NameValue(nv) if nv.path.is_ident("annotation") => {
if let syn::Lit::Str(s) = &nv.lit { if let syn::Lit::Str(s) = &nv.lit {
@ -329,21 +341,13 @@ impl ContainerAttribute {
} else { } else {
return Err(spanned_err(&nv.lit, "Expected string literal.")); return Err(spanned_err(&nv.lit, "Expected string literal."));
} }
continue;
} }
other => { _ => {} // return Err below
return Err(spanned_err(
other,
"Expected `transparent` or `annotation = \"name\"`",
))
}
} }
} else {
return Err(spanned_err(
meta,
"Unknown container attribute, expected `transparent` or \
`annotation(\"err_name\")`",
));
} }
return Err(spanned_err(meta, "Unrecognized `pyo3` container attribute"));
} }
Ok(attrs) Ok(attrs)
} }
@ -514,7 +518,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result<TokenStream> {
syn::Data::Union(_) => { syn::Data::Union(_) => {
return Err(spanned_err( return Err(spanned_err(
tokens, tokens,
"FromPyObject can not be derived for unions.", "#[derive(FromPyObject)] is not supported for unions.",
)) ))
} }
}; };

View File

@ -155,7 +155,6 @@ fn test_tuple_struct() {
} }
#[derive(FromPyObject)] #[derive(FromPyObject)]
#[pyo3(transparent)]
pub struct TransparentTuple(String); pub struct TransparentTuple(String);
#[test] #[test]
@ -301,7 +300,7 @@ fn test_err_rename() {
PyErrValue::ToObject(to) => { PyErrValue::ToObject(to) => {
let o = to.to_object(py); let o = to.to_object(py);
let s = String::extract(o.as_ref(py)).expect("Err val is not a string"); let s = String::extract(o.as_ref(py)).expect("Err val is not a string");
assert_eq!(s, "Can't convert {} (dict) to str, uint, int") assert_eq!(s, "Can't convert {} (dict) to Union[str, uint, int]")
} }
_ => panic!("Expected PyErrValue::ToObject"), _ => panic!("Expected PyErrValue::ToObject"),
}, },

View File

@ -132,7 +132,7 @@ error: Only one of `item`, `attribute` can be provided, possibly with an additio
118 | #[pyo3(item, attribute)] 118 | #[pyo3(item, attribute)]
| ^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^
error: Expected `transparent` or `annotation = "name"` error: Unrecognized `pyo3` container attribute
--> $DIR/invalid_frompy_derive.rs:123:8 --> $DIR/invalid_frompy_derive.rs:123:8
| |
123 | #[pyo3(unknown = "should not work")] 123 | #[pyo3(unknown = "should not work")]
@ -156,7 +156,7 @@ error: FromPyObject can be derived with at most one lifetime parameter.
141 | enum TooManyLifetimes<'a, 'b> { 141 | enum TooManyLifetimes<'a, 'b> {
| ^^^^^^^^ | ^^^^^^^^
error: FromPyObject can not be derived for unions. error: #[derive(FromPyObject)] is not supported for unions.
--> $DIR/invalid_frompy_derive.rs:147:1 --> $DIR/invalid_frompy_derive.rs:147:1
| |
147 | / union Union { 147 | / union Union {