diff --git a/guide/src/conversions.md b/guide/src/conversions.md index b861b6ca..f46da697 100644 --- a/guide/src/conversions.md +++ b/guide/src/conversions.md @@ -183,17 +183,28 @@ use pyo3::prelude::*; struct RustyTuple(String, 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. +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, +specify the struct as ``` use pyo3::prelude::*; #[derive(FromPyObject)] -#[pyo3(transparent)] -struct RustyTransparentTuple(String); +struct RustyTuple((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)] #[pyo3(transparent)] @@ -217,12 +228,10 @@ use pyo3::prelude::*; #[derive(FromPyObject)] enum RustyEnum<'a> { - #[pyo3(transparent)] Int(usize), // input is a positive int - #[pyo3(transparent)] String(String), // input is a string 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 x: usize, y: usize, @@ -257,9 +266,28 @@ enum RustyEnum { ``` If the input is neither a string nor an integer, the error message will be: -`"Can't convert to str, int"`, where `` is replaced by the type name and +`"Can't convert to Union[str, int]"`, where `` is replaced by the type name and `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` This trait defines the to-python conversion for a Rust type. It is usually implemented as diff --git a/pyo3-derive-backend/src/from_pyobject.rs b/pyo3-derive-backend/src/from_pyobject.rs index b32408e7..f7688e2b 100644 --- a/pyo3-derive-backend/src/from_pyobject.rs +++ b/pyo3-derive-backend/src/from_pyobject.rs @@ -65,6 +65,11 @@ impl<'a> Enum<'a> { error_names.push_str(", "); } } + let error_names = if self.variants.len() > 1 { + format!("Union[{}]", error_names) + } else { + error_names + }; quote!( #(#var_extracts)* let type_name = obj.get_type().name(); @@ -134,7 +139,13 @@ impl<'a> Container<'a> { } let style = match (fields, transparent) { (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) => { let field = named .named @@ -321,7 +332,8 @@ impl ContainerAttribute { if let syn::NestedMeta::Meta(metaitem) = &meta { match metaitem { 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") => { if let syn::Lit::Str(s) = &nv.lit { @@ -329,21 +341,13 @@ impl ContainerAttribute { } else { return Err(spanned_err(&nv.lit, "Expected string literal.")); } + continue; } - other => { - return Err(spanned_err( - other, - "Expected `transparent` or `annotation = \"name\"`", - )) - } + _ => {} // return Err below } - } 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) } @@ -514,7 +518,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { syn::Data::Union(_) => { return Err(spanned_err( tokens, - "FromPyObject can not be derived for unions.", + "#[derive(FromPyObject)] is not supported for unions.", )) } }; diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index ca9104cf..0fa937cc 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -155,7 +155,6 @@ fn test_tuple_struct() { } #[derive(FromPyObject)] -#[pyo3(transparent)] pub struct TransparentTuple(String); #[test] @@ -301,7 +300,7 @@ fn test_err_rename() { PyErrValue::ToObject(to) => { let o = to.to_object(py); 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"), }, diff --git a/tests/ui/invalid_frompy_derive.stderr b/tests/ui/invalid_frompy_derive.stderr index a0d1a60b..4eaf1218 100644 --- a/tests/ui/invalid_frompy_derive.stderr +++ b/tests/ui/invalid_frompy_derive.stderr @@ -132,7 +132,7 @@ error: Only one of `item`, `attribute` can be provided, possibly with an additio 118 | #[pyo3(item, attribute)] | ^^^^^^^^^^^^^^^ -error: Expected `transparent` or `annotation = "name"` +error: Unrecognized `pyo3` container attribute --> $DIR/invalid_frompy_derive.rs:123:8 | 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> { | ^^^^^^^^ -error: FromPyObject can not be derived for unions. +error: #[derive(FromPyObject)] is not supported for unions. --> $DIR/invalid_frompy_derive.rs:147:1 | 147 | / union Union {