Skip to content

Commit ae17586

Browse files
committed
Merge branch 'main' of github.com:pydantic/pydantic-core into fix-12273-dict
2 parents 6e35fe9 + ed0d1ca commit ae17586

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+998
-645
lines changed

python/pydantic_core/core_schema.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,6 +1939,7 @@ class DictSchema(TypedDict, total=False):
19391939
values_schema: CoreSchema # default: AnySchema
19401940
min_length: int
19411941
max_length: int
1942+
fail_fast: bool
19421943
strict: bool
19431944
ref: str
19441945
metadata: dict[str, Any]
@@ -1951,6 +1952,7 @@ def dict_schema(
19511952
*,
19521953
min_length: int | None = None,
19531954
max_length: int | None = None,
1955+
fail_fast: bool | None = None,
19541956
strict: bool | None = None,
19551957
ref: str | None = None,
19561958
metadata: dict[str, Any] | None = None,
@@ -1974,6 +1976,7 @@ def dict_schema(
19741976
values_schema: The value must be a dict with values that match this schema
19751977
min_length: The value must be a dict with at least this many items
19761978
max_length: The value must be a dict with at most this many items
1979+
fail_fast: Stop validation on the first error
19771980
strict: Whether the keys and values should be validated with strict mode
19781981
ref: optional unique identifier of the schema, used to reference the schema in other places
19791982
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -1985,6 +1988,7 @@ def dict_schema(
19851988
values_schema=values_schema,
19861989
min_length=min_length,
19871990
max_length=max_length,
1991+
fail_fast=fail_fast,
19881992
strict=strict,
19891993
ref=ref,
19901994
metadata=metadata,

src/build_tools.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::error::Error;
22
use std::fmt;
3+
use std::ops::Deref;
34
use std::str::FromStr;
5+
use std::sync::OnceLock;
46

57
use pyo3::exceptions::PyException;
68
use pyo3::prelude::*;
@@ -217,3 +219,28 @@ impl FromStr for ExtraBehavior {
217219
}
218220
}
219221
}
222+
223+
/// A lazily-initialized value.
224+
///
225+
/// This is a basic replacement for `LazyLock` which is available only in Rust 1.80+.
226+
pub struct LazyLock<T> {
227+
init: fn() -> T,
228+
value: OnceLock<T>,
229+
}
230+
231+
impl<T> Deref for LazyLock<T> {
232+
type Target = T;
233+
234+
fn deref(&self) -> &Self::Target {
235+
self.value.get_or_init(self.init)
236+
}
237+
}
238+
239+
impl<T> LazyLock<T> {
240+
pub const fn new(init: fn() -> T) -> Self {
241+
Self {
242+
init,
243+
value: OnceLock::new(),
244+
}
245+
}
246+
}

src/input/input_abstract.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ pub trait Input<'py>: fmt::Debug {
8585

8686
fn validate_dataclass_args<'a>(&'a self, dataclass_name: &str) -> ValResult<Self::Arguments<'a>>;
8787

88-
fn validate_str(&self, strict: bool, coerce_numbers_to_str: bool) -> ValMatch<EitherString<'_>>;
88+
fn validate_str(&self, strict: bool, coerce_numbers_to_str: bool) -> ValMatch<EitherString<'_, 'py>>;
8989

9090
fn validate_bytes<'a>(&'a self, strict: bool, mode: ValBytesMode) -> ValMatch<EitherBytes<'a, 'py>>;
9191

@@ -103,7 +103,7 @@ pub trait Input<'py>: fmt::Debug {
103103

104104
/// Extract a String from the input, only allowing exact
105105
/// matches for a String (no subclasses)
106-
fn exact_str(&self) -> ValResult<EitherString<'_>> {
106+
fn exact_str(&self) -> ValResult<EitherString<'_, 'py>> {
107107
self.validate_str(true, false).and_then(|val_match| {
108108
val_match
109109
.require_exact()

src/input/input_json.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,11 @@ impl<'py, 'data> Input<'py> for JsonValue<'data> {
107107
}
108108
}
109109

110-
fn validate_str(&self, strict: bool, coerce_numbers_to_str: bool) -> ValResult<ValidationMatch<EitherString<'_>>> {
110+
fn validate_str(
111+
&self,
112+
strict: bool,
113+
coerce_numbers_to_str: bool,
114+
) -> ValResult<ValidationMatch<EitherString<'_, 'py>>> {
111115
// Justification for `strict` instead of `exact` is that in JSON strings can also
112116
// represent other datatypes such as UUID and date more exactly, so string is a
113117
// converting input
@@ -163,7 +167,7 @@ impl<'py, 'data> Input<'py> for JsonValue<'data> {
163167
}
164168
}
165169

166-
fn exact_str(&self) -> ValResult<EitherString<'_>> {
170+
fn exact_str(&self) -> ValResult<EitherString<'_, 'py>> {
167171
match self {
168172
JsonValue::Str(s) => Ok(s.as_ref().into()),
169173
_ => Err(ValError::new(ErrorTypeDefaults::StringType, self)),
@@ -414,7 +418,7 @@ impl<'py> Input<'py> for str {
414418
&self,
415419
_strict: bool,
416420
_coerce_numbers_to_str: bool,
417-
) -> ValResult<ValidationMatch<EitherString<'_>>> {
421+
) -> ValResult<ValidationMatch<EitherString<'_, 'py>>> {
418422
// Justification for `strict` instead of `exact` is that in JSON strings can also
419423
// represent other datatypes such as UUID and date more exactly, so string is a
420424
// converting input

src/input/input_python.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,11 @@ impl<'py> Input<'py> for Bound<'py, PyAny> {
192192
}
193193
}
194194

195-
fn validate_str(&self, strict: bool, coerce_numbers_to_str: bool) -> ValResult<ValidationMatch<EitherString<'_>>> {
195+
fn validate_str(
196+
&self,
197+
strict: bool,
198+
coerce_numbers_to_str: bool,
199+
) -> ValResult<ValidationMatch<EitherString<'_, 'py>>> {
196200
if let Ok(py_str) = self.downcast_exact::<PyString>() {
197201
return Ok(ValidationMatch::exact(py_str.clone().into()));
198202
} else if let Ok(py_str) = self.downcast::<PyString>() {
@@ -339,7 +343,7 @@ impl<'py> Input<'py> for Bound<'py, PyAny> {
339343
}
340344
}
341345

342-
fn exact_str(&self) -> ValResult<EitherString<'_>> {
346+
fn exact_str(&self) -> ValResult<EitherString<'_, 'py>> {
343347
if let Ok(py_str) = self.downcast_exact() {
344348
Ok(EitherString::Py(py_str.clone()))
345349
} else {

src/input/input_string.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ impl<'py> Input<'py> for StringMapping<'py> {
105105
&self,
106106
_strict: bool,
107107
_coerce_numbers_to_str: bool,
108-
) -> ValResult<ValidationMatch<EitherString<'_>>> {
108+
) -> ValResult<ValidationMatch<EitherString<'_, 'py>>> {
109109
match self {
110110
Self::String(s) => Ok(ValidationMatch::strict(s.clone().into())),
111111
Self::Mapping(_) => Err(ValError::new(ErrorTypeDefaults::StringType, self)),

src/input/return_enums.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -468,41 +468,41 @@ impl<'data> GenericJsonIterator<'data> {
468468
}
469469

470470
#[cfg_attr(debug_assertions, derive(Debug))]
471-
pub enum EitherString<'a> {
471+
pub enum EitherString<'a, 'py> {
472472
Cow(Cow<'a, str>),
473-
Py(Bound<'a, PyString>),
473+
Py(Bound<'py, PyString>),
474474
}
475475

476-
impl<'a> EitherString<'a> {
476+
impl<'py> EitherString<'_, 'py> {
477477
pub fn as_cow(&self) -> ValResult<Cow<'_, str>> {
478478
match self {
479-
Self::Cow(data) => Ok(data.clone()),
479+
Self::Cow(data) => Ok(Cow::Borrowed(data)),
480480
Self::Py(py_str) => Ok(Cow::Borrowed(py_string_str(py_str)?)),
481481
}
482482
}
483483

484-
pub fn as_py_string(&'a self, py: Python<'a>, cache_str: StringCacheMode) -> Bound<'a, PyString> {
484+
pub fn as_py_string(&self, py: Python<'py>, cache_str: StringCacheMode) -> Bound<'py, PyString> {
485485
match self {
486486
Self::Cow(cow) => new_py_string(py, cow.as_ref(), cache_str),
487487
Self::Py(py_string) => py_string.clone(),
488488
}
489489
}
490490
}
491491

492-
impl<'a> From<&'a str> for EitherString<'a> {
492+
impl<'a> From<&'a str> for EitherString<'a, '_> {
493493
fn from(data: &'a str) -> Self {
494494
Self::Cow(Cow::Borrowed(data))
495495
}
496496
}
497497

498-
impl From<String> for EitherString<'_> {
498+
impl From<String> for EitherString<'_, '_> {
499499
fn from(data: String) -> Self {
500500
Self::Cow(Cow::Owned(data))
501501
}
502502
}
503503

504-
impl<'a> From<Bound<'a, PyString>> for EitherString<'a> {
505-
fn from(date: Bound<'a, PyString>) -> Self {
504+
impl<'py> From<Bound<'py, PyString>> for EitherString<'_, 'py> {
505+
fn from(date: Bound<'py, PyString>) -> Self {
506506
Self::Py(date)
507507
}
508508
}

src/serializers/computed_fields.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::Arc;
2+
13
use pyo3::prelude::*;
24
use pyo3::types::{PyDict, PyList, PyString};
35
use pyo3::{intern, PyTraverseError, PyVisit};
@@ -21,7 +23,7 @@ impl ComputedFields {
2123
pub fn new(
2224
schema: &Bound<'_, PyDict>,
2325
config: Option<&Bound<'_, PyDict>>,
24-
definitions: &mut DefinitionsBuilder<CombinedSerializer>,
26+
definitions: &mut DefinitionsBuilder<Arc<CombinedSerializer>>,
2527
) -> PyResult<Option<Self>> {
2628
let py = schema.py();
2729
if let Some(computed_fields) = schema.get_as::<Bound<'_, PyList>>(intern!(py, "computed_fields"))? {
@@ -182,7 +184,7 @@ struct ComputedFieldToSerialize<'a, 'py> {
182184
struct ComputedField {
183185
property_name: String,
184186
property_name_py: Py<PyString>,
185-
serializer: CombinedSerializer,
187+
serializer: Arc<CombinedSerializer>,
186188
alias: String,
187189
alias_py: Py<PyString>,
188190
serialize_by_alias: Option<bool>,
@@ -192,7 +194,7 @@ impl ComputedField {
192194
pub fn new(
193195
schema: &Bound<'_, PyAny>,
194196
config: Option<&Bound<'_, PyDict>>,
195-
definitions: &mut DefinitionsBuilder<CombinedSerializer>,
197+
definitions: &mut DefinitionsBuilder<Arc<CombinedSerializer>>,
196198
) -> PyResult<Self> {
197199
let py = schema.py();
198200
let schema: &Bound<'_, PyDict> = schema.downcast()?;

src/serializers/fields.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::borrow::Cow;
2+
use std::sync::Arc;
23

34
use pyo3::prelude::*;
45
use pyo3::types::{PyDict, PyString};
@@ -25,7 +26,7 @@ pub(super) struct SerField {
2526
pub alias: Option<String>,
2627
pub alias_py: Option<Py<PyString>>,
2728
// None serializer means exclude
28-
pub serializer: Option<CombinedSerializer>,
29+
pub serializer: Option<Arc<CombinedSerializer>>,
2930
pub required: bool,
3031
pub serialize_by_alias: Option<bool>,
3132
pub serialization_exclude_if: Option<Py<PyAny>>,
@@ -38,7 +39,7 @@ impl SerField {
3839
py: Python,
3940
key_py: Py<PyString>,
4041
alias: Option<String>,
41-
serializer: Option<CombinedSerializer>,
42+
serializer: Option<Arc<CombinedSerializer>>,
4243
required: bool,
4344
serialize_by_alias: Option<bool>,
4445
serialization_exclude_if: Option<Py<PyAny>>,
@@ -113,7 +114,7 @@ pub struct GeneralFieldsSerializer {
113114
fields: AHashMap<String, SerField>,
114115
computed_fields: Option<ComputedFields>,
115116
mode: FieldsMode,
116-
extra_serializer: Option<Box<CombinedSerializer>>,
117+
extra_serializer: Option<Arc<CombinedSerializer>>,
117118
// isize because we look up filter via `.hash()` which returns an isize
118119
filter: SchemaFilter<isize>,
119120
required_fields: usize,
@@ -132,14 +133,14 @@ impl GeneralFieldsSerializer {
132133
pub(super) fn new(
133134
fields: AHashMap<String, SerField>,
134135
mode: FieldsMode,
135-
extra_serializer: Option<CombinedSerializer>,
136+
extra_serializer: Option<Arc<CombinedSerializer>>,
136137
computed_fields: Option<ComputedFields>,
137138
) -> Self {
138139
let required_fields = fields.values().filter(|f| f.required).count();
139140
Self {
140141
fields,
141142
mode,
142-
extra_serializer: extra_serializer.map(Box::new),
143+
extra_serializer,
143144
filter: SchemaFilter::default(),
144145
computed_fields,
145146
required_fields,

src/serializers/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::fmt::Debug;
22
use std::sync::atomic::{AtomicUsize, Ordering};
3+
use std::sync::Arc;
34

45
use pyo3::prelude::*;
56
use pyo3::types::{PyBytes, PyDict, PyTuple, PyType};
@@ -39,8 +40,8 @@ pub enum WarningsArg {
3940
#[pyclass(module = "pydantic_core._pydantic_core", frozen)]
4041
#[derive(Debug)]
4142
pub struct SchemaSerializer {
42-
serializer: CombinedSerializer,
43-
definitions: Definitions<CombinedSerializer>,
43+
serializer: Arc<CombinedSerializer>,
44+
definitions: Definitions<Arc<CombinedSerializer>>,
4445
expected_json_size: AtomicUsize,
4546
config: SerializationConfig,
4647
// References to the Python schema and config objects are saved to enable

0 commit comments

Comments
 (0)