Skip to content

Commit 9834b90

Browse files
committed
Support enum variant named fields and cover failures
1 parent 1df9a53 commit 9834b90

File tree

5 files changed

+113
-9
lines changed

5 files changed

+113
-9
lines changed

guide/src/conversions/traits.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,9 @@ If the input is neither a string nor an integer, the error message will be:
492492
- if the argument is set, uses the given default value.
493493
- in this case, the argument must be a Rust expression returning a value of the desired Rust type.
494494
- if the argument is not set, [`Default::default`](https://doc.rust-lang.org/std/default/trait.Default.html#tymethod.default) is used.
495-
- this attribute is only supported on named struct fields.
495+
- note that the default value is only used if the field is not set.
496+
If the field is set and the conversion function from Python to Rust fails, an exception is raised and the default value is not used.
497+
- this attribute is only supported on named fields.
496498

497499
For example, the code below applies the given conversion function on the `"value"` dict item to compute its length or fall back to the type default value (0):
498500

@@ -503,6 +505,8 @@ use pyo3::prelude::*;
503505
struct RustyStruct {
504506
#[pyo3(item("value"), default, from_py_with = "Bound::<'_, PyAny>::len")]
505507
len: usize,
508+
#[pyo3(item)]
509+
other: usize,
506510
}
507511
#
508512
# use pyo3::types::PyDict;
@@ -511,11 +515,13 @@ struct RustyStruct {
511515
# // Filled case
512516
# let dict = PyDict::new(py);
513517
# dict.set_item("value", (1,)).unwrap();
518+
# dict.set_item("other", 1).unwrap();
514519
# let result = dict.extract::<RustyStruct>()?;
515520
# assert_eq!(result.len, 1);
516521
#
517522
# // Empty case
518523
# let dict = PyDict::new(py);
524+
# dict.set_item("other", 1).unwrap();
519525
# let result = dict.extract::<RustyStruct>()?;
520526
# assert_eq!(result.len, 0);
521527
# Ok(())

pyo3-macros-backend/src/frompyobject.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ impl<'a> Container<'a> {
147147
attrs.getter.is_none(),
148148
field.span() => "`getter` is not permitted on tuple struct elements."
149149
);
150+
ensure_spanned!(
151+
attrs.default.is_none(),
152+
field.span() => "`default` is not permitted on tuple struct elements."
153+
);
150154
Ok(TupleStructField {
151155
from_py_with: attrs.from_py_with,
152156
})
@@ -200,7 +204,11 @@ impl<'a> Container<'a> {
200204
})
201205
})
202206
.collect::<Result<Vec<_>>>()?;
203-
if options.transparent {
207+
if struct_fields.iter().all(|field| field.default.is_some()) {
208+
bail_spanned!(
209+
fields.span() => "cannot derive FromPyObject for structs and variants with only default values"
210+
)
211+
} else if options.transparent {
204212
ensure_spanned!(
205213
struct_fields.len() == 1,
206214
fields.span() => "transparent structs and variants can only have 1 field"

tests/test_frompyobject.rs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -690,38 +690,49 @@ fn test_with_keyword_item() {
690690
#[derive(Debug, FromPyObject, PartialEq, Eq)]
691691
pub struct WithDefaultItem {
692692
#[pyo3(item, default)]
693-
value: Option<usize>,
693+
opt: Option<usize>,
694+
#[pyo3(item)]
695+
value: usize,
694696
}
695697

696698
#[test]
697699
fn test_with_default_item() {
698700
Python::with_gil(|py| {
699701
let dict = PyDict::new(py);
702+
dict.set_item("value", 3).unwrap();
700703
let result = dict.extract::<WithDefaultItem>().unwrap();
701-
let expected = WithDefaultItem { value: None };
704+
let expected = WithDefaultItem {
705+
value: 3,
706+
opt: None,
707+
};
702708
assert_eq!(result, expected);
703709
});
704710
}
705711

706712
#[derive(Debug, FromPyObject, PartialEq, Eq)]
707713
pub struct WithExplicitDefaultItem {
708714
#[pyo3(item, default = 1)]
715+
opt: usize,
716+
#[pyo3(item)]
709717
value: usize,
710718
}
711719

712720
#[test]
713721
fn test_with_explicit_default_item() {
714722
Python::with_gil(|py| {
715723
let dict = PyDict::new(py);
724+
dict.set_item("value", 3).unwrap();
716725
let result = dict.extract::<WithExplicitDefaultItem>().unwrap();
717-
let expected = WithExplicitDefaultItem { value: 1 };
726+
let expected = WithExplicitDefaultItem { value: 3, opt: 1 };
718727
assert_eq!(result, expected);
719728
});
720729
}
721730

722731
#[derive(Debug, FromPyObject, PartialEq, Eq)]
723732
pub struct WithDefaultItemAndConversionFunction {
724733
#[pyo3(item, default, from_py_with = "Bound::<'_, PyAny>::len")]
734+
opt: usize,
735+
#[pyo3(item)]
725736
value: usize,
726737
}
727738

@@ -730,26 +741,62 @@ fn test_with_default_item_and_conversion_function() {
730741
Python::with_gil(|py| {
731742
// Filled case
732743
let dict = PyDict::new(py);
733-
dict.set_item("value", (1,)).unwrap();
744+
dict.set_item("opt", (1,)).unwrap();
745+
dict.set_item("value", 3).unwrap();
734746
let result = dict
735747
.extract::<WithDefaultItemAndConversionFunction>()
736748
.unwrap();
737-
let expected = WithDefaultItemAndConversionFunction { value: 1 };
749+
let expected = WithDefaultItemAndConversionFunction { opt: 1, value: 3 };
738750
assert_eq!(result, expected);
739751

740752
// Empty case
741753
let dict = PyDict::new(py);
754+
dict.set_item("value", 3).unwrap();
742755
let result = dict
743756
.extract::<WithDefaultItemAndConversionFunction>()
744757
.unwrap();
745-
let expected = WithDefaultItemAndConversionFunction { value: 0 };
758+
let expected = WithDefaultItemAndConversionFunction { opt: 0, value: 3 };
746759
assert_eq!(result, expected);
747760

748761
// Error case
749762
let dict = PyDict::new(py);
750-
dict.set_item("value", 1).unwrap();
763+
dict.set_item("value", 3).unwrap();
764+
dict.set_item("opt", 1).unwrap();
751765
assert!(dict
752766
.extract::<WithDefaultItemAndConversionFunction>()
753767
.is_err());
754768
});
755769
}
770+
771+
#[derive(Debug, FromPyObject, PartialEq, Eq)]
772+
pub enum WithDefaultItemEnum {
773+
#[pyo3(from_item_all)]
774+
Foo {
775+
a: usize,
776+
#[pyo3(default)]
777+
b: usize,
778+
},
779+
NeverUsedA {
780+
a: usize,
781+
},
782+
}
783+
784+
#[test]
785+
fn test_with_default_item_enum() {
786+
Python::with_gil(|py| {
787+
// A and B filled
788+
let dict = PyDict::new(py);
789+
dict.set_item("a", 1).unwrap();
790+
dict.set_item("b", 2).unwrap();
791+
let result = dict.extract::<WithDefaultItemEnum>().unwrap();
792+
let expected = WithDefaultItemEnum::Foo { a: 1, b: 2 };
793+
assert_eq!(result, expected);
794+
795+
// A filled
796+
let dict = PyDict::new(py);
797+
dict.set_item("a", 1).unwrap();
798+
let result = dict.extract::<WithDefaultItemEnum>().unwrap();
799+
let expected = WithDefaultItemEnum::Foo { a: 1, b: 0 };
800+
assert_eq!(result, expected);
801+
});
802+
}

tests/ui/invalid_frompy_derive.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,21 @@ struct FromItemAllConflictAttrWithArgs {
213213
field: String,
214214
}
215215

216+
#[derive(FromPyObject)]
217+
struct StructWithOnlyDefaultValues {
218+
#[pyo3(default)]
219+
field: String,
220+
}
221+
222+
#[derive(FromPyObject)]
223+
enum EnumVariantWithOnlyDefaultValues {
224+
Foo {
225+
#[pyo3(default)]
226+
field: String,
227+
},
228+
}
229+
230+
#[derive(FromPyObject)]
231+
struct NamedTuplesWithDefaultValues(#[pyo3(default)] String);
232+
216233
fn main() {}

tests/ui/invalid_frompy_derive.stderr

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,29 @@ error: The struct is already annotated with `from_item_all`, `attribute` is not
223223
|
224224
210 | #[pyo3(from_item_all)]
225225
| ^^^^^^^^^^^^^
226+
227+
error: cannot derive FromPyObject for structs and variants with only default values
228+
--> tests/ui/invalid_frompy_derive.rs:217:36
229+
|
230+
217 | struct StructWithOnlyDefaultValues {
231+
| ____________________________________^
232+
218 | | #[pyo3(default)]
233+
219 | | field: String,
234+
220 | | }
235+
| |_^
236+
237+
error: cannot derive FromPyObject for structs and variants with only default values
238+
--> tests/ui/invalid_frompy_derive.rs:224:9
239+
|
240+
224 | Foo {
241+
| _________^
242+
225 | | #[pyo3(default)]
243+
226 | | field: String,
244+
227 | | },
245+
| |_____^
246+
247+
error: `default` is not permitted on tuple struct elements.
248+
--> tests/ui/invalid_frompy_derive.rs:231:37
249+
|
250+
231 | struct NamedTuplesWithDefaultValues(#[pyo3(default)] String);
251+
| ^^^^^^^^^^^^^^^^^^^^^^^

0 commit comments

Comments
 (0)