Added custom error messages to enum fields and container types. Covered with tests. Fixed formatting and linting issues
This commit is contained in:
parent
dfc484c43b
commit
416b1132b4
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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")]
|
||||
|
|
Loading…
Reference in New Issue