Skip to content

Commit c18c875

Browse files
Add uuid validator (#772)
Co-authored-by: David Hewitt <[email protected]>
1 parent 6ad908c commit c18c875

File tree

20 files changed

+758
-1
lines changed

20 files changed

+758
-1
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ idna = "0.4.0"
4343
base64 = "0.21.2"
4444
num-bigint = "0.4.3"
4545
python3-dll-a = "0.2.7"
46+
uuid = "1.3.4"
4647

4748
[lib]
4849
name = "_pydantic_core"

python/pydantic_core/core_schema.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def field_name(self) -> str:
206206
'url',
207207
'multi-host-url',
208208
'json',
209+
'uuid',
209210
]
210211

211212

@@ -1199,6 +1200,28 @@ def callable_schema(
11991200
return _dict_not_none(type='callable', ref=ref, metadata=metadata, serialization=serialization)
12001201

12011202

1203+
class UuidSchema(TypedDict, total=False):
1204+
type: Required[Literal['uuid']]
1205+
version: Literal[1, 3, 4, 5]
1206+
strict: bool
1207+
ref: str
1208+
metadata: Any
1209+
serialization: SerSchema
1210+
1211+
1212+
def uuid_schema(
1213+
*,
1214+
version: Literal[1, 3, 4, 5] | None = None,
1215+
strict: bool | None = None,
1216+
ref: str | None = None,
1217+
metadata: Any = None,
1218+
serialization: SerSchema | None = None,
1219+
) -> UuidSchema:
1220+
return _dict_not_none(
1221+
type='uuid', version=version, strict=strict, ref=ref, metadata=metadata, serialization=serialization
1222+
)
1223+
1224+
12021225
class IncExSeqSerSchema(TypedDict, total=False):
12031226
type: Required[Literal['include-exclude-sequence']]
12041227
include: Set[int]
@@ -3731,6 +3754,7 @@ def definition_reference_schema(
37313754
MultiHostUrlSchema,
37323755
DefinitionsSchema,
37333756
DefinitionReferenceSchema,
3757+
UuidSchema,
37343758
]
37353759
elif False:
37363760
CoreSchema: TypeAlias = Mapping[str, Any]
@@ -3784,6 +3808,7 @@ def definition_reference_schema(
37843808
'multi-host-url',
37853809
'definitions',
37863810
'definition-ref',
3811+
'uuid',
37873812
]
37883813

37893814
CoreSchemaFieldType = Literal['model-field', 'dataclass-field', 'typed-dict-field', 'computed-field']
@@ -3879,6 +3904,9 @@ def definition_reference_schema(
38793904
'url_syntax_violation',
38803905
'url_too_long',
38813906
'url_scheme',
3907+
'uuid_type',
3908+
'uuid_parsing',
3909+
'uuid_version',
38823910
]
38833911

38843912

src/errors/types.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,14 @@ pub enum ErrorType {
308308
UrlScheme {
309309
expected_schemes: String,
310310
},
311+
// UUID errors,
312+
UuidType,
313+
UuidParsing {
314+
error: String,
315+
},
316+
UuidVersion {
317+
expected_version: usize,
318+
},
311319
}
312320

313321
macro_rules! render {
@@ -451,6 +459,10 @@ impl ErrorType {
451459
Self::UrlSyntaxViolation { .. } => extract_context!(Cow::Owned, UrlSyntaxViolation, ctx, error: String),
452460
Self::UrlTooLong { .. } => extract_context!(UrlTooLong, ctx, max_length: usize),
453461
Self::UrlScheme { .. } => extract_context!(UrlScheme, ctx, expected_schemes: String),
462+
Self::UuidParsing { .. } => extract_context!(UuidParsing, ctx, error: String),
463+
Self::UuidVersion { .. } => {
464+
extract_context!(UuidVersion, ctx, expected_version: usize)
465+
}
454466
_ => {
455467
if ctx.is_some() {
456468
py_err!(PyTypeError; "'{}' errors do not require context", value)
@@ -555,6 +567,10 @@ impl ErrorType {
555567
Self::UrlSyntaxViolation {..} => "Input violated strict URL syntax rules, {error}",
556568
Self::UrlTooLong {..} => "URL should have at most {max_length} characters",
557569
Self::UrlScheme {..} => "URL scheme should be {expected_schemes}",
570+
Self::UuidType => "UUID input should be a string, bytes or UUID object",
571+
Self::UuidParsing { .. } => "Input should be a valid UUID, {error}",
572+
Self::UuidVersion { .. } => "UUID version {expected_version} expected"
573+
558574
}
559575
}
560576

@@ -674,6 +690,8 @@ impl ErrorType {
674690
Self::UrlSyntaxViolation { error } => render!(tmpl, error),
675691
Self::UrlTooLong { max_length } => to_string_render!(tmpl, max_length),
676692
Self::UrlScheme { expected_schemes } => render!(tmpl, expected_schemes),
693+
Self::UuidParsing { error } => render!(tmpl, error),
694+
Self::UuidVersion { expected_version } => to_string_render!(tmpl, expected_version),
677695
_ => Ok(tmpl.to_string()),
678696
}
679697
}
@@ -734,6 +752,9 @@ impl ErrorType {
734752
Self::UrlSyntaxViolation { error } => py_dict!(py, error),
735753
Self::UrlTooLong { max_length } => py_dict!(py, max_length),
736754
Self::UrlScheme { expected_schemes } => py_dict!(py, expected_schemes),
755+
756+
Self::UuidParsing { error } => py_dict!(py, error),
757+
Self::UuidVersion { expected_version } => py_dict!(py, expected_version),
737758
_ => Ok(None),
738759
}
739760
}

src/input/return_enums.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,13 @@ impl<'a> From<&'a PyBytes> for EitherBytes<'a> {
809809
}
810810

811811
impl<'a> EitherBytes<'a> {
812+
pub fn as_slice(&'a self) -> &[u8] {
813+
match self {
814+
EitherBytes::Cow(bytes) => bytes,
815+
EitherBytes::Py(py_bytes) => py_bytes.as_bytes(),
816+
}
817+
}
818+
812819
pub fn len(&'a self) -> PyResult<usize> {
813820
match self {
814821
EitherBytes::Cow(bytes) => Ok(bytes.len()),

src/serializers/infer.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ pub(crate) fn infer_to_python_known(
187187
let py_url: PyMultiHostUrl = value.extract()?;
188188
py_url.__str__().into_py(py)
189189
}
190+
ObType::Uuid => {
191+
let py_uuid: &PyAny = value.downcast()?;
192+
let uuid = super::type_serializers::uuid::uuid_to_string(py_uuid)?;
193+
uuid.into_py(py)
194+
}
190195
ObType::PydanticSerializable => serialize_with_serializer()?,
191196
ObType::Dataclass => serialize_dict(dataclass_to_dict(value)?)?,
192197
ObType::Enum => {
@@ -483,6 +488,11 @@ pub(crate) fn infer_serialize_known<S: Serializer>(
483488
pydantic_serializer.serialize(serializer)
484489
}
485490
ObType::Dataclass => serialize_dict!(dataclass_to_dict(value).map_err(py_err_se_err)?),
491+
ObType::Uuid => {
492+
let py_uuid: &PyAny = value.downcast().map_err(py_err_se_err)?;
493+
let uuid = super::type_serializers::uuid::uuid_to_string(py_uuid).map_err(py_err_se_err)?;
494+
serializer.serialize_str(&uuid)
495+
}
486496
ObType::Enum => {
487497
let v = value.getattr(intern!(value.py(), "value")).map_err(py_err_se_err)?;
488498
infer_serialize(v, serializer, include, exclude, extra)
@@ -584,6 +594,11 @@ pub(crate) fn infer_json_key_known<'py>(ob_type: &ObType, key: &'py PyAny, extra
584594
let iso_time = super::type_serializers::datetime_etc::time_to_string(py_time)?;
585595
Ok(Cow::Owned(iso_time))
586596
}
597+
ObType::Uuid => {
598+
let py_uuid: &PyAny = key.downcast()?;
599+
let uuid = super::type_serializers::uuid::uuid_to_string(py_uuid)?;
600+
Ok(Cow::Owned(uuid))
601+
}
587602
ObType::Timedelta => {
588603
let py_timedelta: &PyDelta = key.downcast()?;
589604
extra.config.timedelta_mode.json_key(py_timedelta)

src/serializers/ob_type.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub struct ObTypeLookup {
3939
// types from this package
4040
url: usize,
4141
multi_host_url: usize,
42+
// uuid type
43+
uuid: usize,
4244
// enum type
4345
enum_type: usize,
4446
// generator
@@ -83,6 +85,7 @@ impl ObTypeLookup {
8385
enum_type: py.import("enum").unwrap().getattr("Enum").unwrap().get_type_ptr() as usize,
8486
generator: py.import("types").unwrap().getattr("GeneratorType").unwrap().as_ptr() as usize,
8587
path: py.import("pathlib").unwrap().getattr("Path").unwrap().as_ptr() as usize,
88+
uuid: py.import("uuid").unwrap().getattr("UUID").unwrap().as_ptr() as usize,
8689
}
8790
}
8891

@@ -130,6 +133,7 @@ impl ObTypeLookup {
130133
ObType::Enum => self.enum_type == ob_type,
131134
ObType::Generator => self.generator == ob_type,
132135
ObType::Path => self.path == ob_type,
136+
ObType::Uuid => is_uuid(op_value),
133137
ObType::Unknown => false,
134138
};
135139

@@ -218,6 +222,8 @@ impl ObTypeLookup {
218222
ObType::Generator
219223
} else if ob_type == self.path {
220224
ObType::Path
225+
} else if ob_type == self.uuid || is_uuid(op_value) {
226+
ObType::Uuid
221227
} else {
222228
// this allows for subtypes of the supported class types,
223229
// if `ob_type` didn't match any member of self, we try again with the next base type pointer
@@ -256,6 +262,13 @@ fn is_dataclass(op_value: Option<&PyAny>) -> bool {
256262
}
257263
}
258264

265+
fn is_uuid(op_value: Option<&PyAny>) -> bool {
266+
if let Some(value) = op_value {
267+
value.hasattr(intern!(value.py(), "int")).unwrap_or(false)
268+
} else {
269+
false
270+
}
271+
}
259272
fn is_pydantic_serializable(op_value: Option<&PyAny>) -> bool {
260273
if let Some(value) = op_value {
261274
value
@@ -315,6 +328,8 @@ pub enum ObType {
315328
Generator,
316329
// Path
317330
Path,
331+
// Uuid
332+
Uuid,
318333
// unknown type
319334
Unknown,
320335
}

src/serializers/shared.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ combined_serializer! {
129129
Dataclass: super::type_serializers::dataclass::DataclassSerializer;
130130
Url: super::type_serializers::url::UrlSerializer;
131131
MultiHostUrl: super::type_serializers::url::MultiHostUrlSerializer;
132+
Uuid: super::type_serializers::uuid::UuidSerializer;
132133
Any: super::type_serializers::any::AnySerializer;
133134
Format: super::type_serializers::format::FormatSerializer;
134135
ToString: super::type_serializers::format::ToStringSerializer;
@@ -254,6 +255,7 @@ impl PyGcTraverse for CombinedSerializer {
254255
CombinedSerializer::Recursive(inner) => inner.py_gc_traverse(visit),
255256
CombinedSerializer::TuplePositional(inner) => inner.py_gc_traverse(visit),
256257
CombinedSerializer::TupleVariable(inner) => inner.py_gc_traverse(visit),
258+
CombinedSerializer::Uuid(inner) => inner.py_gc_traverse(visit),
257259
}
258260
}
259261
}

src/serializers/type_serializers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub mod tuple;
2222
pub mod typed_dict;
2323
pub mod union;
2424
pub mod url;
25+
pub mod uuid;
2526
pub mod with_default;
2627

2728
use super::computed_fields::ComputedFields;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use std::borrow::Cow;
2+
3+
use pyo3::prelude::*;
4+
use pyo3::types::PyDict;
5+
6+
use crate::definitions::DefinitionsBuilder;
7+
8+
use super::{
9+
infer_json_key, infer_serialize, infer_to_python, py_err_se_err, BuildSerializer, CombinedSerializer, Extra,
10+
IsType, ObType, SerMode, TypeSerializer,
11+
};
12+
13+
pub(crate) fn uuid_to_string(py_uuid: &PyAny) -> PyResult<String> {
14+
Ok(py_uuid.str()?.to_string())
15+
}
16+
17+
#[derive(Debug, Clone)]
18+
pub struct UuidSerializer;
19+
20+
impl_py_gc_traverse!(UuidSerializer {});
21+
22+
impl BuildSerializer for UuidSerializer {
23+
const EXPECTED_TYPE: &'static str = "uuid";
24+
25+
fn build(
26+
_schema: &PyDict,
27+
_config: Option<&PyDict>,
28+
_definitions: &mut DefinitionsBuilder<CombinedSerializer>,
29+
) -> PyResult<CombinedSerializer> {
30+
Ok(Self {}.into())
31+
}
32+
}
33+
34+
impl TypeSerializer for UuidSerializer {
35+
fn to_python(
36+
&self,
37+
value: &PyAny,
38+
include: Option<&PyAny>,
39+
exclude: Option<&PyAny>,
40+
extra: &Extra,
41+
) -> PyResult<PyObject> {
42+
let py = value.py();
43+
match extra.ob_type_lookup.is_type(value, ObType::Uuid) {
44+
IsType::Exact | IsType::Subclass => match extra.mode {
45+
SerMode::Json => Ok(uuid_to_string(value)?.into_py(py)),
46+
_ => Ok(value.into_py(py)),
47+
},
48+
IsType::False => {
49+
extra.warnings.on_fallback_py(self.get_name(), value, extra)?;
50+
infer_to_python(value, include, exclude, extra)
51+
}
52+
}
53+
}
54+
55+
fn json_key<'py>(&self, key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
56+
match extra.ob_type_lookup.is_type(key, ObType::Uuid) {
57+
IsType::Exact | IsType::Subclass => {
58+
let str = uuid_to_string(key)?;
59+
Ok(Cow::Owned(str))
60+
}
61+
IsType::False => {
62+
extra.warnings.on_fallback_py(self.get_name(), key, extra)?;
63+
infer_json_key(key, extra)
64+
}
65+
}
66+
}
67+
68+
fn serde_serialize<S: serde::ser::Serializer>(
69+
&self,
70+
value: &PyAny,
71+
serializer: S,
72+
include: Option<&PyAny>,
73+
exclude: Option<&PyAny>,
74+
extra: &Extra,
75+
) -> Result<S::Ok, S::Error> {
76+
match extra.ob_type_lookup.is_type(value, ObType::Uuid) {
77+
IsType::Exact | IsType::Subclass => {
78+
let s = uuid_to_string(value).map_err(py_err_se_err)?;
79+
serializer.serialize_str(&s)
80+
}
81+
IsType::False => {
82+
extra.warnings.on_fallback_ser::<S>(self.get_name(), value, extra)?;
83+
infer_serialize(value, serializer, include, exclude, extra)
84+
}
85+
}
86+
}
87+
88+
fn get_name(&self) -> &str {
89+
Self::EXPECTED_TYPE
90+
}
91+
}

src/validators/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ mod tuple;
5353
mod typed_dict;
5454
mod union;
5555
mod url;
56+
mod uuid;
5657
mod with_default;
5758

5859
pub use with_default::DefaultType;
@@ -507,6 +508,8 @@ pub fn build_validator<'a>(
507508
// url types
508509
url::UrlValidator,
509510
url::MultiHostUrlValidator,
511+
// uuid types
512+
uuid::UuidValidator,
510513
// recursive (self-referencing) models
511514
definitions::DefinitionRefValidator,
512515
definitions::DefinitionsValidatorBuilder,
@@ -650,6 +653,8 @@ pub enum CombinedValidator {
650653
// url types
651654
Url(url::UrlValidator),
652655
MultiHostUrl(url::MultiHostUrlValidator),
656+
// uuid types
657+
Uuid(uuid::UuidValidator),
653658
// reference to definition, useful for recursive (self-referencing) models
654659
DefinitionRef(definitions::DefinitionRefValidator),
655660
// input dependent

0 commit comments

Comments
 (0)