Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ class MultiHostUrl:
def __repr__(self) -> str: ...

class SchemaError(Exception):
pass
def error_count(self) -> int: ...
def errors(self) -> 'list[ErrorDetails]': ...

class ValidationError(ValueError):
@property
Expand Down
70 changes: 53 additions & 17 deletions src/build_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use std::fmt;

use pyo3::exceptions::{PyException, PyKeyError};
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyString};
use pyo3::types::{PyDict, PyList, PyString};
use pyo3::{intern, FromPyObject, PyErrArguments};

use crate::errors::{pretty_line_errors, ValError};
use crate::errors::ValError;
use crate::ValidationError;

pub trait SchemaDict<'py> {
fn get_as<T>(&'py self, key: &PyString) -> PyResult<Option<T>>
Expand Down Expand Up @@ -98,22 +99,25 @@ pub fn is_strict(schema: &PyDict, config: Option<&PyDict>) -> PyResult<bool> {
Ok(schema_or_config_same(schema, config, intern!(py, "strict"))?.unwrap_or(false))
}

enum SchemaErrorEnum {
Message(String),
ValidationError(ValidationError),
}

// we could perhaps do clever things here to store each schema error, or have different types for the top
// level error group, and other errors, we could perhaps also support error groups!?
#[pyclass(extends=PyException, module="pydantic_core._pydantic_core")]
pub struct SchemaError {
message: String,
}
pub struct SchemaError(SchemaErrorEnum);

impl fmt::Debug for SchemaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SchemaError({:?})", self.message)
write!(f, "SchemaError({:?})", self.message())
}
}

impl fmt::Display for SchemaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
f.write_str(self.message())
}
}

Expand All @@ -134,12 +138,24 @@ impl SchemaError {

pub fn from_val_error(py: Python, error: ValError) -> PyErr {
match error {
ValError::LineErrors(line_errors) => {
let details = pretty_line_errors(py, line_errors);
SchemaError::new_err(format!("Invalid Schema:\n{details}"))
ValError::LineErrors(raw_errors) => {
let line_errors = raw_errors.into_iter().map(|e| e.into_py(py)).collect();
let validation_error = ValidationError::new(line_errors, "Schema".to_object(py));
let schema_error = SchemaError(SchemaErrorEnum::ValidationError(validation_error));
match Py::new(py, schema_error) {
Ok(err) => PyErr::from_value(err.into_ref(py)),
Err(err) => err,
}
}
ValError::InternalErr(py_err) => py_err,
ValError::Omit => unreachable!(),
ValError::InternalErr(err) => err,
ValError::Omit => Self::new_err("Unexpected Omit error."),
}
}

fn message(&self) -> &str {
match &self.0 {
SchemaErrorEnum::Message(message) => message.as_str(),
SchemaErrorEnum::ValidationError(_) => "<ValidationError>",
}
}
}
Expand All @@ -148,15 +164,35 @@ impl SchemaError {
impl SchemaError {
#[new]
fn py_new(message: String) -> Self {
Self { message }
Self(SchemaErrorEnum::Message(message))
}

fn error_count(&self) -> usize {
match &self.0 {
SchemaErrorEnum::Message(_) => 0,
SchemaErrorEnum::ValidationError(error) => error.error_count(),
}
}

fn __repr__(&self) -> String {
format!("{self:?}")
fn errors(&self, py: Python) -> PyResult<Py<PyList>> {
match &self.0 {
SchemaErrorEnum::Message(_) => Ok(PyList::empty(py).into_py(py)),
SchemaErrorEnum::ValidationError(error) => error.errors(py, None),
}
}

fn __str__(&self) -> String {
self.to_string()
fn __str__(&self, py: Python) -> String {
match &self.0 {
SchemaErrorEnum::Message(message) => message.to_owned(),
SchemaErrorEnum::ValidationError(error) => error.display(py),
}
}

fn __repr__(&self, py: Python) -> String {
match &self.0 {
SchemaErrorEnum::Message(message) => format!("SchemaError({message:?})"),
SchemaErrorEnum::ValidationError(error) => error.display(py),
}
}
}

Expand Down
6 changes: 0 additions & 6 deletions src/errors/line_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use crate::input::{Input, JsonInput};

use super::location::{LocItem, Location};
use super::types::ErrorType;
use super::validation_exception::{pretty_py_line_errors, PyLineError};

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

Expand Down Expand Up @@ -71,11 +70,6 @@ impl<'a> ValError<'a> {
}
}

pub fn pretty_line_errors(py: Python, line_errors: Vec<ValLineError>) -> String {
let py_line_errors: Vec<PyLineError> = line_errors.into_iter().map(|e| e.into_py(py)).collect();
pretty_py_line_errors(py, py_line_errors.iter())
}

/// 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).
Expand Down
2 changes: 1 addition & 1 deletion src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod types;
mod validation_exception;
mod value_exception;

pub use self::line_error::{pretty_line_errors, InputValue, ValError, ValLineError, ValResult};
pub use self::line_error::{InputValue, ValError, ValLineError, ValResult};
pub use self::location::LocItem;
pub use self::types::{list_all_errors, ErrorType};
pub use self::validation_exception::ValidationError;
Expand Down
10 changes: 7 additions & 3 deletions src/errors/validation_exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub struct ValidationError {
}

impl ValidationError {
pub fn new(line_errors: Vec<PyLineError>, title: PyObject) -> Self {
Self { line_errors, title }
}

pub fn from_val_error(py: Python, title: PyObject, error: ValError, outer_location: Option<LocItem>) -> PyErr {
match error {
ValError::LineErrors(raw_errors) => {
Expand All @@ -41,7 +45,7 @@ impl ValidationError {
}
}

fn display(&self, py: Python) -> String {
pub fn display(&self, py: Python) -> String {
let count = self.line_errors.len();
let plural = if count == 1 { "" } else { "s" };
let title: &str = self.title.extract(py).unwrap();
Expand Down Expand Up @@ -77,11 +81,11 @@ impl ValidationError {
self.title.clone_ref(py)
}

fn error_count(&self) -> usize {
pub fn error_count(&self) -> usize {
self.line_errors.len()
}

fn errors(&self, py: Python, include_context: Option<bool>) -> PyResult<Py<PyList>> {
pub fn errors(&self, py: Python, include_context: Option<bool>) -> PyResult<Py<PyList>> {
// taken approximately from the pyo3, but modified to return the error during iteration
// https://github.com/PyO3/pyo3/blob/a3edbf4fcd595f0e234c87d4705eb600a9779130/src/types/list.rs#L27-L55
unsafe {
Expand Down
4 changes: 2 additions & 2 deletions tests/serializers/test_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ def test_def_error():
)
)

assert exc_info.value.args[0].startswith(
"Invalid Schema:\ndefinitions -> definitions -> 1\n Input tag 'wrong' found using 'type'"
assert str(exc_info.value).startswith(
"1 validation error for Schema\ndefinitions -> definitions -> 1\n Input tag 'wrong' found using 'type'"
)


Expand Down
4 changes: 2 additions & 2 deletions tests/serializers/test_list_tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ class RemovedContains(ImplicitContains):
({4.2}, 'Input should be a valid integer, got a number with a fractional part'),
({'a'}, 'Input should be a valid integer, unable to parse string as an integer'),
(ImplicitContains(), 'Input should be a valid set'),
(ExplicitContains(), re.compile('.*Invalid Schema:.*Input should be a valid set.*', re.DOTALL)),
(RemovedContains(), re.compile('.*Invalid Schema:.*Input should be a valid set.*', re.DOTALL)),
(ExplicitContains(), re.compile('.*1 validation error for Schema.*Input should be a valid set.*', re.DOTALL)),
(RemovedContains(), re.compile('.*1 validation error for Schema.*Input should be a valid set.*', re.DOTALL)),
],
)
@pytest.mark.parametrize('schema_func', [core_schema.list_schema, core_schema.tuple_variable_schema])
Expand Down
13 changes: 11 additions & 2 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,19 @@ def test_schema_as_string():
def test_schema_wrong_type():
with pytest.raises(SchemaError) as exc_info:
SchemaValidator(1)
assert exc_info.value.args[0] == (
'Invalid Schema:\n Input should be a valid dictionary or instance to'
assert str(exc_info.value) == (
'1 validation error for Schema\n Input should be a valid dictionary or instance to'
' extract fields from [type=dict_attributes_type, input_value=1, input_type=int]'
)
assert exc_info.value.errors() == [
{
'input': 1,
'loc': (),
'msg': 'Input should be a valid dictionary or instance to extract fields ' 'from',
'type': 'dict_attributes_type',
}
]
assert exc_info.value.error_count() == 1


@pytest.mark.parametrize('pickle_protocol', range(1, pickle.HIGHEST_PROTOCOL + 1))
Expand Down
5 changes: 3 additions & 2 deletions tests/validators/test_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ def test_def_error():
[core_schema.int_schema(ref='foobar'), {'type': 'wrong'}],
)
)
assert exc_info.value.args[0].startswith(
"Invalid Schema:\ndefinitions -> definitions -> 1\n Input tag 'wrong' found using 'type'"
assert str(exc_info.value).startswith(
"1 validation error for Schema\ndefinitions -> definitions -> 1\n Input tag 'wrong' found using 'type'"
)
assert exc_info.value.error_count() == 1


def test_dict_repeat():
Expand Down
8 changes: 6 additions & 2 deletions tests/validators/test_union.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,15 @@ def test_no_choices():
with pytest.raises(SchemaError) as exc_info:
SchemaValidator({'type': 'union'})

assert exc_info.value.args[0] == (
'Invalid Schema:\n'
assert str(exc_info.value) == (
'1 validation error for Schema\n'
'union -> choices\n'
" Field required [type=missing, input_value={'type': 'union'}, input_type=dict]"
)
assert exc_info.value.error_count() == 1
assert exc_info.value.errors() == [
{'input': {'type': 'union'}, 'loc': ('union', 'choices'), 'msg': 'Field required', 'type': 'missing'}
]


def test_empty_choices():
Expand Down