frompyobject: improve error messages of derived impls

This commit is contained in:
David Hewitt 2022-06-02 11:10:59 +01:00
parent a388901fc6
commit 0aa4f95a98
3 changed files with 52 additions and 50 deletions

View File

@ -36,12 +36,7 @@ impl<'a> Enum<'a> {
.map(|variant| { .map(|variant| {
let attrs = ContainerOptions::from_attrs(&variant.attrs)?; let attrs = ContainerOptions::from_attrs(&variant.attrs)?;
let var_ident = &variant.ident; let var_ident = &variant.ident;
Container::new( Container::new(&variant.fields, parse_quote!(#ident::#var_ident), attrs)
&variant.fields,
parse_quote!(#ident::#var_ident),
attrs,
true,
)
}) })
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
@ -123,19 +118,13 @@ struct Container<'a> {
path: syn::Path, path: syn::Path,
ty: ContainerType<'a>, ty: ContainerType<'a>,
err_name: String, err_name: String,
is_enum_variant: bool,
} }
impl<'a> Container<'a> { impl<'a> Container<'a> {
/// Construct a container based on fields, identifier and attributes. /// Construct a container based on fields, identifier and attributes.
/// ///
/// Fails if the variant has no fields or incompatible attributes. /// Fails if the variant has no fields or incompatible attributes.
fn new( fn new(fields: &'a Fields, path: syn::Path, options: ContainerOptions) -> Result<Self> {
fields: &'a Fields,
path: syn::Path,
options: ContainerOptions,
is_enum_variant: bool,
) -> Result<Self> {
ensure_spanned!( ensure_spanned!(
!fields.is_empty(), !fields.is_empty(),
fields.span() => "cannot derive FromPyObject for empty structs and variants" fields.span() => "cannot derive FromPyObject for empty structs and variants"
@ -195,11 +184,21 @@ impl<'a> Container<'a> {
path, path,
ty: style, ty: style,
err_name, err_name,
is_enum_variant,
}; };
Ok(v) Ok(v)
} }
fn name(&self) -> String {
let mut value = String::new();
for segment in &self.path.segments {
if !value.is_empty() {
value.push_str("::");
}
value.push_str(&segment.ident.to_string());
}
value
}
/// Build derivation body for a struct. /// Build derivation body for a struct.
fn build(&self) -> TokenStream { fn build(&self) -> TokenStream {
match &self.ty { match &self.ty {
@ -212,37 +211,28 @@ impl<'a> Container<'a> {
fn build_newtype_struct(&self, field_ident: Option<&Ident>) -> TokenStream { fn build_newtype_struct(&self, field_ident: Option<&Ident>) -> TokenStream {
let self_ty = &self.path; let self_ty = &self.path;
let struct_name = self.name();
if let Some(ident) = field_ident { if let Some(ident) = field_ident {
let struct_name = quote!(#self_ty).to_string();
let field_name = ident.to_string(); let field_name = ident.to_string();
quote!( quote!(
::std::result::Result::Ok(#self_ty{ ::std::result::Result::Ok(#self_ty{
#ident: _pyo3::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)? #ident: _pyo3::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)?
}) })
) )
} else if !self.is_enum_variant {
let error_msg = format!("failed to extract inner field of {}", quote!(#self_ty));
quote!(
::std::result::Result::Ok(#self_ty(obj.extract().map_err(|err| {
let py = _pyo3::PyNativeType::py(obj);
let err_msg = ::std::format!("{}: {}",
#error_msg,
err.value(py).str().unwrap());
_pyo3::exceptions::PyTypeError::new_err(err_msg)
})?))
)
} else { } else {
quote!(obj.extract().map(#self_ty)) quote!(
_pyo3::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty)
)
} }
} }
fn build_tuple_struct(&self, tups: &[FieldPyO3Attributes]) -> TokenStream { fn build_tuple_struct(&self, tups: &[FieldPyO3Attributes]) -> TokenStream {
let self_ty = &self.path; let self_ty = &self.path;
let struct_name = &self.name();
let field_idents: Vec<_> = (0..tups.len()) let field_idents: Vec<_> = (0..tups.len())
.into_iter() .into_iter()
.map(|i| format_ident!("arg{}", i)) .map(|i| format_ident!("arg{}", i))
.collect(); .collect();
let struct_name = &quote!(#self_ty).to_string();
let fields = tups.iter().zip(&field_idents).enumerate().map(|(index, (attrs, ident))| { let fields = tups.iter().zip(&field_idents).enumerate().map(|(index, (attrs, ident))| {
match &attrs.from_py_with { match &attrs.from_py_with {
None => quote!( None => quote!(
@ -265,9 +255,9 @@ impl<'a> Container<'a> {
fn build_struct(&self, tups: &[(&Ident, FieldPyO3Attributes)]) -> TokenStream { fn build_struct(&self, tups: &[(&Ident, FieldPyO3Attributes)]) -> TokenStream {
let self_ty = &self.path; let self_ty = &self.path;
let struct_name = &self.name();
let mut fields: Punctuated<TokenStream, syn::Token![,]> = Punctuated::new(); let mut fields: Punctuated<TokenStream, syn::Token![,]> = Punctuated::new();
for (ident, attrs) in tups { for (ident, attrs) in tups {
let struct_name = quote!(#self_ty).to_string();
let field_name = ident.to_string(); let field_name = ident.to_string();
let getter = match &attrs.getter { let getter = match &attrs.getter {
FieldGetter::GetAttr(Some(name)) => { FieldGetter::GetAttr(Some(name)) => {
@ -523,7 +513,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result<TokenStream> {
bail_spanned!(lit_str.span() => "`annotation` is unsupported for structs"); bail_spanned!(lit_str.span() => "`annotation` is unsupported for structs");
} }
let ident = &tokens.ident; let ident = &tokens.ident;
let st = Container::new(&st.fields, parse_quote!(#ident), options, false)?; let st = Container::new(&st.fields, parse_quote!(#ident), options)?;
st.build() st.build()
} }
syn::Data::Union(_) => bail_spanned!( syn::Data::Union(_) => bail_spanned!(

View File

@ -8,6 +8,7 @@ pub fn failed_to_extract_enum(
error_names: &[&str], error_names: &[&str],
errors: &[PyErr], errors: &[PyErr],
) -> PyErr { ) -> PyErr {
// TODO maybe use ExceptionGroup on Python 3.11+ ?
let mut err_msg = format!( let mut err_msg = format!(
"failed to extract enum {} ('{}')", "failed to extract enum {} ('{}')",
type_name, type_name,
@ -19,12 +20,23 @@ pub fn failed_to_extract_enum(
"- variant {variant_name} ({error_name}): {error_msg}", "- variant {variant_name} ({error_name}): {error_msg}",
variant_name = variant_name, variant_name = variant_name,
error_name = error_name, error_name = error_name,
error_msg = error.value(py).str().unwrap().to_str().unwrap(), error_msg = extract_traceback(py, error.clone_ref(py)),
)); ));
} }
PyTypeError::new_err(err_msg) PyTypeError::new_err(err_msg)
} }
/// Flattens a chain of errors into a single string.
fn extract_traceback(py: Python<'_>, mut error: PyErr) -> String {
let mut error_msg = error.to_string();
while let Some(cause) = error.cause(py) {
error_msg.push_str(", caused by ");
error_msg.push_str(&cause.to_string());
error = cause
}
error_msg
}
pub fn extract_struct_field<'py, T>( pub fn extract_struct_field<'py, T>(
obj: &'py PyAny, obj: &'py PyAny,
struct_name: &str, struct_name: &str,

View File

@ -282,7 +282,7 @@ fn test_transparent_tuple_error_message() {
assert!(tup.is_err()); assert!(tup.is_err());
assert_eq!( assert_eq!(
extract_traceback(py, tup.unwrap_err()), extract_traceback(py, tup.unwrap_err()),
"TypeError: failed to extract inner field of TransparentTuple: 'int' object \ "TypeError: failed to extract field TransparentTuple.0: TypeError: 'int' object \
cannot be converted to 'PyString'", cannot be converted to 'PyString'",
); );
}); });
@ -391,13 +391,13 @@ fn test_enum_error() {
err.to_string(), err.to_string(),
"\ "\
TypeError: failed to extract enum Foo ('TupleVar | StructVar | TransparentTuple | TransparentStructVar | StructVarGetAttrArg | StructWithGetItem | StructWithGetItemArg') TypeError: failed to extract enum Foo ('TupleVar | StructVar | TransparentTuple | TransparentStructVar | StructVarGetAttrArg | StructWithGetItem | StructWithGetItemArg')
- variant TupleVar (TupleVar): 'dict' object cannot be converted to 'PyTuple' - variant TupleVar (TupleVar): TypeError: 'dict' object cannot be converted to 'PyTuple'
- variant StructVar (StructVar): 'dict' object has no attribute 'test' - variant StructVar (StructVar): AttributeError: 'dict' object has no attribute 'test'
- variant TransparentTuple (TransparentTuple): 'dict' object cannot be interpreted as an integer - variant TransparentTuple (TransparentTuple): TypeError: failed to extract field Foo::TransparentTuple.0, caused by TypeError: 'dict' object cannot be interpreted as an integer
- variant TransparentStructVar (TransparentStructVar): failed to extract field Foo :: TransparentStructVar.a - variant TransparentStructVar (TransparentStructVar): TypeError: failed to extract field Foo::TransparentStructVar.a, caused by TypeError: 'dict' object cannot be converted to 'PyString'
- variant StructVarGetAttrArg (StructVarGetAttrArg): 'dict' object has no attribute 'bla' - variant StructVarGetAttrArg (StructVarGetAttrArg): AttributeError: 'dict' object has no attribute 'bla'
- variant StructWithGetItem (StructWithGetItem): 'a' - variant StructWithGetItem (StructWithGetItem): KeyError: 'a'
- variant StructWithGetItemArg (StructWithGetItemArg): 'foo'" - variant StructWithGetItemArg (StructWithGetItemArg): KeyError: 'foo'"
); );
let tup = PyTuple::empty(py); let tup = PyTuple::empty(py);
@ -406,13 +406,13 @@ TypeError: failed to extract enum Foo ('TupleVar | StructVar | TransparentTuple
err.to_string(), err.to_string(),
"\ "\
TypeError: failed to extract enum Foo ('TupleVar | StructVar | TransparentTuple | TransparentStructVar | StructVarGetAttrArg | StructWithGetItem | StructWithGetItemArg') TypeError: failed to extract enum Foo ('TupleVar | StructVar | TransparentTuple | TransparentStructVar | StructVarGetAttrArg | StructWithGetItem | StructWithGetItemArg')
- variant TupleVar (TupleVar): expected tuple of length 2, but got tuple of length 0 - variant TupleVar (TupleVar): ValueError: expected tuple of length 2, but got tuple of length 0
- variant StructVar (StructVar): 'tuple' object has no attribute 'test' - variant StructVar (StructVar): AttributeError: 'tuple' object has no attribute 'test'
- variant TransparentTuple (TransparentTuple): 'tuple' object cannot be interpreted as an integer - variant TransparentTuple (TransparentTuple): TypeError: failed to extract field Foo::TransparentTuple.0, caused by TypeError: 'tuple' object cannot be interpreted as an integer
- variant TransparentStructVar (TransparentStructVar): failed to extract field Foo :: TransparentStructVar.a - variant TransparentStructVar (TransparentStructVar): TypeError: failed to extract field Foo::TransparentStructVar.a, caused by TypeError: 'tuple' object cannot be converted to 'PyString'
- variant StructVarGetAttrArg (StructVarGetAttrArg): 'tuple' object has no attribute 'bla' - variant StructVarGetAttrArg (StructVarGetAttrArg): AttributeError: 'tuple' object has no attribute 'bla'
- variant StructWithGetItem (StructWithGetItem): tuple indices must be integers or slices, not str - variant StructWithGetItem (StructWithGetItem): TypeError: tuple indices must be integers or slices, not str
- variant StructWithGetItemArg (StructWithGetItemArg): tuple indices must be integers or slices, not str" - variant StructWithGetItemArg (StructWithGetItemArg): TypeError: tuple indices must be integers or slices, not str"
); );
}); });
} }
@ -463,10 +463,10 @@ fn test_err_rename() {
assert_eq!( assert_eq!(
f.unwrap_err().to_string(), f.unwrap_err().to_string(),
"\ "\
TypeError: failed to extract enum Bar (\'str | uint | int\') TypeError: failed to extract enum Bar ('str | uint | int')
- variant A (str): \'dict\' object cannot be converted to \'PyString\' - variant A (str): TypeError: failed to extract field Bar::A.0, caused by TypeError: 'dict' object cannot be converted to 'PyString'
- variant B (uint): \'dict\' object cannot be interpreted as an integer - variant B (uint): TypeError: failed to extract field Bar::B.0, caused by TypeError: 'dict' object cannot be interpreted as an integer
- variant C (int): \'dict\' object cannot be interpreted as an integer" - variant C (int): TypeError: failed to extract field Bar::C.0, caused by TypeError: 'dict' object cannot be interpreted as an integer"
); );
}); });
} }