Skip to content

Commit bd710f2

Browse files
authored
Make ValidationError constructor a non-init method (#601)
1 parent 47c2df1 commit bd710f2

File tree

3 files changed

+35
-27
lines changed

3 files changed

+35
-27
lines changed

pydantic_core/_pydantic_core.pyi

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,16 @@ class SchemaError(Exception):
171171
def errors(self) -> 'list[ErrorDetails]': ...
172172

173173
class ValidationError(ValueError):
174-
def __init__(
175-
self, title: str, errors: 'list[InitErrorDetails]', error_mode: Literal['python', 'json'] = 'python'
176-
) -> None: ...
174+
@staticmethod
175+
def from_exception_data(
176+
title: str, errors: 'list[InitErrorDetails]', error_mode: Literal['python', 'json'] = 'python'
177+
) -> ValidationError:
178+
"""
179+
Provisory constructor for a Validation Error.
180+
This API will probably change and be deprecated in the the future; we will make it easier and more
181+
powerful to construct and use ValidationErrors, but we cannot do that before our initial Pydantic V2 release.
182+
So if you use this method please be aware that it may change or be removed before Pydantic V3.
183+
"""
177184
@property
178185
def title(self) -> str: ...
179186
def error_count(self) -> int: ...

src/errors/validation_exception.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use super::types::{ErrorMode, ErrorType};
2424
use super::value_exception::PydanticCustomError;
2525
use super::ValError;
2626

27-
#[pyclass(subclass, extends=PyValueError, module="pydantic_core._pydantic_core")]
27+
#[pyclass(extends=PyValueError, module="pydantic_core._pydantic_core")]
2828
#[derive(Clone)]
2929
#[cfg_attr(debug_assertions, derive(Debug))]
3030
pub struct ValidationError {
@@ -123,13 +123,21 @@ impl<'a> IntoPy<ValError<'a>> for ValidationError {
123123

124124
#[pymethods]
125125
impl ValidationError {
126-
#[new]
127-
fn py_new(title: PyObject, line_errors: &PyList, error_mode: Option<&str>) -> PyResult<Self> {
128-
Ok(Self {
129-
line_errors: line_errors.iter().map(PyLineError::try_from).collect::<PyResult<_>>()?,
130-
title,
131-
error_mode: ErrorMode::try_from(error_mode)?,
132-
})
126+
#[staticmethod]
127+
fn from_exception_data(
128+
py: Python,
129+
title: PyObject,
130+
line_errors: &PyList,
131+
error_mode: Option<&str>,
132+
) -> PyResult<Py<Self>> {
133+
Py::new(
134+
py,
135+
Self {
136+
line_errors: line_errors.iter().map(PyLineError::try_from).collect::<PyResult<_>>()?,
137+
title,
138+
error_mode: ErrorMode::try_from(error_mode)?,
139+
},
140+
)
133141
}
134142

135143
#[getter]

tests/test_errors.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -553,27 +553,31 @@ def test_error_json_loc():
553553

554554
def test_raise_validation_error():
555555
with pytest.raises(ValidationError, match='1 validation error for Foobar\n') as exc_info:
556-
raise ValidationError('Foobar', [{'type': 'greater_than', 'loc': ('a', 2), 'input': 4, 'ctx': {'gt': 5}}])
556+
raise ValidationError.from_exception_data(
557+
'Foobar', [{'type': 'greater_than', 'loc': ('a', 2), 'input': 4, 'ctx': {'gt': 5}}]
558+
)
557559

558560
# insert_assert(exc_info.value.errors(include_url=False))
559561
assert exc_info.value.errors(include_url=False) == [
560562
{'type': 'greater_than', 'loc': (2, 'a'), 'msg': 'Input should be greater than 5', 'input': 4, 'ctx': {'gt': 5}}
561563
]
562564
with pytest.raises(TypeError, match='GreaterThan requires context: {gt: Number}'):
563-
raise ValidationError('Foobar', [{'type': 'greater_than', 'loc': ('a', 2), 'input': 4}])
565+
raise ValidationError.from_exception_data('Foobar', [{'type': 'greater_than', 'loc': ('a', 2), 'input': 4}])
564566

565567

566568
def test_raise_validation_error_json():
567569
with pytest.raises(ValidationError) as exc_info:
568-
raise ValidationError('Foobar', [{'type': 'none_required', 'loc': [-42], 'input': 'x'}])
570+
raise ValidationError.from_exception_data('Foobar', [{'type': 'none_required', 'loc': [-42], 'input': 'x'}])
569571

570572
# insert_assert(exc_info.value.errors(include_url=False))
571573
assert exc_info.value.errors(include_url=False) == [
572574
{'type': 'none_required', 'loc': (-42,), 'msg': 'Input should be None', 'input': 'x'}
573575
]
574576

575577
with pytest.raises(ValidationError) as exc_info:
576-
raise ValidationError('Foobar', [{'type': 'none_required', 'loc': (), 'input': 'x'}], 'json')
578+
raise ValidationError.from_exception_data(
579+
'Foobar', [{'type': 'none_required', 'loc': (), 'input': 'x'}], 'json'
580+
)
577581

578582
# insert_assert(exc_info.value.errors(include_url=False))
579583
assert exc_info.value.errors(include_url=False) == [
@@ -586,7 +590,7 @@ def test_raise_validation_error_custom():
586590
'my_error', 'this is a custom error {missed} {foo} {bar}', {'foo': 'X', 'bar': 42}
587591
)
588592
with pytest.raises(ValidationError) as exc_info:
589-
raise ValidationError('Foobar', [{'type': custom_error, 'input': 'x'}])
593+
raise ValidationError.from_exception_data('Foobar', [{'type': custom_error, 'input': 'x'}])
590594

591595
# insert_assert(exc_info.value.errors(include_url=False))
592596
assert exc_info.value.errors(include_url=False) == [
@@ -631,14 +635,3 @@ def test_loc_with_dots():
631635
"[type=int_parsing, input_value='x', input_type=str]\n"
632636
f' For further information visit https://errors.pydantic.dev/{__version__}/v/int_parsing'
633637
)
634-
635-
636-
def subclass_validation_error():
637-
class MyValidationError(ValidationError):
638-
def __init__(self, title: str, errors, error_mode='python'):
639-
self.body = 123
640-
super().__init__(title, errors, error_mode)
641-
642-
e = MyValidationError('testing', [])
643-
assert e.body == 123
644-
assert e.title == 'testing'

0 commit comments

Comments
 (0)