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 {