Skip to content

Add uuid validator #772

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 13 commits into from
Jul 24, 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
7 changes: 7 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ idna = "0.3.0"
base64 = "0.13.1"
num-bigint = "0.4.3"
python3-dll-a = "0.2.7"
uuid = "1.3.4"

[lib]
name = "_pydantic_core"
Expand Down
28 changes: 28 additions & 0 deletions python/pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ def field_name(self) -> str:
'url',
'multi-host-url',
'json',
'uuid',
]


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


class UuidSchema(TypedDict, total=False):
type: Required[Literal['uuid']]
version: Literal[1, 3, 4, 5]
strict: bool
ref: str
metadata: Any
serialization: SerSchema


def uuid_schema(
*,
version: Literal[1, 3, 4, 5] | None = None,
strict: bool | None = None,
ref: str | None = None,
metadata: Any = None,
serialization: SerSchema | None = None,
) -> UuidSchema:
return _dict_not_none(
type='uuid', version=version, strict=strict, ref=ref, metadata=metadata, serialization=serialization
)


class IncExSeqSerSchema(TypedDict, total=False):
type: Required[Literal['include-exclude-sequence']]
include: Set[int]
Expand Down Expand Up @@ -3731,6 +3754,7 @@ def definition_reference_schema(
MultiHostUrlSchema,
DefinitionsSchema,
DefinitionReferenceSchema,
UuidSchema,
]
elif False:
CoreSchema: TypeAlias = Mapping[str, Any]
Expand Down Expand Up @@ -3784,6 +3808,7 @@ def definition_reference_schema(
'multi-host-url',
'definitions',
'definition-ref',
'uuid',
]

CoreSchemaFieldType = Literal['model-field', 'dataclass-field', 'typed-dict-field', 'computed-field']
Expand Down Expand Up @@ -3879,6 +3904,9 @@ def definition_reference_schema(
'url_syntax_violation',
'url_too_long',
'url_scheme',
'uuid_type',
'uuid_parsing',
'uuid_version',
]


Expand Down
21 changes: 21 additions & 0 deletions src/errors/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,14 @@ pub enum ErrorType {
UrlScheme {
expected_schemes: String,
},
// UUID errors,
UuidType,
UuidParsing {
error: String,
},
UuidVersion {
expected_version: usize,
},
}

macro_rules! render {
Expand Down Expand Up @@ -451,6 +459,10 @@ impl ErrorType {
Self::UrlSyntaxViolation { .. } => extract_context!(Cow::Owned, UrlSyntaxViolation, ctx, error: String),
Self::UrlTooLong { .. } => extract_context!(UrlTooLong, ctx, max_length: usize),
Self::UrlScheme { .. } => extract_context!(UrlScheme, ctx, expected_schemes: String),
Self::UuidParsing { .. } => extract_context!(UuidParsing, ctx, error: String),
Self::UuidVersion { .. } => {
extract_context!(UuidVersion, ctx, expected_version: usize)
}
_ => {
if ctx.is_some() {
py_err!(PyTypeError; "'{}' errors do not require context", value)
Expand Down Expand Up @@ -555,6 +567,10 @@ impl ErrorType {
Self::UrlSyntaxViolation {..} => "Input violated strict URL syntax rules, {error}",
Self::UrlTooLong {..} => "URL should have at most {max_length} characters",
Self::UrlScheme {..} => "URL scheme should be {expected_schemes}",
Self::UuidType => "UUID input should be a string, bytes or UUID object",
Self::UuidParsing { .. } => "Input should be a valid UUID, {error}",
Self::UuidVersion { .. } => "UUID version {expected_version} expected"

}
}

Expand Down Expand Up @@ -674,6 +690,8 @@ impl ErrorType {
Self::UrlSyntaxViolation { error } => render!(tmpl, error),
Self::UrlTooLong { max_length } => to_string_render!(tmpl, max_length),
Self::UrlScheme { expected_schemes } => render!(tmpl, expected_schemes),
Self::UuidParsing { error } => render!(tmpl, error),
Self::UuidVersion { expected_version } => to_string_render!(tmpl, expected_version),
_ => Ok(tmpl.to_string()),
}
}
Expand Down Expand Up @@ -734,6 +752,9 @@ impl ErrorType {
Self::UrlSyntaxViolation { error } => py_dict!(py, error),
Self::UrlTooLong { max_length } => py_dict!(py, max_length),
Self::UrlScheme { expected_schemes } => py_dict!(py, expected_schemes),

Self::UuidParsing { error } => py_dict!(py, error),
Self::UuidVersion { expected_version } => py_dict!(py, expected_version),
_ => Ok(None),
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/input/return_enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,13 @@ impl<'a> From<&'a PyBytes> for EitherBytes<'a> {
}

impl<'a> EitherBytes<'a> {
pub fn as_slice(&'a self) -> &[u8] {
match self {
EitherBytes::Cow(bytes) => bytes,
EitherBytes::Py(py_bytes) => py_bytes.as_bytes(),
}
}

pub fn len(&'a self) -> PyResult<usize> {
match self {
EitherBytes::Cow(bytes) => Ok(bytes.len()),
Expand Down
15 changes: 15 additions & 0 deletions src/serializers/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ pub(crate) fn infer_to_python_known(
let py_url: PyMultiHostUrl = value.extract()?;
py_url.__str__().into_py(py)
}
ObType::Uuid => {
let py_uuid: &PyAny = value.downcast()?;
let uuid = super::type_serializers::uuid::uuid_to_string(py_uuid)?;
uuid.into_py(py)
}
ObType::PydanticSerializable => serialize_with_serializer()?,
ObType::Dataclass => serialize_dict(dataclass_to_dict(value)?)?,
ObType::Enum => {
Expand Down Expand Up @@ -483,6 +488,11 @@ pub(crate) fn infer_serialize_known<S: Serializer>(
pydantic_serializer.serialize(serializer)
}
ObType::Dataclass => serialize_dict!(dataclass_to_dict(value).map_err(py_err_se_err)?),
ObType::Uuid => {
let py_uuid: &PyAny = value.downcast().map_err(py_err_se_err)?;
let uuid = super::type_serializers::uuid::uuid_to_string(py_uuid).map_err(py_err_se_err)?;
serializer.serialize_str(&uuid)
}
ObType::Enum => {
let v = value.getattr(intern!(value.py(), "value")).map_err(py_err_se_err)?;
infer_serialize(v, serializer, include, exclude, extra)
Expand Down Expand Up @@ -584,6 +594,11 @@ pub(crate) fn infer_json_key_known<'py>(ob_type: &ObType, key: &'py PyAny, extra
let iso_time = super::type_serializers::datetime_etc::time_to_string(py_time)?;
Ok(Cow::Owned(iso_time))
}
ObType::Uuid => {
let py_uuid: &PyAny = key.downcast()?;
let uuid = super::type_serializers::uuid::uuid_to_string(py_uuid)?;
Ok(Cow::Owned(uuid))
}
ObType::Timedelta => {
let py_timedelta: &PyDelta = key.downcast()?;
extra.config.timedelta_mode.json_key(py_timedelta)
Expand Down
15 changes: 15 additions & 0 deletions src/serializers/ob_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub struct ObTypeLookup {
// types from this package
url: usize,
multi_host_url: usize,
// uuid type
uuid: usize,
// enum type
enum_type: usize,
// generator
Expand Down Expand Up @@ -83,6 +85,7 @@ impl ObTypeLookup {
enum_type: py.import("enum").unwrap().getattr("Enum").unwrap().get_type_ptr() as usize,
generator: py.import("types").unwrap().getattr("GeneratorType").unwrap().as_ptr() as usize,
path: py.import("pathlib").unwrap().getattr("Path").unwrap().as_ptr() as usize,
uuid: py.import("uuid").unwrap().getattr("UUID").unwrap().as_ptr() as usize,
}
}

Expand Down Expand Up @@ -130,6 +133,7 @@ impl ObTypeLookup {
ObType::Enum => self.enum_type == ob_type,
ObType::Generator => self.generator == ob_type,
ObType::Path => self.path == ob_type,
ObType::Uuid => is_uuid(op_value),
ObType::Unknown => false,
};

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

fn is_uuid(op_value: Option<&PyAny>) -> bool {
if let Some(value) = op_value {
value.hasattr(intern!(value.py(), "int")).unwrap_or(false)
} else {
false
}
}
fn is_pydantic_serializable(op_value: Option<&PyAny>) -> bool {
if let Some(value) = op_value {
value
Expand Down Expand Up @@ -315,6 +328,8 @@ pub enum ObType {
Generator,
// Path
Path,
// Uuid
Uuid,
// unknown type
Unknown,
}
2 changes: 2 additions & 0 deletions src/serializers/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ combined_serializer! {
Dataclass: super::type_serializers::dataclass::DataclassSerializer;
Url: super::type_serializers::url::UrlSerializer;
MultiHostUrl: super::type_serializers::url::MultiHostUrlSerializer;
Uuid: super::type_serializers::uuid::UuidSerializer;
Any: super::type_serializers::any::AnySerializer;
Format: super::type_serializers::format::FormatSerializer;
ToString: super::type_serializers::format::ToStringSerializer;
Expand Down Expand Up @@ -254,6 +255,7 @@ impl PyGcTraverse for CombinedSerializer {
CombinedSerializer::Recursive(inner) => inner.py_gc_traverse(visit),
CombinedSerializer::TuplePositional(inner) => inner.py_gc_traverse(visit),
CombinedSerializer::TupleVariable(inner) => inner.py_gc_traverse(visit),
CombinedSerializer::Uuid(inner) => inner.py_gc_traverse(visit),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/serializers/type_serializers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod tuple;
pub mod typed_dict;
pub mod union;
pub mod url;
pub mod uuid;
pub mod with_default;

use super::computed_fields::ComputedFields;
Expand Down
91 changes: 91 additions & 0 deletions src/serializers/type_serializers/uuid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use std::borrow::Cow;

use pyo3::prelude::*;
use pyo3::types::PyDict;

use crate::definitions::DefinitionsBuilder;

use super::{
infer_json_key, infer_serialize, infer_to_python, py_err_se_err, BuildSerializer, CombinedSerializer, Extra,
IsType, ObType, SerMode, TypeSerializer,
};

pub(crate) fn uuid_to_string(py_uuid: &PyAny) -> PyResult<String> {
Ok(py_uuid.str()?.to_string())
}

#[derive(Debug, Clone)]
pub struct UuidSerializer;

impl_py_gc_traverse!(UuidSerializer {});

impl BuildSerializer for UuidSerializer {
const EXPECTED_TYPE: &'static str = "uuid";

fn build(
_schema: &PyDict,
_config: Option<&PyDict>,
_definitions: &mut DefinitionsBuilder<CombinedSerializer>,
) -> PyResult<CombinedSerializer> {
Ok(Self {}.into())
}
}

impl TypeSerializer for UuidSerializer {
fn to_python(
&self,
value: &PyAny,
include: Option<&PyAny>,
exclude: Option<&PyAny>,
extra: &Extra,
) -> PyResult<PyObject> {
let py = value.py();
match extra.ob_type_lookup.is_type(value, ObType::Uuid) {
IsType::Exact | IsType::Subclass => match extra.mode {
SerMode::Json => Ok(uuid_to_string(value)?.into_py(py)),
_ => Ok(value.into_py(py)),
},
IsType::False => {
extra.warnings.on_fallback_py(self.get_name(), value, extra)?;
infer_to_python(value, include, exclude, extra)
}
}
}

fn json_key<'py>(&self, key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
match extra.ob_type_lookup.is_type(key, ObType::Uuid) {
IsType::Exact | IsType::Subclass => {
let str = uuid_to_string(key)?;
Ok(Cow::Owned(str))
}
IsType::False => {
extra.warnings.on_fallback_py(self.get_name(), key, extra)?;
infer_json_key(key, extra)
}
}
}

fn serde_serialize<S: serde::ser::Serializer>(
&self,
value: &PyAny,
serializer: S,
include: Option<&PyAny>,
exclude: Option<&PyAny>,
extra: &Extra,
) -> Result<S::Ok, S::Error> {
match extra.ob_type_lookup.is_type(value, ObType::Uuid) {
IsType::Exact | IsType::Subclass => {
let s = uuid_to_string(value).map_err(py_err_se_err)?;
serializer.serialize_str(&s)
}
IsType::False => {
extra.warnings.on_fallback_ser::<S>(self.get_name(), value, extra)?;
infer_serialize(value, serializer, include, exclude, extra)
}
}
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}
}
5 changes: 5 additions & 0 deletions src/validators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ mod tuple;
mod typed_dict;
mod union;
mod url;
mod uuid;
mod with_default;

pub use with_default::DefaultType;
Expand Down Expand Up @@ -507,6 +508,8 @@ pub fn build_validator<'a>(
// url types
url::UrlValidator,
url::MultiHostUrlValidator,
// uuid types
uuid::UuidValidator,
// recursive (self-referencing) models
definitions::DefinitionRefValidator,
definitions::DefinitionsValidatorBuilder,
Expand Down Expand Up @@ -650,6 +653,8 @@ pub enum CombinedValidator {
// url types
Url(url::UrlValidator),
MultiHostUrl(url::MultiHostUrlValidator),
// uuid types
Uuid(uuid::UuidValidator),
// reference to definition, useful for recursive (self-referencing) models
DefinitionRef(definitions::DefinitionRefValidator),
// input dependent
Expand Down
Loading