Skip to content

PYD-137: Implement PydanticUseDefault error #714

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 29, 2023
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
2 changes: 0 additions & 2 deletions .mypy-stubtest-allowlist
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
# TODO: __init__ signature inherited from BaseException, probably needs investigating
pydantic_core._pydantic_core.PydanticOmit.__init__
# TODO: don't want to expose this staticmethod, requires https://github.com/PyO3/pyo3/issues/2384
pydantic_core._pydantic_core.PydanticUndefinedType.new
2 changes: 2 additions & 0 deletions python/pydantic_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
PydanticSerializationUnexpectedValue,
PydanticUndefined,
PydanticUndefinedType,
PydanticUseDefault,
SchemaError,
SchemaSerializer,
SchemaValidator,
Expand Down Expand Up @@ -55,6 +56,7 @@
'PydanticCustomError',
'PydanticKnownError',
'PydanticOmit',
'PydanticUseDefault',
'PydanticSerializationError',
'PydanticSerializationUnexpectedValue',
'to_json',
Expand Down
7 changes: 6 additions & 1 deletion python/pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ __all__ = [
'PydanticCustomError',
'PydanticKnownError',
'PydanticOmit',
'PydanticUseDefault',
'PydanticSerializationError',
'PydanticSerializationUnexpectedValue',
'PydanticUndefined',
Expand Down Expand Up @@ -258,7 +259,11 @@ class PydanticKnownError(ValueError):

@final
class PydanticOmit(Exception):
def __init__(self) -> None: ...
def __new__(self) -> PydanticOmit: ...

@final
class PydanticUseDefault(Exception):
def __new__(self) -> PydanticUseDefault: ...

@final
class PydanticSerializationError(ValueError):
Expand Down
1 change: 1 addition & 0 deletions src/build_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ impl SchemaError {
}
ValError::InternalErr(err) => err,
ValError::Omit => Self::new_err("Unexpected Omit error."),
ValError::UseDefault => Self::new_err("Unexpected UseDefault error."),
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/errors/line_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub enum ValError<'a> {
LineErrors(Vec<ValLineError<'a>>),
InternalErr(PyErr),
Omit,
UseDefault,
}

impl<'a> From<PyErr> for ValError<'a> {
Expand Down Expand Up @@ -66,6 +67,7 @@ impl<'a> ValError<'a> {
ValError::LineErrors(errors) => errors.iter().map(|e| e.duplicate(py)).collect::<Vec<_>>().into(),
ValError::InternalErr(err) => ValError::InternalErr(err.clone_ref(py)),
ValError::Omit => ValError::Omit,
ValError::UseDefault => ValError::UseDefault,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub use self::line_error::{InputValue, ValError, ValLineError, ValResult};
pub use self::location::LocItem;
pub use self::types::{list_all_errors, ErrorMode, ErrorType};
pub use self::validation_exception::ValidationError;
pub use self::value_exception::{PydanticCustomError, PydanticKnownError, PydanticOmit};
pub use self::value_exception::{PydanticCustomError, PydanticKnownError, PydanticOmit, PydanticUseDefault};

pub fn py_err_string(py: Python, err: PyErr) -> String {
let value = err.value(py);
Expand Down
5 changes: 5 additions & 0 deletions src/errors/validation_exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl ValidationError {
}
ValError::InternalErr(err) => err,
ValError::Omit => Self::omit_error(),
ValError::UseDefault => Self::use_default_error(),
}
}

Expand All @@ -89,6 +90,10 @@ impl ValidationError {
pub fn omit_error() -> PyErr {
py_schema_error_type!("Uncaught Omit error, please check your usage of `default` validators.")
}

pub fn use_default_error() -> PyErr {
py_schema_error_type!("Uncaught UseDefault error, please check your usage of `default` validators.")
}
}

static URL_ENV_VAR: GILOnceCell<bool> = GILOnceCell::new();
Expand Down
20 changes: 20 additions & 0 deletions src/errors/value_exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ impl PydanticOmit {
}
}

#[pyclass(extends=PyException, module="pydantic_core._pydantic_core")]
#[derive(Debug, Clone)]
pub struct PydanticUseDefault {}

#[pymethods]
impl PydanticUseDefault {
#[new]
pub fn py_new() -> Self {
Self {}
}

fn __str__(&self) -> &'static str {
self.__repr__()
}

fn __repr__(&self) -> &'static str {
"PydanticUseDefault()"
}
}

#[pyclass(extends=PyValueError, module="pydantic_core._pydantic_core")]
#[derive(Debug, Clone, Default)]
pub struct PydanticCustomError {
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ mod validators;
pub use self::url::{PyMultiHostUrl, PyUrl};
pub use argument_markers::{ArgsKwargs, PydanticUndefinedType};
pub use build_tools::SchemaError;
pub use errors::{list_all_errors, PydanticCustomError, PydanticKnownError, PydanticOmit, ValidationError};
pub use errors::{
list_all_errors, PydanticCustomError, PydanticKnownError, PydanticOmit, PydanticUseDefault, ValidationError,
};
pub use serializers::{
to_json, to_jsonable_python, PydanticSerializationError, PydanticSerializationUnexpectedValue, SchemaSerializer,
};
Expand Down Expand Up @@ -54,6 +56,7 @@ fn _pydantic_core(py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<PydanticCustomError>()?;
m.add_class::<PydanticKnownError>()?;
m.add_class::<PydanticOmit>()?;
m.add_class::<PydanticUseDefault>()?;
m.add_class::<PydanticSerializationError>()?;
m.add_class::<PydanticSerializationUnexpectedValue>()?;
m.add_class::<PyUrl>()?;
Expand Down
3 changes: 3 additions & 0 deletions src/validators/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::errors::{
use crate::input::Input;
use crate::recursion_guard::RecursionGuard;
use crate::tools::{function_name, py_err, SchemaDict};
use crate::PydanticUseDefault;

use super::generator::InternalValidator;
use super::{
Expand Down Expand Up @@ -485,6 +486,8 @@ pub fn convert_err<'a>(py: Python<'a>, err: PyErr, input: &'a impl Input<'a>) ->
py_err_string!(err.value(py), AssertionError, input)
} else if err.is_instance_of::<PydanticOmit>(py) {
ValError::Omit
} else if err.is_instance_of::<PydanticUseDefault>(py) {
ValError::UseDefault
} else {
ValError::InternalErr(err)
}
Expand Down
1 change: 1 addition & 0 deletions src/validators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ impl SchemaValidator {
Ok(_) => Ok(true),
Err(ValError::InternalErr(err)) => Err(err),
Err(ValError::Omit) => Err(ValidationError::omit_error()),
Err(ValError::UseDefault) => Err(ValidationError::use_default_error()),
Err(ValError::LineErrors(_)) => Ok(false),
}
}
Expand Down
15 changes: 10 additions & 5 deletions src/validators/with_default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,17 @@ impl Validator for WithDefaultValidator {
} else {
match self.validator.validate(py, input, extra, definitions, recursion_guard) {
Ok(v) => Ok(v),
Err(e) => match self.on_error {
OnError::Raise => Err(e),
OnError::Default => Ok(self
Err(e) => match e {
ValError::UseDefault => Ok(self
.default_value(py, None::<usize>, extra, definitions, recursion_guard)?
.unwrap()),
OnError::Omit => Err(ValError::Omit),
.ok_or(e)?),
e => match self.on_error {
OnError::Raise => Err(e),
OnError::Default => Ok(self
.default_value(py, None::<usize>, extra, definitions, recursion_guard)?
.ok_or(e)?),
OnError::Omit => Err(ValError::Omit),
},
},
}
}
Expand Down
39 changes: 38 additions & 1 deletion tests/validators/test_with_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@

import pytest

from pydantic_core import ArgsKwargs, SchemaError, SchemaValidator, Some, ValidationError, core_schema
from pydantic_core import (
ArgsKwargs,
PydanticUseDefault,
SchemaError,
SchemaValidator,
Some,
ValidationError,
core_schema,
)

from ..conftest import PyAndJson

Expand Down Expand Up @@ -581,3 +589,32 @@ def f(v: Union[Some[Any], None]) -> str:

res = f(SchemaValidator(core_schema.int_schema()).get_default_value())
assert res == 'case5'


def test_use_default_error() -> None:
def val_func(v: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any:
if isinstance(v, str) and v == '':
raise PydanticUseDefault
return handler(v)

validator = SchemaValidator(
core_schema.with_default_schema(
core_schema.no_info_wrap_validator_function(val_func, core_schema.int_schema()), default=10
)
)

assert validator.validate_python('1') == 1
assert validator.validate_python('') == 10

# without a default value the error bubbles up
# the error message is the same as the error message produced by PydanticOmit
validator = SchemaValidator(
core_schema.with_default_schema(core_schema.no_info_wrap_validator_function(val_func, core_schema.int_schema()))
)
with pytest.raises(SchemaError, match='Uncaught UseDefault error, please check your usage of `default` validators'):
validator.validate_python('')

# same if there is no WithDefault validator
validator = SchemaValidator(core_schema.no_info_wrap_validator_function(val_func, core_schema.int_schema()))
with pytest.raises(SchemaError, match='Uncaught UseDefault error, please check your usage of `default` validators'):
validator.validate_python('')