Skip to content

Commit d260499

Browse files
committed
Add unset-sentinel validator/serializer
1 parent a0c3885 commit d260499

File tree

11 files changed

+157
-15
lines changed

11 files changed

+157
-15
lines changed

python/pydantic_core/core_schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4075,6 +4075,7 @@ def definition_reference_schema(
40754075
DatetimeSchema,
40764076
TimedeltaSchema,
40774077
LiteralSchema,
4078+
UnsetSentinelSchema,
40784079
EnumSchema,
40794080
IsInstanceSchema,
40804081
IsSubclassSchema,
@@ -4133,6 +4134,7 @@ def definition_reference_schema(
41334134
'datetime',
41344135
'timedelta',
41354136
'literal',
4137+
'unset-sentinel',
41364138
'enum',
41374139
'is-instance',
41384140
'is-subclass',

src/common/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub(crate) mod prebuilt;
22
pub(crate) mod union;
3+
pub(crate) mod unset_sentinel;

src/common/unset_sentinel.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use pyo3::intern;
2+
use pyo3::prelude::*;
3+
use pyo3::sync::GILOnceCell;
4+
5+
static UNSET_SENTINEL_OBJECT: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
6+
7+
pub fn get_unset_sentinel_object(py: Python) -> &Bound<'_, PyAny> {
8+
UNSET_SENTINEL_OBJECT
9+
.get_or_init(py, || {
10+
py.import(intern!(py, "pydantic_core"))
11+
.and_then(|core_module| core_module.getattr(intern!(py, "UNSET")))
12+
.unwrap()
13+
.into()
14+
})
15+
.bind(py)
16+
}

src/errors/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,8 @@ error_types! {
316316
expected: {ctx_type: String, ctx_fn: field_from_context},
317317
},
318318
// ---------------------
319+
// unset sentinel
320+
UnsetSentinelError {},
319321
// date errors
320322
DateType {},
321323
DateParsing {
@@ -531,6 +533,7 @@ impl ErrorType {
531533
Self::AssertionError {..} => "Assertion failed, {error}",
532534
Self::CustomError {..} => "", // custom errors are handled separately
533535
Self::LiteralError {..} => "Input should be {expected}",
536+
Self::UnsetSentinelError { .. } => "Input should be the 'UNSET' sentinel",
534537
Self::DateType {..} => "Input should be a valid date",
535538
Self::DateParsing {..} => "Input should be a valid date in the format YYYY-MM-DD, {error}",
536539
Self::DateFromDatetimeParsing {..} => "Input should be a valid date or datetime, {error}",

src/serializers/computed_fields.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ use pyo3::{intern, PyTraverseError, PyVisit};
44
use serde::ser::SerializeMap;
55

66
use crate::build_tools::py_schema_error_type;
7+
use crate::common::unset_sentinel::get_unset_sentinel_object;
78
use crate::definitions::DefinitionsBuilder;
89
use crate::py_gc::PyGcTraverse;
910
use crate::serializers::filter::SchemaFilter;
10-
use crate::serializers::shared::{get_unset_sentinel_object, BuildSerializer, CombinedSerializer, PydanticSerializer};
11+
use crate::serializers::shared::{BuildSerializer, CombinedSerializer, PydanticSerializer};
1112
use crate::tools::SchemaDict;
1213

1314
use super::errors::py_err_se_err;

src/serializers/fields.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use ahash::AHashMap;
77
use serde::ser::SerializeMap;
88
use smallvec::SmallVec;
99

10+
use crate::common::unset_sentinel::get_unset_sentinel_object;
1011
use crate::serializers::extra::SerCheck;
1112
use crate::PydanticSerializationUnexpectedValue;
1213

@@ -15,7 +16,7 @@ use super::errors::py_err_se_err;
1516
use super::extra::Extra;
1617
use super::filter::SchemaFilter;
1718
use super::infer::{infer_json_key, infer_serialize, infer_to_python, SerializeInfer};
18-
use super::shared::{get_unset_sentinel_object, CombinedSerializer, PydanticSerializer, TypeSerializer};
19+
use super::shared::{CombinedSerializer, PydanticSerializer, TypeSerializer};
1920

2021
/// representation of a field for serialization
2122
#[derive(Debug)]

src/serializers/shared.rs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,6 @@ pub(crate) trait BuildSerializer: Sized {
3434
) -> PyResult<CombinedSerializer>;
3535
}
3636

37-
static UNSET_SENTINEL_OBJECT: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
38-
39-
pub fn get_unset_sentinel_object(py: Python) -> &Bound<'_, PyAny> {
40-
UNSET_SENTINEL_OBJECT
41-
.get_or_init(py, || {
42-
py.import(intern!(py, "pydantic_core"))
43-
.and_then(|core_module| core_module.getattr(intern!(py, "UNSET")))
44-
.unwrap()
45-
.into()
46-
})
47-
.bind(py)
48-
}
49-
5037
/// Build the `CombinedSerializer` enum and implement a `find_serializer` method for it.
5138
macro_rules! combined_serializer {
5239
(
@@ -155,6 +142,7 @@ combined_serializer! {
155142
Union: super::type_serializers::union::UnionSerializer;
156143
TaggedUnion: super::type_serializers::union::TaggedUnionSerializer;
157144
Literal: super::type_serializers::literal::LiteralSerializer;
145+
UnsetSentinel: super::type_serializers::unset_sentinel::UnsetSentinelSerializer;
158146
Enum: super::type_serializers::enum_::EnumSerializer;
159147
Recursive: super::type_serializers::definitions::DefinitionRefSerializer;
160148
Tuple: super::type_serializers::tuple::TupleSerializer;
@@ -356,6 +344,7 @@ impl PyGcTraverse for CombinedSerializer {
356344
CombinedSerializer::Union(inner) => inner.py_gc_traverse(visit),
357345
CombinedSerializer::TaggedUnion(inner) => inner.py_gc_traverse(visit),
358346
CombinedSerializer::Literal(inner) => inner.py_gc_traverse(visit),
347+
CombinedSerializer::UnsetSentinel(inner) => inner.py_gc_traverse(visit),
359348
CombinedSerializer::Enum(inner) => inner.py_gc_traverse(visit),
360349
CombinedSerializer::Recursive(inner) => inner.py_gc_traverse(visit),
361350
CombinedSerializer::Tuple(inner) => inner.py_gc_traverse(visit),

src/serializers/type_serializers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod timedelta;
2525
pub mod tuple;
2626
pub mod typed_dict;
2727
pub mod union;
28+
pub mod unset_sentinel;
2829
pub mod url;
2930
pub mod uuid;
3031
pub mod with_default;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// This serializer is defined so that building a schema serializer containing an
2+
// 'unset-sentinel' core schema doesn't crash. In practice, the serializer isn't
3+
// used for model-like classes, as the 'fields' serializer takes care of omitting
4+
// the fields from the output (the serializer can still be used if the 'unset-sentinel'
5+
// core schema is used standalone (e.g. with a Pydantic type adapter), but this isn't
6+
// something we explicitly support.
7+
8+
use std::borrow::Cow;
9+
10+
use pyo3::prelude::*;
11+
use pyo3::types::PyDict;
12+
13+
use serde::ser::Error;
14+
15+
use crate::common::unset_sentinel::get_unset_sentinel_object;
16+
use crate::definitions::DefinitionsBuilder;
17+
use crate::PydanticSerializationUnexpectedValue;
18+
19+
use super::{BuildSerializer, CombinedSerializer, Extra, TypeSerializer};
20+
21+
#[derive(Debug)]
22+
pub struct UnsetSentinelSerializer {}
23+
24+
impl BuildSerializer for UnsetSentinelSerializer {
25+
const EXPECTED_TYPE: &'static str = "unset-sentinel";
26+
27+
fn build(
28+
_schema: &Bound<'_, PyDict>,
29+
_config: Option<&Bound<'_, PyDict>>,
30+
_definitions: &mut DefinitionsBuilder<CombinedSerializer>,
31+
) -> PyResult<CombinedSerializer> {
32+
Ok(Self {}.into())
33+
}
34+
}
35+
36+
impl_py_gc_traverse!(UnsetSentinelSerializer {});
37+
38+
impl TypeSerializer for UnsetSentinelSerializer {
39+
fn to_python(
40+
&self,
41+
value: &Bound<'_, PyAny>,
42+
_include: Option<&Bound<'_, PyAny>>,
43+
_exclude: Option<&Bound<'_, PyAny>>,
44+
_extra: &Extra,
45+
) -> PyResult<PyObject> {
46+
let unset_obj = get_unset_sentinel_object(value.py());
47+
48+
if value.is(unset_obj) {
49+
Ok(unset_obj.to_owned().into())
50+
} else {
51+
Err(
52+
PydanticSerializationUnexpectedValue::new_from_msg(Some("Expected 'UNSET' sentinel".to_string()))
53+
.to_py_err(),
54+
)
55+
}
56+
}
57+
58+
fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult<Cow<'a, str>> {
59+
self.invalid_as_json_key(key, extra, Self::EXPECTED_TYPE)
60+
}
61+
62+
fn serde_serialize<S: serde::ser::Serializer>(
63+
&self,
64+
_value: &Bound<'_, PyAny>,
65+
_serializer: S,
66+
_include: Option<&Bound<'_, PyAny>>,
67+
_exclude: Option<&Bound<'_, PyAny>>,
68+
_extra: &Extra,
69+
) -> Result<S::Ok, S::Error> {
70+
Err(Error::custom("'UNSET' can't be serialized to JSON".to_string()))
71+
}
72+
73+
fn get_name(&self) -> &str {
74+
Self::EXPECTED_TYPE
75+
}
76+
}

src/validators/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ mod timedelta;
5959
mod tuple;
6060
mod typed_dict;
6161
mod union;
62+
mod unset_sentinel;
6263
mod url;
6364
mod uuid;
6465
mod validation_state;
@@ -574,6 +575,8 @@ fn build_validator_inner(
574575
call::CallValidator,
575576
// literals
576577
literal::LiteralValidator,
578+
// unset sentinel
579+
unset_sentinel::UnsetSentinelValidator,
577580
// enums
578581
enum_::BuildEnumValidator,
579582
// any
@@ -741,6 +744,8 @@ pub enum CombinedValidator {
741744
FunctionCall(call::CallValidator),
742745
// literals
743746
Literal(literal::LiteralValidator),
747+
// Unset sentinel
748+
UnsetSentinel(unset_sentinel::UnsetSentinelValidator),
744749
// enums
745750
IntEnum(enum_::EnumValidator<enum_::IntEnumValidator>),
746751
StrEnum(enum_::EnumValidator<enum_::StrEnumValidator>),

0 commit comments

Comments
 (0)