Skip to content

Commit 477ee17

Browse files
authored
cleanup mod and fix a few things (#107)
* cleanup mod and fix a few things * tweak order * linting * improve coverage
1 parent 9330a17 commit 477ee17

File tree

6 files changed

+152
-21
lines changed

6 files changed

+152
-21
lines changed

pydantic_core/_pydantic_core.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class SchemaError(ValueError):
1414
pass
1515

1616
class ValidationError(ValueError):
17-
model_name: str
17+
title: str
1818

1919
def error_count(self) -> int: ...
2020
def errors(self) -> List[Dict[str, Any]]: ...

src/build_tools.rs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
use pyo3::exceptions::{PyKeyError, PyTypeError};
1+
use std::error::Error;
2+
use std::fmt;
3+
4+
use pyo3::exceptions::{PyException, PyKeyError, PyTypeError};
25
use pyo3::prelude::*;
36
use pyo3::types::PyDict;
4-
use pyo3::FromPyObject;
7+
use pyo3::{FromPyObject, PyErrArguments};
58

69
pub trait SchemaDict<'py> {
710
fn get_as<T>(&'py self, key: &str) -> PyResult<Option<T>>
@@ -80,12 +83,63 @@ pub fn is_strict(schema: &PyDict, config: Option<&PyDict>) -> PyResult<bool> {
8083
Ok(schema_or_config(schema, config, "strict", "strict")?.unwrap_or(false))
8184
}
8285

86+
// we could perhaps do clever things here to store each schema error, or have different types for the top
87+
// level error group, and other errors, we could perhaps also support error groups!?
88+
#[pyclass(extends=PyException, module="pydantic_core._pydantic_core")]
89+
pub struct SchemaError {
90+
message: String,
91+
}
92+
93+
impl fmt::Debug for SchemaError {
94+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95+
write!(f, "SchemaError({:?})", self.message)
96+
}
97+
}
98+
99+
impl fmt::Display for SchemaError {
100+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101+
write!(f, "{}", self.message)
102+
}
103+
}
104+
105+
impl Error for SchemaError {
106+
#[cfg_attr(has_no_coverage, no_coverage)]
107+
fn source(&self) -> Option<&(dyn Error + 'static)> {
108+
None
109+
}
110+
}
111+
112+
impl SchemaError {
113+
pub fn new_err<A>(args: A) -> PyErr
114+
where
115+
A: PyErrArguments + Send + Sync + 'static,
116+
{
117+
PyErr::new::<SchemaError, A>(args)
118+
}
119+
}
120+
121+
#[pymethods]
122+
impl SchemaError {
123+
#[new]
124+
fn py_new(message: String) -> Self {
125+
Self { message }
126+
}
127+
128+
fn __repr__(&self) -> String {
129+
format!("{:?}", self)
130+
}
131+
132+
fn __str__(&self) -> String {
133+
self.to_string()
134+
}
135+
}
136+
83137
macro_rules! py_error {
84138
($msg:expr) => {
85-
crate::build_tools::py_error!(crate::SchemaError; $msg)
139+
crate::build_tools::py_error!(crate::build_tools::SchemaError; $msg)
86140
};
87141
($msg:expr, $( $msg_args:expr ),+ ) => {
88-
crate::build_tools::py_error!(crate::SchemaError; $msg, $( $msg_args ),+)
142+
crate::build_tools::py_error!(crate::build_tools::SchemaError; $msg, $( $msg_args ),+)
89143
};
90144

91145
($error_type:ty; $msg:expr) => {

src/errors/validation_exception.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use super::line_error::{Context, LocItem, Location, ValLineError};
1414

1515
use super::ValError;
1616

17-
#[pyclass(extends=PyValueError)]
17+
#[pyclass(extends=PyValueError, module="pydantic_core._pydantic_core")]
1818
#[derive(Debug)]
1919
pub struct ValidationError {
2020
line_errors: Vec<PyLineError>,
@@ -32,13 +32,13 @@ pub fn as_validation_err(py: Python, model_name: &str, error: ValError) -> PyErr
3232
}
3333

3434
impl fmt::Display for ValidationError {
35+
#[cfg_attr(has_no_coverage, no_coverage)]
3536
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3637
write!(f, "{}", self.display(None))
3738
}
3839
}
3940

4041
impl ValidationError {
41-
#[inline]
4242
pub fn new_err<A>(args: A) -> PyErr
4343
where
4444
A: PyErrArguments + Send + Sync + 'static,
@@ -64,6 +64,7 @@ impl ValidationError {
6464
}
6565

6666
impl Error for ValidationError {
67+
#[cfg_attr(has_no_coverage, no_coverage)]
6768
fn source(&self) -> Option<&(dyn Error + 'static)> {
6869
// we could in theory set self.source as `ValError::LineErrors(line_errors.clone())`, then return that here
6970
// source is not used, and I can't imagine why it would be

src/lib.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
#![cfg_attr(has_no_coverage, feature(no_coverage))]
22
#![allow(clippy::borrow_deref_ref)]
33

4-
use pyo3::create_exception;
5-
use pyo3::exceptions::PyException;
64
use pyo3::prelude::*;
75

86
#[cfg(feature = "mimalloc")]
@@ -15,22 +13,25 @@ mod input;
1513
mod validators;
1614

1715
// required for benchmarks
16+
pub use build_tools::SchemaError;
17+
pub use errors::ValidationError;
1818
pub use validators::SchemaValidator;
1919

20-
create_exception!(_pydantic_core, SchemaError, PyException);
21-
22-
#[pymodule]
23-
fn _pydantic_core(py: Python, m: &PyModule) -> PyResult<()> {
24-
let mut version = env!("CARGO_PKG_VERSION").to_string();
20+
pub fn get_version() -> String {
21+
let version = env!("CARGO_PKG_VERSION").to_string();
2522
// cargo uses "1.0-alpha1" etc. while python uses "1.0.0a1", this is not full compatibility,
2623
// but it's good enough for now
2724
// see https://docs.rs/semver/1.0.9/semver/struct.Version.html#method.parse for rust spec
2825
// see https://peps.python.org/pep-0440/ for python spec
2926
// it seems the dot after "alpha/beta" e.g. "-alpha.1" is not necessary, hence why this works
30-
version = version.replace("-alpha", "a").replace("-beta", "b");
31-
m.add("__version__", version)?;
32-
m.add("ValidationError", py.get_type::<errors::ValidationError>())?;
33-
m.add("SchemaError", py.get_type::<SchemaError>())?;
27+
version.replace("-alpha", "a").replace("-beta", "b")
28+
}
29+
30+
#[pymodule]
31+
fn _pydantic_core(_py: Python, m: &PyModule) -> PyResult<()> {
32+
m.add("__version__", get_version())?;
3433
m.add_class::<SchemaValidator>()?;
34+
m.add_class::<ValidationError>()?;
35+
m.add_class::<SchemaError>()?;
3536
Ok(())
3637
}

src/validators/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ use pyo3::prelude::*;
77
use pyo3::types::{PyAny, PyDict};
88
use serde_json::from_str as parse_json;
99

10-
use crate::build_tools::{py_error, SchemaDict};
10+
use crate::build_tools::{py_error, SchemaDict, SchemaError};
1111
use crate::errors::{as_validation_err, val_line_error, ErrorKind, ValError, ValResult};
1212
use crate::input::{Input, JsonInput};
13-
use crate::SchemaError;
1413

1514
mod any;
1615
mod bool;
@@ -140,7 +139,7 @@ macro_rules! validator_match {
140139
<$validator>::EXPECTED_TYPE => {
141140
$build_context.incr_check_depth()?;
142141
let val = <$validator>::build($dict, $config, $build_context).map_err(|err| {
143-
crate::SchemaError::new_err(format!("Error building \"{}\" validator:\n {}", $type, err))
142+
SchemaError::new_err(format!("Error building \"{}\" validator:\n {}", $type, err))
144143
})?;
145144
$build_context.decr_depth();
146145
Ok((val, $dict))

tests/test_misc.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import pytest
2+
3+
from pydantic_core._pydantic_core import SchemaError, SchemaValidator, ValidationError, __version__
4+
5+
6+
@pytest.mark.parametrize('obj', [ValidationError, SchemaValidator, SchemaError])
7+
def test_module(obj):
8+
assert obj.__module__ == 'pydantic_core._pydantic_core'
9+
10+
11+
def test_version():
12+
assert isinstance(__version__, str)
13+
assert '.' in __version__
14+
15+
16+
def test_schema_error():
17+
err = SchemaError('test')
18+
assert isinstance(err, Exception)
19+
assert str(err) == 'test'
20+
assert repr(err) == 'SchemaError("test")'
21+
22+
23+
def test_validation_error():
24+
v = SchemaValidator('int')
25+
with pytest.raises(ValidationError) as exc_info:
26+
v.validate_python(1.5)
27+
28+
assert exc_info.value.title == 'int'
29+
assert exc_info.value.error_count() == 1
30+
31+
32+
def test_validation_error_multiple():
33+
class MyModel:
34+
# this is not required, but it avoids `__fields_set__` being included in `__dict__`
35+
__slots__ = '__dict__', '__fields_set__'
36+
field_a: str
37+
field_b: int
38+
39+
v = SchemaValidator(
40+
{
41+
'type': 'model-class',
42+
'class_type': MyModel,
43+
'model': {
44+
'type': 'model',
45+
'fields': {'x': {'schema': {'type': 'float'}}, 'y': {'schema': {'type': 'int'}}},
46+
},
47+
}
48+
)
49+
with pytest.raises(ValidationError) as exc_info:
50+
v.validate_python({'x': 'x' * 60, 'y': 'y'})
51+
52+
assert exc_info.value.title == 'MyModel'
53+
assert exc_info.value.error_count() == 2
54+
assert exc_info.value.errors() == [
55+
{
56+
'kind': 'float_parsing',
57+
'loc': ['x'],
58+
'message': 'Value must be a valid number, unable to parse string as an number',
59+
'input_value': 'x' * 60,
60+
},
61+
{
62+
'kind': 'int_parsing',
63+
'loc': ['y'],
64+
'message': 'Value must be a valid integer, unable to parse string as an integer',
65+
'input_value': 'y',
66+
},
67+
]
68+
assert repr(exc_info.value) == (
69+
'2 validation errors for MyModel\n'
70+
'x\n'
71+
" Value must be a valid number, unable to parse string as an number [kind=float_parsing, input_value='xxxxxxx"
72+
"xxxxxxxxxxxxxxxxx...xxxxxxxxxxxxxxxxxxxxxxx', input_type=str]\n"
73+
'y\n'
74+
" Value must be a valid integer, unable to parse string as an integer [kind=int_parsing, input_value='y', "
75+
'input_type=str]'
76+
)

0 commit comments

Comments
 (0)