Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into jiter
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt committed Sep 21, 2023
2 parents c8db754 + 33a7cc0 commit 1316dee
Show file tree
Hide file tree
Showing 27 changed files with 394 additions and 86 deletions.
5 changes: 1 addition & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pydantic-core"
version = "2.8.0"
version = "2.9.0"
edition = "2021"
license = "MIT"
homepage = "https://github.com/pydantic/pydantic-core"
Expand Down
15 changes: 13 additions & 2 deletions python/pydantic_core/_pydantic_core.pyi
Expand Up @@ -748,25 +748,36 @@ class ValidationError(ValueError):
Returns:
The number of errors in the validation error.
"""
def errors(self, *, include_url: bool = True, include_context: bool = True) -> list[ErrorDetails]:
def errors(
self, *, include_url: bool = True, include_context: bool = True, include_input: bool = True
) -> list[ErrorDetails]:
"""
Details about each error in the validation error.
Args:
include_url: Whether to include a URL to documentation on the error each error.
include_context: Whether to include the context of each error.
include_input: Whether to include the input value of each error.
Returns:
A list of [`ErrorDetails`][pydantic_core.ErrorDetails] for each error in the validation error.
"""
def json(self, *, indent: int | None = None, include_url: bool = True, include_context: bool = True) -> str:
def json(
self,
*,
indent: int | None = None,
include_url: bool = True,
include_context: bool = True,
include_input: bool = True,
) -> str:
"""
Same as [`errors()`][pydantic_core.ValidationError.errors] but returns a JSON string.
Args:
indent: The number of spaces to indent the JSON by, or `None` for no indentation - compact JSON.
include_url: Whether to include a URL to documentation on the error each error.
include_context: Whether to include the context of each error.
include_input: Whether to include the input value of each error.
Returns:
a JSON string.
Expand Down
21 changes: 19 additions & 2 deletions python/pydantic_core/core_schema.py
Expand Up @@ -11,15 +11,20 @@
from decimal import Decimal
from typing import TYPE_CHECKING, Any, Callable, Dict, Hashable, List, Set, Tuple, Type, Union

if sys.version_info < (3, 12):
from typing_extensions import TypedDict
else:
from typing import TypedDict

if sys.version_info < (3, 11):
from typing_extensions import Protocol, Required, TypeAlias
else:
from typing import Protocol, Required, TypeAlias

if sys.version_info < (3, 9):
from typing_extensions import Literal, TypedDict
from typing_extensions import Literal
else:
from typing import Literal, TypedDict
from typing import Literal

if TYPE_CHECKING:
from pydantic_core import PydanticUndefined
Expand Down Expand Up @@ -63,6 +68,8 @@ class CoreConfig(TypedDict, total=False):
hide_input_in_errors: Whether to hide input data from `ValidationError` representation.
validation_error_cause: Whether to add user-python excs to the __cause__ of a ValidationError.
Requires exceptiongroup backport pre Python 3.11.
coerce_numbers_to_str: Whether to enable coercion of any `Number` type to `str` (not applicable in `strict` mode).
regex_engine: The regex engine to use for regex pattern validation. Default is 'rust-regex'. See `StringSchema`.
"""

title: str
Expand Down Expand Up @@ -95,6 +102,7 @@ class CoreConfig(TypedDict, total=False):
# used to hide input data from ValidationError repr
hide_input_in_errors: bool
validation_error_cause: bool # default: False
coerce_numbers_to_str: bool # default: False


IncExCall: TypeAlias = 'set[int | str] | dict[int | str, IncExCall] | None'
Expand Down Expand Up @@ -745,6 +753,7 @@ class StringSchema(TypedDict, total=False):
strip_whitespace: bool
to_lower: bool
to_upper: bool
regex_engine: Literal['rust-regex', 'python-re'] # default: 'rust-regex'
strict: bool
ref: str
metadata: Any
Expand All @@ -759,6 +768,7 @@ def str_schema(
strip_whitespace: bool | None = None,
to_lower: bool | None = None,
to_upper: bool | None = None,
regex_engine: Literal['rust-regex', 'python-re'] | None = None,
strict: bool | None = None,
ref: str | None = None,
metadata: Any = None,
Expand All @@ -782,6 +792,12 @@ def str_schema(
strip_whitespace: Whether to strip whitespace from the value
to_lower: Whether to convert the value to lowercase
to_upper: Whether to convert the value to uppercase
regex_engine: The regex engine to use for pattern validation. Default is 'rust-regex'.
- `rust-regex` uses the [`regex`](https://docs.rs/regex) Rust
crate, which is non-backtracking and therefore more DDoS
resistant, but does not support all regex features.
- `python-re` use the [`re`](https://docs.python.org/3/library/re.html) module,
which supports all regex features, but may be slower.
strict: Whether the value should be a string or a value that can be converted to a string
ref: optional unique identifier of the schema, used to reference the schema in other places
metadata: Any other information you want to include with the schema, not used by pydantic-core
Expand All @@ -795,6 +811,7 @@ def str_schema(
strip_whitespace=strip_whitespace,
to_lower=to_lower,
to_upper=to_upper,
regex_engine=regex_engine,
strict=strict,
ref=ref,
metadata=metadata,
Expand Down
2 changes: 1 addition & 1 deletion src/build_tools.rs
Expand Up @@ -125,7 +125,7 @@ impl SchemaError {
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, false, false),
SchemaErrorEnum::ValidationError(error) => error.errors(py, false, false, true),
}
}

Expand Down
45 changes: 29 additions & 16 deletions src/errors/validation_exception.rs
Expand Up @@ -265,8 +265,14 @@ impl ValidationError {
self.line_errors.len()
}

#[pyo3(signature = (*, include_url = true, include_context = true))]
pub fn errors(&self, py: Python, include_url: bool, include_context: bool) -> PyResult<Py<PyList>> {
#[pyo3(signature = (*, include_url = true, include_context = true, include_input = true))]
pub fn errors(
&self,
py: Python,
include_url: bool,
include_context: bool,
include_input: bool,
) -> PyResult<Py<PyList>> {
let url_prefix = get_url_prefix(py, include_url);
let mut iteration_error = None;
let list = PyList::new(
Expand All @@ -278,7 +284,7 @@ impl ValidationError {
if iteration_error.is_some() {
return py.None();
}
e.as_dict(py, url_prefix, include_context, self.input_type)
e.as_dict(py, url_prefix, include_context, self.input_type, include_input)
.unwrap_or_else(|err| {
iteration_error = Some(err);
py.None()
Expand All @@ -292,13 +298,14 @@ impl ValidationError {
}
}

#[pyo3(signature = (*, indent = None, include_url = true, include_context = true))]
#[pyo3(signature = (*, indent = None, include_url = true, include_context = true, include_input = true))]
pub fn json<'py>(
&self,
py: Python<'py>,
indent: Option<usize>,
include_url: bool,
include_context: bool,
include_input: bool,
) -> PyResult<&'py PyString> {
let state = SerializationState::new("iso8601", "utf8")?;
let extra = state.extra(py, &SerMode::Json, true, false, false, true, None);
Expand All @@ -307,6 +314,7 @@ impl ValidationError {
line_errors: &self.line_errors,
url_prefix: get_url_prefix(py, include_url),
include_context,
include_input,
extra: &extra,
input_type: &self.input_type,
};
Expand Down Expand Up @@ -477,12 +485,15 @@ impl PyLineError {
url_prefix: Option<&str>,
include_context: bool,
input_type: InputType,
include_input: bool,
) -> PyResult<PyObject> {
let dict = PyDict::new(py);
dict.set_item("type", self.error_type.type_string())?;
dict.set_item("loc", self.location.to_object(py))?;
dict.set_item("msg", self.error_type.render_message(py, input_type)?)?;
dict.set_item("input", &self.input_value)?;
if include_input {
dict.set_item("input", &self.input_value)?;
}
if include_context {
if let Some(context) = self.error_type.py_dict(py)? {
dict.set_item("ctx", context)?;
Expand Down Expand Up @@ -563,6 +574,7 @@ struct ValidationErrorSerializer<'py> {
line_errors: &'py [PyLineError],
url_prefix: Option<&'py str>,
include_context: bool,
include_input: bool,
extra: &'py crate::serializers::Extra<'py>,
input_type: &'py InputType,
}
Expand All @@ -579,6 +591,7 @@ impl<'py> Serialize for ValidationErrorSerializer<'py> {
line_error,
url_prefix: self.url_prefix,
include_context: self.include_context,
include_input: self.include_input,
extra: self.extra,
input_type: self.input_type,
};
Expand All @@ -593,6 +606,7 @@ struct PyLineErrorSerializer<'py> {
line_error: &'py PyLineError,
url_prefix: Option<&'py str>,
include_context: bool,
include_input: bool,
extra: &'py crate::serializers::Extra<'py>,
input_type: &'py InputType,
}
Expand All @@ -603,13 +617,10 @@ impl<'py> Serialize for PyLineErrorSerializer<'py> {
S: Serializer,
{
let py = self.py;
let mut size = 4;
if self.url_prefix.is_some() {
size += 1;
}
if self.include_context {
size += 1;
}
let size = 3 + [self.url_prefix.is_some(), self.include_context, self.include_input]
.into_iter()
.filter(|b| *b)
.count();
let mut map = serializer.serialize_map(Some(size))?;

map.serialize_entry("type", &self.line_error.error_type.type_string())?;
Expand All @@ -623,10 +634,12 @@ impl<'py> Serialize for PyLineErrorSerializer<'py> {
.map_err(py_err_json::<S>)?;
map.serialize_entry("msg", &msg)?;

map.serialize_entry(
"input",
&self.extra.serialize_infer(self.line_error.input_value.as_ref(py)),
)?;
if self.include_input {
map.serialize_entry(
"input",
&self.extra.serialize_infer(self.line_error.input_value.as_ref(py)),
)?;
}

if self.include_context {
if let Some(context) = self.line_error.error_type.py_dict(py).map_err(py_err_json::<S>)? {
Expand Down
6 changes: 3 additions & 3 deletions src/input/input_abstract.rs
Expand Up @@ -91,16 +91,16 @@ pub trait Input<'a>: fmt::Debug + ToPyObject + AsLocItem {

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

fn validate_str(&'a self, strict: bool) -> ValResult<EitherString<'a>> {
fn validate_str(&'a self, strict: bool, coerce_numbers_to_str: bool) -> ValResult<EitherString<'a>> {
if strict {
self.strict_str()
} else {
self.lax_str()
self.lax_str(coerce_numbers_to_str)
}
}
fn strict_str(&'a self) -> ValResult<EitherString<'a>>;
#[cfg_attr(has_coverage_attribute, coverage(off))]
fn lax_str(&'a self) -> ValResult<EitherString<'a>> {
fn lax_str(&'a self, _coerce_numbers_to_str: bool) -> ValResult<EitherString<'a>> {
self.strict_str()
}

Expand Down
5 changes: 4 additions & 1 deletion src/input/input_json.rs
Expand Up @@ -90,9 +90,12 @@ impl<'a> Input<'a> for JsonValue {
_ => Err(ValError::new(ErrorTypeDefaults::StringType, self)),
}
}
fn lax_str(&'a self) -> ValResult<EitherString<'a>> {
fn lax_str(&'a self, coerce_numbers_to_str: bool) -> ValResult<EitherString<'a>> {
match self {
JsonValue::String(s) => Ok(s.as_str().into()),
JsonValue::BigInt(v) if coerce_numbers_to_str => Ok(v.to_string().into()),
JsonValue::Float(v) if coerce_numbers_to_str => Ok(v.to_string().into()),
JsonValue::Int(v) if coerce_numbers_to_str => Ok(v.to_string().into()),
_ => Err(ValError::new(ErrorTypeDefaults::StringType, self)),
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/input/input_python.rs
Expand Up @@ -212,7 +212,7 @@ impl<'a> Input<'a> for PyAny {
}
}

fn lax_str(&'a self) -> ValResult<EitherString<'a>> {
fn lax_str(&'a self, coerce_numbers_to_str: bool) -> ValResult<EitherString<'a>> {
if let Ok(py_str) = <PyString as PyTryFrom>::try_from_exact(self) {
Ok(py_str.into())
} else if let Ok(py_str) = self.downcast::<PyString>() {
Expand All @@ -235,6 +235,15 @@ impl<'a> Input<'a> for PyAny {
Err(_) => return Err(ValError::new(ErrorTypeDefaults::StringUnicode, self)),
};
Ok(s.into())
} else if coerce_numbers_to_str && {
let py = self.py();
let decimal_type: Py<PyType> = get_decimal_type(py);

self.is_instance_of::<PyInt>()
|| self.is_instance_of::<PyFloat>()
|| self.is_instance(decimal_type.as_ref(py)).unwrap_or_default()
} {
Ok(self.str()?.into())
} else {
Err(ValError::new(ErrorTypeDefaults::StringType, self))
}
Expand Down
6 changes: 6 additions & 0 deletions src/input/return_enums.rs
Expand Up @@ -768,6 +768,12 @@ impl<'a> From<&'a str> for EitherString<'a> {
}
}

impl<'a> From<String> for EitherString<'a> {
fn from(data: String) -> Self {
Self::Cow(Cow::Owned(data))
}
}

impl<'a> From<&'a PyString> for EitherString<'a> {
fn from(date: &'a PyString) -> Self {
Self::Py(date)
Expand Down
2 changes: 1 addition & 1 deletion src/input/shared.rs
Expand Up @@ -10,7 +10,7 @@ use super::{EitherFloat, EitherInt, Input};
pub fn map_json_err<'a>(input: &'a impl Input<'a>, error: JsonValueError) -> ValError<'a> {
ValError::new(
ErrorType::JsonInvalid {
error: error.error_type.to_string(),
error: error.to_string(),
context: None,
},
input,
Expand Down
4 changes: 2 additions & 2 deletions src/serializers/shared.rs
Expand Up @@ -13,7 +13,7 @@ use serde_json::ser::PrettyFormatter;

use crate::build_tools::py_schema_err;
use crate::build_tools::py_schema_error_type;
use crate::definitions::DefinitionsBuilder;
use crate::definitions::{Definitions, DefinitionsBuilder};
use crate::py_gc::PyGcTraverse;
use crate::tools::{py_err, SchemaDict};

Expand Down Expand Up @@ -293,7 +293,7 @@ pub(crate) trait TypeSerializer: Send + Sync + Clone + Debug {
fn get_name(&self) -> &str;

/// Used by union serializers to decide if it's worth trying again while allowing subclasses
fn retry_with_lax_check(&self) -> bool {
fn retry_with_lax_check(&self, _definitions: &Definitions<CombinedSerializer>) -> bool {
false
}

Expand Down
4 changes: 2 additions & 2 deletions src/serializers/type_serializers/dataclass.rs
Expand Up @@ -6,7 +6,7 @@ use std::borrow::Cow;
use ahash::AHashMap;

use crate::build_tools::{py_schema_error_type, ExtraBehavior};
use crate::definitions::DefinitionsBuilder;
use crate::definitions::{Definitions, DefinitionsBuilder};
use crate::tools::SchemaDict;

use super::{
Expand Down Expand Up @@ -179,7 +179,7 @@ impl TypeSerializer for DataclassSerializer {
&self.name
}

fn retry_with_lax_check(&self) -> bool {
fn retry_with_lax_check(&self, _definitions: &Definitions<CombinedSerializer>) -> bool {
true
}
}

0 comments on commit 1316dee

Please sign in to comment.