Skip to content

Commit

Permalink
Remove lifetime from errors (#1084)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt committed Nov 21, 2023
1 parent 2bb53d8 commit be9c21c
Show file tree
Hide file tree
Showing 53 changed files with 294 additions and 319 deletions.
83 changes: 34 additions & 49 deletions src/errors/line_error.rs
Expand Up @@ -9,44 +9,54 @@ use crate::input::Input;
use super::location::{LocItem, Location};
use super::types::ErrorType;

pub type ValResult<'a, T> = Result<T, ValError<'a>>;
pub type ValResult<T> = Result<T, ValError>;

pub trait AsErrorValue {
fn as_error_value(&self) -> InputValue;
}

impl<'a, T: Input<'a>> AsErrorValue for T {
fn as_error_value(&self) -> InputValue {
Input::as_error_value(self)
}
}

#[cfg_attr(debug_assertions, derive(Debug))]
pub enum ValError<'a> {
LineErrors(Vec<ValLineError<'a>>),
pub enum ValError {
LineErrors(Vec<ValLineError>),
InternalErr(PyErr),
Omit,
UseDefault,
}

impl<'a> From<PyErr> for ValError<'a> {
impl From<PyErr> for ValError {
fn from(py_err: PyErr) -> Self {
Self::InternalErr(py_err)
}
}

impl<'a> From<PyDowncastError<'_>> for ValError<'a> {
impl From<PyDowncastError<'_>> for ValError {
fn from(py_downcast: PyDowncastError) -> Self {
Self::InternalErr(PyTypeError::new_err(py_downcast.to_string()))
}
}

impl<'a> From<Vec<ValLineError<'a>>> for ValError<'a> {
fn from(line_errors: Vec<ValLineError<'a>>) -> Self {
impl From<Vec<ValLineError>> for ValError {
fn from(line_errors: Vec<ValLineError>) -> Self {
Self::LineErrors(line_errors)
}
}

impl<'a> ValError<'a> {
pub fn new(error_type: ErrorType, input: &'a impl Input<'a>) -> ValError<'a> {
impl ValError {
pub fn new(error_type: ErrorType, input: &impl AsErrorValue) -> ValError {
Self::LineErrors(vec![ValLineError::new(error_type, input)])
}

pub fn new_with_loc(error_type: ErrorType, input: &'a impl Input<'a>, loc: impl Into<LocItem>) -> ValError<'a> {
pub fn new_with_loc(error_type: ErrorType, input: &impl AsErrorValue, loc: impl Into<LocItem>) -> ValError {
Self::LineErrors(vec![ValLineError::new_with_loc(error_type, input, loc)])
}

pub fn new_custom_input(error_type: ErrorType, input_value: InputValue<'a>) -> ValError<'a> {
pub fn new_custom_input(error_type: ErrorType, input_value: InputValue) -> ValError {
Self::LineErrors(vec![ValLineError::new_custom_input(error_type, input_value)])
}

Expand All @@ -62,55 +72,45 @@ impl<'a> ValError<'a> {
other => other,
}
}

/// a bit like clone but change the lifetime to match py
pub fn into_owned(self, py: Python<'_>) -> ValError<'_> {
match self {
ValError::LineErrors(errors) => errors.into_iter().map(|e| e.into_owned(py)).collect::<Vec<_>>().into(),
ValError::InternalErr(err) => ValError::InternalErr(err),
ValError::Omit => ValError::Omit,
ValError::UseDefault => ValError::UseDefault,
}
}
}

/// A `ValLineError` is a single error that occurred during validation which is converted to a `PyLineError`
/// to eventually form a `ValidationError`.
/// I don't like the name `ValLineError`, but it's the best I could come up with (for now).
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct ValLineError<'a> {
pub struct ValLineError {
pub error_type: ErrorType,
// location is reversed so that adding an "outer" location item is pushing, it's reversed before showing to the user
pub location: Location,
pub input_value: InputValue<'a>,
pub input_value: InputValue,
}

impl<'a> ValLineError<'a> {
pub fn new(error_type: ErrorType, input: &'a impl Input<'a>) -> ValLineError<'a> {
impl ValLineError {
pub fn new(error_type: ErrorType, input: &impl AsErrorValue) -> ValLineError {
Self {
error_type,
input_value: input.as_error_value(),
location: Location::default(),
}
}

pub fn new_with_loc(error_type: ErrorType, input: &'a impl Input<'a>, loc: impl Into<LocItem>) -> ValLineError<'a> {
pub fn new_with_loc(error_type: ErrorType, input: &impl AsErrorValue, loc: impl Into<LocItem>) -> ValLineError {
Self {
error_type,
input_value: input.as_error_value(),
location: Location::new_some(loc.into()),
}
}

pub fn new_with_full_loc(error_type: ErrorType, input: &'a impl Input<'a>, location: Location) -> ValLineError<'a> {
pub fn new_with_full_loc(error_type: ErrorType, input: &impl AsErrorValue, location: Location) -> ValLineError {
Self {
error_type,
input_value: input.as_error_value(),
location,
}
}

pub fn new_custom_input(error_type: ErrorType, input_value: InputValue<'a>) -> ValLineError<'a> {
pub fn new_custom_input(error_type: ErrorType, input_value: InputValue) -> ValLineError {
Self {
error_type,
input_value,
Expand All @@ -130,35 +130,20 @@ impl<'a> ValLineError<'a> {
self.error_type = error_type;
self
}

/// a bit like clone but change the lifetime to match py, used by ValError.into_owned above
pub fn into_owned(self, py: Python<'_>) -> ValLineError<'_> {
ValLineError {
error_type: self.error_type,
input_value: match self.input_value {
InputValue::PyAny(input) => InputValue::PyAny(input.to_object(py).into_ref(py)),
InputValue::JsonInput(input) => InputValue::JsonInput(input),
InputValue::String(input) => InputValue::PyAny(input.to_object(py).into_ref(py)),
},
location: self.location,
}
}
}

#[cfg_attr(debug_assertions, derive(Debug))]
#[derive(Clone)]
pub enum InputValue<'a> {
PyAny(&'a PyAny),
JsonInput(JsonValue),
String(&'a str),
pub enum InputValue {
Python(PyObject),
Json(JsonValue),
}

impl<'a> ToPyObject for InputValue<'a> {
impl ToPyObject for InputValue {
fn to_object(&self, py: Python) -> PyObject {
match self {
Self::PyAny(input) => input.into_py(py),
Self::JsonInput(input) => input.to_object(py),
Self::String(input) => input.into_py(py),
Self::Python(input) => input.clone_ref(py),
Self::Json(input) => input.to_object(py),
}
}
}
2 changes: 1 addition & 1 deletion src/errors/mod.rs
Expand Up @@ -6,7 +6,7 @@ mod types;
mod validation_exception;
mod value_exception;

pub use self::line_error::{InputValue, ValError, ValLineError, ValResult};
pub use self::line_error::{AsErrorValue, InputValue, ValError, ValLineError, ValResult};
pub use self::location::{AsLocItem, LocItem};
pub use self::types::{list_all_errors, ErrorType, ErrorTypeDefaults, Number};
pub use self::validation_exception::ValidationError;
Expand Down
20 changes: 8 additions & 12 deletions src/errors/validation_exception.rs
Expand Up @@ -225,12 +225,8 @@ fn get_url_prefix(py: Python, include_url: bool) -> Option<&str> {

// used to convert a validation error back to ValError for wrap functions
impl ValidationError {
pub(crate) fn into_val_error(self, py: Python<'_>) -> ValError<'_> {
self.line_errors
.into_iter()
.map(|e| e.into_val_line_error(py))
.collect::<Vec<_>>()
.into()
pub(crate) fn into_val_error(self) -> ValError {
self.line_errors.into_iter().map(Into::into).collect::<Vec<_>>().into()
}
}

Expand Down Expand Up @@ -416,7 +412,7 @@ pub struct PyLineError {
input_value: PyObject,
}

impl<'a> IntoPy<PyLineError> for ValLineError<'a> {
impl IntoPy<PyLineError> for ValLineError {
fn into_py(self, py: Python<'_>) -> PyLineError {
PyLineError {
error_type: self.error_type,
Expand All @@ -426,13 +422,13 @@ impl<'a> IntoPy<PyLineError> for ValLineError<'a> {
}
}

impl PyLineError {
impl From<PyLineError> for ValLineError {
/// Used to extract line errors from a validation error for wrap functions
fn into_val_line_error(self, py: Python<'_>) -> ValLineError<'_> {
fn from(other: PyLineError) -> ValLineError {
ValLineError {
error_type: self.error_type,
location: self.location,
input_value: InputValue::PyAny(self.input_value.into_ref(py)),
error_type: other.error_type,
location: other.location,
input_value: InputValue::Python(other.input_value),
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/errors/value_exception.rs
Expand Up @@ -2,9 +2,10 @@ use pyo3::exceptions::{PyException, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyString};

use crate::input::{Input, InputType};
use crate::input::InputType;
use crate::tools::extract_i64;

use super::line_error::AsErrorValue;
use super::{ErrorType, ValError};

#[pyclass(extends=PyException, module="pydantic_core._pydantic_core")]
Expand Down Expand Up @@ -105,7 +106,7 @@ impl PydanticCustomError {
}

impl PydanticCustomError {
pub fn into_val_error<'a>(self, input: &'a impl Input<'a>) -> ValError<'a> {
pub fn into_val_error(self, input: &impl AsErrorValue) -> ValError {
let error_type = ErrorType::CustomError {
error_type: self.error_type,
message_template: self.message_template,
Expand Down Expand Up @@ -184,7 +185,7 @@ impl PydanticKnownError {
}

impl PydanticKnownError {
pub fn into_val_error<'a>(self, input: &'a impl Input<'a>) -> ValError<'a> {
pub fn into_val_error(self, input: &impl AsErrorValue) -> ValError {
ValError::new(self.error_type, input)
}
}
10 changes: 5 additions & 5 deletions src/input/datetime.rs
Expand Up @@ -286,7 +286,7 @@ impl<'a> EitherDateTime<'a> {
}
}

pub fn bytes_as_date<'a>(input: &'a impl Input<'a>, bytes: &[u8]) -> ValResult<'a, EitherDate<'a>> {
pub fn bytes_as_date<'a>(input: &'a impl Input<'a>, bytes: &[u8]) -> ValResult<EitherDate<'a>> {
match Date::parse_bytes(bytes) {
Ok(date) => Ok(date.into()),
Err(err) => Err(ValError::new(
Expand All @@ -303,7 +303,7 @@ pub fn bytes_as_time<'a>(
input: &'a impl Input<'a>,
bytes: &[u8],
microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior,
) -> ValResult<'a, EitherTime<'a>> {
) -> ValResult<EitherTime<'a>> {
match Time::parse_bytes_with_config(
bytes,
&TimeConfig {
Expand All @@ -326,7 +326,7 @@ pub fn bytes_as_datetime<'a, 'b>(
input: &'a impl Input<'a>,
bytes: &'b [u8],
microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior,
) -> ValResult<'a, EitherDateTime<'a>> {
) -> ValResult<EitherDateTime<'a>> {
match DateTime::parse_bytes_with_config(
bytes,
&TimeConfig {
Expand Down Expand Up @@ -455,7 +455,7 @@ pub fn float_as_time<'a>(input: &'a impl Input<'a>, timestamp: f64) -> ValResult
int_as_time(input, timestamp.floor() as i64, microseconds.round() as u32)
}

fn map_timedelta_err<'a>(input: &'a impl Input<'a>, err: ParseError) -> ValError<'a> {
fn map_timedelta_err<'a>(input: &'a impl Input<'a>, err: ParseError) -> ValError {
ValError::new(
ErrorType::TimeDeltaParsing {
error: Cow::Borrowed(err.get_documentation().unwrap_or_default()),
Expand All @@ -469,7 +469,7 @@ pub fn bytes_as_timedelta<'a, 'b>(
input: &'a impl Input<'a>,
bytes: &'b [u8],
microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior,
) -> ValResult<'a, EitherTimedelta<'a>> {
) -> ValResult<EitherTimedelta<'a>> {
match Duration::parse_bytes_with_config(
bytes,
&TimeConfig {
Expand Down
22 changes: 9 additions & 13 deletions src/input/input_abstract.rs
Expand Up @@ -49,7 +49,7 @@ impl TryFrom<&str> for InputType {
/// * `strict_*` & `lax_*` if they have different behavior
/// * or, `validate_*` and `strict_*` to just call `validate_*` if the behavior for strict and lax is the same
pub trait Input<'a>: fmt::Debug + ToPyObject + AsLocItem + Sized {
fn as_error_value(&'a self) -> InputValue<'a>;
fn as_error_value(&self) -> InputValue;

fn identity(&self) -> Option<usize> {
None
Expand Down Expand Up @@ -85,11 +85,11 @@ pub trait Input<'a>: fmt::Debug + ToPyObject + AsLocItem + Sized {
false
}

fn validate_args(&'a self) -> ValResult<'a, GenericArguments<'a>>;
fn validate_args(&'a self) -> ValResult<GenericArguments<'a>>;

fn validate_dataclass_args(&'a self, dataclass_name: &str) -> ValResult<'a, GenericArguments<'a>>;
fn validate_dataclass_args(&'a self, dataclass_name: &str) -> ValResult<GenericArguments<'a>>;

fn parse_json(&'a self) -> ValResult<'a, JsonValue>;
fn parse_json(&'a self) -> ValResult<JsonValue>;

fn validate_str(
&'a self,
Expand All @@ -99,9 +99,9 @@ pub trait Input<'a>: fmt::Debug + ToPyObject + AsLocItem + Sized {

fn validate_bytes(&'a self, strict: bool) -> ValResult<ValidationMatch<EitherBytes<'a>>>;

fn validate_bool(&self, strict: bool) -> ValResult<'_, ValidationMatch<bool>>;
fn validate_bool(&self, strict: bool) -> ValResult<ValidationMatch<bool>>;

fn validate_int(&'a self, strict: bool) -> ValResult<'a, ValidationMatch<EitherInt<'a>>>;
fn validate_int(&'a self, strict: bool) -> ValResult<ValidationMatch<EitherInt<'a>>>;

fn exact_int(&'a self) -> ValResult<EitherInt<'a>> {
self.validate_int(true).and_then(|val_match| {
Expand All @@ -121,7 +121,7 @@ pub trait Input<'a>: fmt::Debug + ToPyObject + AsLocItem + Sized {
})
}

fn validate_float(&'a self, strict: bool) -> ValResult<'a, ValidationMatch<EitherFloat<'a>>>;
fn validate_float(&'a self, strict: bool) -> ValResult<ValidationMatch<EitherFloat<'a>>>;

fn validate_decimal(&'a self, strict: bool, py: Python<'a>) -> ValResult<&'a PyAny> {
if strict {
Expand Down Expand Up @@ -230,15 +230,11 @@ pub trait Input<'a>: fmt::Debug + ToPyObject + AsLocItem + Sized {
) -> ValResult<ValidationMatch<EitherTimedelta>>;
}

/// The problem to solve here is that iterating a `StringMapping` returns an owned
/// `StringMapping`, but all the other iterators return references. By introducing
/// The problem to solve here is that iterating collections often returns owned
/// values, but inputs are usually taken by reference. By introducing
/// this trait we abstract over whether the return value from the iterator is owned
/// or borrowed; all we care about is that we can borrow it again with `borrow_input`
/// for some lifetime 'a.
///
/// This lifetime `'a` is shorter than the original lifetime `'data` of the input,
/// which is only a problem in error branches. To resolve we have to call `into_owned`
/// to extend out the lifetime to match the original input.
pub trait BorrowInput {
type Input<'a>: Input<'a>
where
Expand Down

0 comments on commit be9c21c

Please sign in to comment.