Added custom error messages to enum fields and container types. Covered with tests. Fixed formatting and linting issues

This commit is contained in:
R2D2 2021-06-01 22:44:16 +02:00
parent dfc484c43b
commit 416b1132b4
4 changed files with 188 additions and 35 deletions

View File

@ -12,7 +12,7 @@ pub mod kw {
syn::custom_keyword!(annotation);
syn::custom_keyword!(attribute);
syn::custom_keyword!(from_py_with);
syn::custom_keyword!(conversion_error);
syn::custom_keyword!(error_message);
syn::custom_keyword!(item);
syn::custom_keyword!(pass_module);
syn::custom_keyword!(name);

View File

@ -51,8 +51,8 @@ impl<'a> Enum<'a> {
/// Build derivation body for enums.
fn build(&self) -> TokenStream {
let mut var_extracts = Vec::new();
let mut error_names: Vec<String> = Vec::new();
for var in &self.variants {
let mut error_names = String::new();
for (i, var) in self.variants.iter().enumerate() {
let struct_derive = var.build();
let ext = quote!(
let maybe_ret = || -> pyo3::PyResult<Self> {
@ -67,12 +67,15 @@ impl<'a> Enum<'a> {
);
var_extracts.push(ext);
error_names.push(var.err_name.clone());
error_names.push_str(&var.err_name);
if i < self.variants.len() - 1 {
error_names.push_str(", ");
}
}
let error_names = if self.variants.len() > 1 {
format!("Union[{}]", error_names.join(","))
format!("Union[{}]", error_names)
} else {
error_names[0].clone()
error_names
};
let ty_name = self.enum_ident.to_string();
quote!(
@ -121,6 +124,7 @@ struct Container<'a> {
ty: ContainerType<'a>,
err_name: String,
is_enum_variant: bool,
error_msg: Option<String>,
}
impl<'a> Container<'a> {
@ -180,11 +184,14 @@ impl<'a> Container<'a> {
|lit_str| lit_str.value(),
);
let error_msg = options.error_msg.map(|inner| inner.value());
let v = Container {
path,
ty: style,
err_name,
is_enum_variant,
error_msg,
};
Ok(v)
}
@ -202,19 +209,30 @@ impl<'a> Container<'a> {
fn build_newtype_struct(&self, field_ident: Option<&Ident>) -> TokenStream {
let self_ty = &self.path;
if let Some(ident) = field_ident {
let error_msg = self.error_msg.as_ref().map_or(
format!(
"failed to extract field {}.{}",
quote!(#self_ty),
quote!(#ident)
),
|msg| msg.clone(),
);
quote!(
Ok(#self_ty{#ident: obj.extract().map_err(|inner| {
let err_msg = format!("failed to extract field {}.{}\n\nCaused by:\n {}\n",
stringify!(#self_ty),
stringify!(#ident),
let err_msg = format!("{}\n\nCaused by:\n {}\n",
#error_msg,
inner);
pyo3::exceptions::PyTypeError::new_err(err_msg)
})?})
)
} else {
let error_msg = self.error_msg.as_ref().map_or(
format!("failed to extract inner field of {}", quote!(#self_ty)),
|msg| msg.clone(),
);
quote!(Ok(#self_ty(obj.extract().map_err(|inner| {
let err_msg = format!("failed to extract inner field of {}\n\nCaused by:\n {}\n",
stringify!(#self_ty),
let err_msg = format!("{}\n\nCaused by:\n {}\n",
#error_msg,
inner);
pyo3::exceptions::PyTypeError::new_err(err_msg)
})?)))
@ -224,11 +242,15 @@ impl<'a> Container<'a> {
fn build_tuple_struct(&self, len: usize) -> TokenStream {
let self_ty = &self.path;
let mut fields: Punctuated<TokenStream, syn::Token![,]> = Punctuated::new();
for i in 0..len {
let error_msg = self.error_msg.as_ref().map_or(
format!("failed to extract field {}.{}", quote!(#self_ty), i),
|msg| msg.clone(),
);
fields.push(quote!(s.get_item(#i).extract().map_err(|inner| {
let err_msg = format!("failed to extract field {}.{}\n\nCaused by:\n {}\n",
stringify!(#self_ty),
#i,
let err_msg = format!("{}\n\nCaused by:\n {}\n",
#error_msg,
inner);
pyo3::exceptions::PyTypeError::new_err(err_msg)
})?));
@ -261,7 +283,7 @@ impl<'a> Container<'a> {
FieldGetter::GetItem(Some(key)) => quote!(get_item(#key)),
FieldGetter::GetItem(None) => quote!(get_item(stringify!(#ident))),
};
let conversion_error_msg = attrs.conversion_error.as_ref().map_or(
let conversion_error_msg = attrs.error_msg.as_ref().map_or(
format!("failed to extract field {}.{}", quote!(#self_ty), ident),
|msg| msg.value(),
);
@ -293,6 +315,8 @@ struct ContainerOptions {
transparent: bool,
/// Change the name of an enum variant in the generated error message.
annotation: Option<syn::LitStr>,
/// Change the error message displayed when extract fails
error_msg: Option<syn::LitStr>,
}
/// Attributes for deriving FromPyObject scoped on containers.
@ -302,6 +326,8 @@ enum ContainerPyO3Attribute {
Transparent(attributes::kw::transparent),
/// Change the name of an enum variant in the generated error message.
ErrorAnnotation(LitStr),
/// Change the error message displayed when extract fails
ErrorMsg(LitStr),
}
impl Parse for ContainerPyO3Attribute {
@ -314,6 +340,10 @@ impl Parse for ContainerPyO3Attribute {
let _: attributes::kw::annotation = input.parse()?;
let _: Token![=] = input.parse()?;
input.parse().map(ContainerPyO3Attribute::ErrorAnnotation)
} else if lookahead.peek(attributes::kw::error_message) {
let _: attributes::kw::error_message = input.parse()?;
let _: Token![=] = input.parse()?;
input.parse().map(ContainerPyO3Attribute::ErrorMsg)
} else {
Err(lookahead.error())
}
@ -325,6 +355,7 @@ impl ContainerOptions {
let mut options = ContainerOptions {
transparent: false,
annotation: None,
error_msg: None,
};
for attr in attrs {
if let Some(pyo3_attrs) = get_pyo3_attributes(attr)? {
@ -344,6 +375,13 @@ impl ContainerOptions {
);
options.annotation = Some(lit_str);
}
ContainerPyO3Attribute::ErrorMsg(lit_str) => {
ensure_spanned!(
options.error_msg.is_none(),
lit_str.span() => "`error_message` may only be provided once"
);
options.error_msg = Some(lit_str);
}
}
}
}
@ -357,7 +395,7 @@ impl ContainerOptions {
struct FieldPyO3Attributes {
getter: FieldGetter,
from_py_with: Option<FromPyWithAttribute>,
conversion_error: Option<LitStr>,
error_msg: Option<LitStr>,
}
#[derive(Clone, Debug)]
@ -369,7 +407,7 @@ enum FieldGetter {
enum FieldPyO3Attribute {
Getter(FieldGetter),
FromPyWith(FromPyWithAttribute),
ConversionError(LitStr),
ErrorMsg(LitStr),
}
impl Parse for FieldPyO3Attribute {
@ -413,10 +451,10 @@ impl Parse for FieldPyO3Attribute {
}
} else if lookahead.peek(attributes::kw::from_py_with) {
input.parse().map(FieldPyO3Attribute::FromPyWith)
} else if lookahead.peek(attributes::kw::conversion_error) {
let _: attributes::kw::conversion_error = input.parse()?;
} else if lookahead.peek(attributes::kw::error_message) {
let _: attributes::kw::error_message = input.parse()?;
let _: Token![=] = input.parse()?;
input.parse().map(FieldPyO3Attribute::ConversionError)
input.parse().map(FieldPyO3Attribute::ErrorMsg)
} else {
Err(lookahead.error())
}
@ -429,7 +467,7 @@ impl FieldPyO3Attributes {
fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
let mut getter = None;
let mut from_py_with = None;
let mut conversion_error = None;
let mut error_msg = None;
for attr in attrs {
if let Some(pyo3_attrs) = get_pyo3_attributes(attr)? {
@ -449,12 +487,12 @@ impl FieldPyO3Attributes {
);
from_py_with = Some(from_py_with_attr);
}
FieldPyO3Attribute::ConversionError(conversion_error_msg) => {
FieldPyO3Attribute::ErrorMsg(err_msg) => {
ensure_spanned!(
conversion_error.is_none(),
error_msg.is_none(),
attr.span() => "`conversion_error` may only be provided once"
);
conversion_error = Some(conversion_error_msg)
error_msg = Some(err_msg)
}
}
}
@ -464,7 +502,7 @@ impl FieldPyO3Attributes {
Ok(FieldPyO3Attributes {
getter: getter.unwrap_or(FieldGetter::GetAttr(None)),
from_py_with,
conversion_error,
error_msg,
})
}
}

View File

@ -199,11 +199,11 @@ fn test_struct_nested_type_errors() {
}
.into_py(py);
let baz: PyResult<Baz<String, usize>> = FromPyObject::extract(pybaz.as_ref(py));
assert!(baz.is_err());
let test: PyResult<Baz<String, usize>> = FromPyObject::extract(pybaz.as_ref(py));
assert!(test.is_err());
assert_eq!(
"TypeError: failed to extract field Baz.tup\n\nCaused by:\n TypeError: failed to extract field Tuple.1\n\nCaused by:\n TypeError: 'str' object cannot be interpreted as an integer\n\n",
baz.unwrap_err().to_string()
test.unwrap_err().to_string()
);
}
@ -221,11 +221,122 @@ fn test_struct_nested_type_errors_with_generics() {
}
.into_py(py);
let baz: PyResult<Baz<usize, String>> = FromPyObject::extract(pybaz.as_ref(py));
assert!(baz.is_err());
let test: PyResult<Baz<usize, String>> = FromPyObject::extract(pybaz.as_ref(py));
assert!(test.is_err());
assert_eq!(
"TypeError: failed to extract field Baz.e\n\nCaused by:\n TypeError: failed to extract field E.test\n\nCaused by:\n TypeError: \'str\' object cannot be interpreted as an integer\n\n",
baz.unwrap_err().to_string()
test.unwrap_err().to_string()
);
}
#[derive(Debug, FromPyObject)]
struct Custom<U, T> {
e: E<U, T>,
#[pyo3(error_message = "Type 'Tuple' should be of the form (str, integer)")]
tup: Tuple,
}
#[test]
fn test_custom_error_message_struct() {
let gil = Python::acquire_gil();
let py = gil.python();
let pybaz = PyBaz {
tup: ("test".into(), "test".into()),
e: PyE {
test: "foo".into(),
test2: 0,
},
}
.into_py(py);
let test: PyResult<Custom<String, usize>> = FromPyObject::extract(pybaz.as_ref(py));
assert!(test.is_err());
assert_eq!(
"TypeError: Type \'Tuple\' should be of the form (str, integer)\n\nCaused by:\n TypeError: failed to extract field Tuple.1\n\nCaused by:\n TypeError: \'str\' object cannot be interpreted as an integer\n\n",
test.unwrap_err().to_string()
);
}
#[derive(Debug, FromPyObject)]
#[pyo3(error_message = "Conversion failed: Tuple expects 'str', 'usize'")]
pub struct Tuple2(String, usize);
#[test]
fn test_custom_error_message_tuple_struct() {
let gil = Python::acquire_gil();
let py = gil.python();
let pytup = PyTuple::new(py, &["test".into_py(py), "test".into_py(py)]);
let test: PyResult<Tuple2> = FromPyObject::extract(pytup);
assert!(test.is_err());
assert_eq!(
"TypeError: Conversion failed: Tuple expects \'str\', \'usize\'\n\nCaused by:\n TypeError: \'str\' object cannot be interpreted as an integer\n",
test.unwrap_err().to_string()
);
}
#[derive(Debug, FromPyObject)]
#[pyo3(error_message = "Expected type str")]
pub struct TransparentTuple2(String);
#[test]
fn test_transparent_tuple_struct_error_message() {
let gil = Python::acquire_gil();
let py = gil.python();
let tup: PyObject = 1.into_py(py);
let tup = TransparentTuple2::extract(tup.as_ref(py));
assert!(tup.is_err());
assert_eq!(
"TypeError: Expected type str\n\nCaused by:\n TypeError: \'int\' object cannot be converted to \'PyString\'\n",
tup.unwrap_err().to_string()
);
}
#[derive(Debug, FromPyObject)]
#[pyo3(transparent, error_message = "Expected type str")]
pub struct TransparentStruct {
a: String,
}
#[test]
fn test_transparent_struct_error_message() {
let gil = Python::acquire_gil();
let py = gil.python();
let tup: PyObject = 1.into_py(py);
let tup = TransparentStruct::extract(tup.as_ref(py));
assert!(tup.is_err());
assert_eq!(
"TypeError: Expected type str\n\nCaused by:\n TypeError: \'int\' object cannot be converted to \'PyString\'\n",
tup.unwrap_err().to_string()
);
}
#[derive(Debug, FromPyObject)]
struct Custom2<U, T> {
e: E<U, T>,
#[pyo3(error_message = "Type 'Tuple' should be of the form (str, integer)")]
tup: Tuple2,
}
#[test]
fn test_nested_custom_errors() {
let gil = Python::acquire_gil();
let py = gil.python();
let pybaz = PyBaz {
tup: ("test".into(), "test".into()),
e: PyE {
test: "foo".into(),
test2: 0,
},
}
.into_py(py);
let test: PyResult<Custom2<String, usize>> = FromPyObject::extract(pybaz.as_ref(py));
assert!(test.is_err());
assert_eq!(
"TypeError: Type \'Tuple\' should be of the form (str, integer)\n\nCaused by:\n TypeError: Conversion failed: Tuple expects \'str\', \'usize\'\n\nCaused by:\n TypeError: \'str\' object cannot be interpreted as an integer\n\n",
test.unwrap_err().to_string()
);
}
@ -342,7 +453,11 @@ pub enum Bar {
A(String),
#[pyo3(annotation = "uint")]
B(usize),
#[pyo3(annotation = "int", transparent)]
#[pyo3(
annotation = "int",
transparent,
error_message = "Bar::C expects an integer type"
)]
C(isize),
}
@ -355,7 +470,7 @@ fn test_err_rename() {
assert!(f.is_err());
assert_eq!(
f.unwrap_err().to_string(),
"TypeError: Failed to extract type Bar\n\nCaused by:\n TypeError: \'dict\' object cannot be converted to \'Union[str,uint,int]\'\n\nTypeError: failed to extract inner field of Bar :: A\n\nCaused by:\n TypeError: \'dict\' object cannot be converted to \'PyString\'\n\nTypeError: failed to extract inner field of Bar :: B\n\nCaused by:\n TypeError: \'dict\' object cannot be interpreted as an integer\n\nTypeError: failed to extract inner field of Bar :: C\n\nCaused by:\n TypeError: \'dict\' object cannot be interpreted as an integer\n\n"
"TypeError: Failed to extract type Bar\n\nCaused by:\n TypeError: \'dict\' object cannot be converted to \'Union[str, uint, int]\'\n\nTypeError: failed to extract inner field of Bar :: A\n\nCaused by:\n TypeError: \'dict\' object cannot be converted to \'PyString\'\n\nTypeError: failed to extract inner field of Bar :: B\n\nCaused by:\n TypeError: \'dict\' object cannot be interpreted as an integer\n\nTypeError: Bar::C expects an integer type\n\nCaused by:\n TypeError: \'dict\' object cannot be interpreted as an integer\n\n"
);
}

View File

@ -84,7 +84,7 @@ error: transparent structs and variants can only have 1 field
70 | | },
| |_____^
error: expected one of: `attribute`, `item`, `from_py_with`, `conversion_error`
error: expected one of: `attribute`, `item`, `from_py_with`, `error_message`
--> $DIR/invalid_frompy_derive.rs:76:12
|
76 | #[pyo3(attr)]
@ -132,7 +132,7 @@ error: only one of `attribute` or `item` can be provided
118 | #[pyo3(item, attribute)]
| ^
error: expected `transparent` or `annotation`
error: expected one of: `transparent`, `annotation`, `error_message`
--> $DIR/invalid_frompy_derive.rs:123:8
|
123 | #[pyo3(unknown = "should not work")]