Skip to content
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

Union Serialization #357

Merged
merged 27 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ba6b16d
adding error_on_fallback
samuelcolvin Jan 14, 2023
1f6c322
implementing union
samuelcolvin Jan 14, 2023
c71d23a
union working
samuelcolvin Jan 14, 2023
9bfdeba
add get_name to serializers, use it
samuelcolvin Jan 14, 2023
33d18c0
refactoring infer serializers, fix tests
samuelcolvin Jan 14, 2023
76c3bc2
restrict new-class serializers to the exact type
samuelcolvin Jan 14, 2023
f22a290
move error_on_fallback into extra
samuelcolvin Jan 14, 2023
2c176ce
key and subclass union serialization
samuelcolvin Jan 14, 2023
2e92ce0
implementing json_key everywhere else
samuelcolvin Jan 14, 2023
cc1d437
tweaks while reviewing
samuelcolvin Jan 15, 2023
ee30e3d
remove default for TypeSerializer.to_python
samuelcolvin Jan 15, 2023
7bb3b12
improve ObType for common subclasses
samuelcolvin Jan 15, 2023
01397c8
tests for tuple dict keys
samuelcolvin Jan 16, 2023
7c3bb96
tweaking tuple dict keys
samuelcolvin Jan 16, 2023
42e6e60
error on unknown serializer
samuelcolvin Jan 16, 2023
e5545c9
support all all types in serializers, test
samuelcolvin Jan 17, 2023
83afdf4
invalidate ci cache
samuelcolvin Jan 17, 2023
3f43362
failing test for NamedTuple
samuelcolvin Jan 17, 2023
c6f8d21
switching error_on_fallback -> check
samuelcolvin Jan 17, 2023
3d979eb
adding containment checks to literals
samuelcolvin Jan 17, 2023
158727f
Merge branch 'main' into union-serialization
samuelcolvin Jan 18, 2023
b3fe668
cast_as -> downcast
samuelcolvin Jan 18, 2023
200303e
checks on typeddicts
samuelcolvin Jan 18, 2023
27b7743
linting
samuelcolvin Jan 20, 2023
1bb09e4
more tests for typed-dicts
samuelcolvin Jan 20, 2023
3f8a3cf
Merge branch 'main' into union-serialization
samuelcolvin Jan 22, 2023
58582d6
Merge branch 'main' into union-serialization
samuelcolvin Jan 23, 2023
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
Prev Previous commit
Next Next commit
tweaking tuple dict keys
  • Loading branch information
samuelcolvin committed Jan 16, 2023
commit 7c3bb96ef814cffbab5a69dfee3ea63924ffb93c
2 changes: 2 additions & 0 deletions pydantic_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
PydanticKnownError,
PydanticOmit,
PydanticSerializationError,
PydanticSerializationUnexpectedValue,
SchemaError,
SchemaSerializer,
SchemaValidator,
Expand All @@ -27,4 +28,5 @@
'PydanticKnownError',
'PydanticOmit',
'PydanticSerializationError',
'PydanticSerializationUnexpectedValue',
)
3 changes: 3 additions & 0 deletions pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ class PydanticOmit(Exception):
class PydanticSerializationError(ValueError):
def __init__(self, message: str) -> None: ...

class PydanticSerializationUnexpectedValue(ValueError):
def __init__(self, message: 'str | None' = None) -> None: ...

class ErrorTypeInfo(TypedDict):
type: ErrorType
message_template: str
Expand Down
23 changes: 23 additions & 0 deletions pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,34 @@ class FunctionSerSchema(TypedDict, total=False):
return_type: ExpectedSerializationTypes


def function_ser_schema(
function: SerializeFunction, return_type: ExpectedSerializationTypes | None = None
) -> FunctionSerSchema:
"""
Returns a schema for serialization with a function.

Args:
function: The function to use for serialization
return_type: The type that the function returns
"""
return dict_not_none(type='function', function=function, return_type=return_type)


class FormatSerSchema(TypedDict, total=False):
type: Required[Literal['format']]
formatting_string: Required[str]


def format_ser_schema(formatting_string: str) -> FormatSerSchema:
"""
Returns a schema for serialization using python's `format` method.

Args:
formatting_string: String defining the format to use
"""
return FormatSerSchema(type='format', formatting_string=formatting_string)


class NewClassSerSchema(TypedDict, total=False):
type: Required[Literal['new-class']]
cls: Required[Type[Any]]
Expand Down
13 changes: 6 additions & 7 deletions src/serializers/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,16 @@ pub(super) fn py_err_se_err<T: ser::Error, E: fmt::Display>(py_error: E) -> T {
/// convert a serde serialization error into a `PyErr`
pub(super) fn se_err_py_err(error: serde_json::Error) -> PyErr {
let s = error.to_string();
return if s.starts_with(UNEXPECTED_TYPE_SER) {
if s.len() == UNEXPECTED_TYPE_SER.len() {
if let Some(msg) = s.strip_prefix(UNEXPECTED_TYPE_SER) {
if msg.is_empty() {
PydanticSerializationUnexpectedValue::new_err(None)
} else {
let msg = s[s.len()..].to_string();
PydanticSerializationUnexpectedValue::new_err(Some(msg))
PydanticSerializationUnexpectedValue::new_err(Some(msg.to_string()))
}
} else {
let msg = format!("Error serializing to JSON: {s}");
PydanticSerializationError::new_err(msg)
};
}
}

#[pyclass(extends=PyValueError, module="pydantic_core._pydantic_core")]
Expand All @@ -52,7 +51,7 @@ impl PydanticSerializationError {
&self.message
}

fn __repr__(&self) -> String {
pub fn __repr__(&self) -> String {
format!("PydanticSerializationError({})", self.message)
}
}
Expand Down Expand Up @@ -83,7 +82,7 @@ impl PydanticSerializationUnexpectedValue {
}
}

fn __repr__(&self) -> String {
pub(crate) fn __repr__(&self) -> String {
format!("PydanticSerializationUnexpectedValue({})", self.__str__())
}
}
12 changes: 9 additions & 3 deletions src/serializers/extra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,13 @@ impl CollectWarnings {
}
}

pub(crate) fn on_fallback_py(&self, field_type: &str, value: &PyAny, error_on_fallback: bool) -> PyResult<()> {
pub fn custom_warning(&self, warning: String) {
if self.active {
self.add_warning(warning);
}
}

pub fn on_fallback_py(&self, field_type: &str, value: &PyAny, error_on_fallback: bool) -> PyResult<()> {
if error_on_fallback {
Err(PydanticSerializationUnexpectedValue::new_err(None))
} else {
Expand All @@ -182,7 +188,7 @@ impl CollectWarnings {
}
}

pub(crate) fn on_fallback_ser<S: serde::ser::Serializer>(
pub fn on_fallback_ser<S: serde::ser::Serializer>(
&self,
field_type: &str,
value: &PyAny,
Expand Down Expand Up @@ -217,7 +223,7 @@ impl CollectWarnings {
}
}

pub(crate) fn final_check(&self, py: Python) -> PyResult<()> {
pub fn final_check(&self, py: Python) -> PyResult<()> {
if self.active {
match *self.warnings.borrow() {
Some(ref warnings) => {
Expand Down
4 changes: 2 additions & 2 deletions src/serializers/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,10 @@ fn unknown_type_error(value: &PyAny) -> PyErr {

pub(crate) fn infer_json_key<'py>(key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
let ob_type = extra.ob_type_lookup.get_type(key);
infer_json_key_known(key, ob_type, extra)
infer_json_key_known(&ob_type, key, extra)
}

pub(crate) fn infer_json_key_known<'py>(key: &'py PyAny, ob_type: ObType, extra: &Extra) -> PyResult<Cow<'py, str>> {
pub(crate) fn infer_json_key_known<'py>(ob_type: &ObType, key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
match ob_type {
ObType::None => Ok(Cow::Borrowed("None")),
ObType::Int | ObType::IntSubclass | ObType::Float | ObType::FloatSubclass => Ok(key.str()?.to_string_lossy()),
Expand Down
116 changes: 83 additions & 33 deletions src/serializers/type_serializers/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ use crate::build_context::BuildContext;
use crate::build_tools::{function_name, kwargs, py_error_type, SchemaDict};
use crate::PydanticSerializationUnexpectedValue;

use super::super::errors::UNEXPECTED_TYPE_SER;
use super::{
infer_json_key, infer_serialize, infer_serialize_known, infer_to_python, infer_to_python_known, BuildSerializer,
CombinedSerializer, Extra, ObType, PydanticSerializationError, SerMode, TypeSerializer,
infer_json_key, infer_json_key_known, infer_serialize, infer_serialize_known, infer_to_python,
infer_to_python_known, py_err_se_err, BuildSerializer, CombinedSerializer, Extra, ObType,
PydanticSerializationError, TypeSerializer,
};

#[derive(Debug, Clone)]
pub struct FunctionSerializer {
func: PyObject,
name: String,
function_name: String,
return_ob_type: Option<ObType>,
}
Expand All @@ -36,9 +37,11 @@ impl BuildSerializer for FunctionSerializer {
let py = schema.py();
let function = schema.get_as_req::<&PyAny>(intern!(py, "function"))?;
let function_name = function_name(function)?;
let name = format!("function[{}]", function_name);
Ok(Self {
func: function.into_py(py),
function_name,
name,
return_ob_type: match schema.get_as::<&str>(intern!(py, "return_type"))? {
Some(t) => Some(ObType::from_str(t).map_err(|_| py_error_type!("Unknown return type {:?}", t))?),
None => None,
Expand All @@ -54,17 +57,11 @@ impl FunctionSerializer {
value: &PyAny,
include: Option<&PyAny>,
exclude: Option<&PyAny>,
mode: &SerMode,
) -> Result<PyObject, String> {
extra: &Extra,
) -> PyResult<PyObject> {
let py = value.py();
let kwargs = kwargs!(py, mode: mode.to_object(py), include: include, exclude: exclude);
self.func.call(py, (value,), kwargs).map_err(|err| {
if err.is_instance_of::<PydanticSerializationUnexpectedValue>(py) {
format!("{}{}", UNEXPECTED_TYPE_SER, err)
} else {
format!("Error calling `{}`: {}", self.function_name, err)
}
})
let kwargs = kwargs!(py, mode: extra.mode.to_object(py), include: include, exclude: exclude);
self.func.call(py, (value,), kwargs)
}
}

Expand All @@ -77,23 +74,58 @@ impl TypeSerializer for FunctionSerializer {
extra: &Extra,
) -> PyResult<PyObject> {
let py = value.py();
let v = self
.call(value, include, exclude, extra.mode)
.map_err(PydanticSerializationError::new_err)?;

if let Some(ref ob_type) = self.return_ob_type {
infer_to_python_known(ob_type, v.as_ref(py), include, exclude, extra)
} else {
infer_to_python(v.as_ref(py), include, exclude, extra)
match self.call(value, include, exclude, extra) {
Ok(v) => {
let next_value = v.as_ref(py);
match self.return_ob_type {
Some(ref ob_type) => infer_to_python_known(ob_type, next_value, include, exclude, extra),
None => infer_to_python(next_value, include, exclude, extra),
}
}
Err(err) => match err.value(py).extract::<PydanticSerializationUnexpectedValue>() {
Ok(ser_err) => {
if extra.error_on_fallback {
Err(err)
} else {
extra.warnings.custom_warning(ser_err.__repr__());
infer_to_python(value, include, exclude, extra)
}
}
Err(_) => {
let new_err = py_error_type!(PydanticSerializationError; "Error calling function `{}`: {}", self.function_name, err);
new_err.set_cause(py, Some(err));
Err(new_err)
}
},
}
}

fn json_key<'py>(&self, key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
let v = self
.call(key, None, None, extra.mode)
.map_err(PydanticSerializationError::new_err)?;

infer_json_key(v.into_ref(key.py()), extra)
let py = key.py();
match self.call(key, None, None, extra) {
Ok(v) => {
let next_key = v.into_ref(py);
match self.return_ob_type {
Some(ref ob_type) => infer_json_key_known(ob_type, next_key, extra),
None => infer_json_key(next_key, extra),
}
}
Err(err) => match err.value(py).extract::<PydanticSerializationUnexpectedValue>() {
Ok(ser_err) => {
if extra.error_on_fallback {
Err(err)
} else {
extra.warnings.custom_warning(ser_err.__repr__());
infer_json_key(key, extra)
}
}
Err(_) => {
let new_err = py_error_type!(PydanticSerializationError; "Error calling function `{}`: {}", self.function_name, err);
new_err.set_cause(py, Some(err));
Err(new_err)
}
},
}
}

fn serde_serialize<S: serde::ser::Serializer>(
Expand All @@ -105,16 +137,34 @@ impl TypeSerializer for FunctionSerializer {
extra: &Extra,
) -> Result<S::Ok, S::Error> {
let py = value.py();
let return_value = self.call(value, include, exclude, extra.mode).map_err(Error::custom)?;

if let Some(ref ob_type) = self.return_ob_type {
infer_serialize_known(ob_type, return_value.as_ref(py), serializer, include, exclude, extra)
} else {
infer_serialize(return_value.as_ref(py), serializer, include, exclude, extra)
match self.call(value, include, exclude, extra) {
Ok(v) => {
let next_value = v.as_ref(py);
match self.return_ob_type {
Some(ref ob_type) => {
infer_serialize_known(ob_type, next_value, serializer, include, exclude, extra)
}
None => infer_serialize(next_value, serializer, include, exclude, extra),
}
}
Err(err) => match err.value(py).extract::<PydanticSerializationUnexpectedValue>() {
Ok(ser_err) => {
if extra.error_on_fallback {
Err(py_err_se_err(err))
} else {
extra.warnings.custom_warning(ser_err.__repr__());
infer_serialize(value, serializer, include, exclude, extra)
}
}
Err(_) => Err(Error::custom(format!(
"Error calling function `{}`: {}",
self.function_name, err
))),
},
}
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
&self.name
}
}
2 changes: 1 addition & 1 deletion src/serializers/type_serializers/new_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl TypeSerializer for NewClassSerializer {

fn json_key<'py>(&self, key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
if self.allow_value(key, extra)? {
infer_json_key_known(key, ObType::PydanticModel, extra)
infer_json_key_known(&ObType::PydanticModel, key, extra)
} else {
extra
.warnings
Expand Down
2 changes: 1 addition & 1 deletion src/serializers/type_serializers/nullable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl TypeSerializer for NullableSerializer {

fn json_key<'py>(&self, key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
match extra.ob_type_lookup.is_type(key, ObType::None) {
IsType::Exact => infer_json_key_known(key, ObType::None, extra),
IsType::Exact => infer_json_key_known(&ObType::None, key, extra),
_ => self.serializer.json_key(key, extra),
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/serializers/type_serializers/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl TypeSerializer for NoneSerializer {

fn json_key<'py>(&self, key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
match extra.ob_type_lookup.is_type(key, ObType::None) {
IsType::Exact => infer_json_key_known(key, ObType::None, extra),
IsType::Exact => infer_json_key_known(&ObType::None, key, extra),
_ => {
extra
.warnings
Expand Down Expand Up @@ -129,7 +129,7 @@ macro_rules! build_simple_serializer {

fn json_key<'py>(&self, key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
match extra.ob_type_lookup.is_type(key, $ob_type) {
IsType::Exact | IsType::Subclass => infer_json_key_known(key, $ob_type, extra),
IsType::Exact | IsType::Subclass => infer_json_key_known(&$ob_type, key, extra),
IsType::False => {
extra
.warnings
Expand Down
11 changes: 5 additions & 6 deletions src/serializers/type_serializers/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,10 @@ pub(crate) struct KeyBuilder {

impl KeyBuilder {
pub fn new() -> Self {
let mut key = String::with_capacity(31);
key.push('(');
Self { key, first: true }
Self {
key: String::with_capacity(31),
first: true,
}
}

pub fn push(&mut self, key: &str) {
Expand All @@ -357,8 +358,6 @@ impl KeyBuilder {
}

pub fn finish(self) -> String {
let mut key = self.key;
key.push(')');
key
self.key
}
}
3 changes: 2 additions & 1 deletion tests/serializers/test_any.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ def test_set_member_db(any_serializer):
(bytearray(b'foobar'), b'"foobar"'),
((1, 2, 3), b'[1,2,3]'),
({1: 2, 'a': 4}, b'{"1":2,"a":4}'),
({(1, 'a', 2): 3}, b'{"(1,a,2)":3}'),
({(1, 'a', 2): 3}, b'{"1,a,2":3}'),
({(1,): 3}, b'{"1":3}'),
(datetime(2022, 12, 3, 12, 30, 45), b'"2022-12-03T12:30:45"'),
(date(2022, 12, 3), b'"2022-12-03"'),
(time(12, 30, 45), b'"12:30:45"'),
Expand Down
4 changes: 2 additions & 2 deletions tests/serializers/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ def test_dict_any_any():
assert v.to_json({'a': 1, b'b': 2, 33: 3, True: 4}) == b'{"a":1,"b":2,"33":3,"true":4}'

assert v.to_python({(1, 2): 3}) == {(1, 2): 3}
assert v.to_python({(1, 2): 3}, mode='json') == {'(1, 2)': 3}
assert v.to_json({(1, 2): 3}) == b'{"(1, 2)":3}'
assert v.to_python({(1, 2): 3}, mode='json') == {'1,2': 3}
assert v.to_json({(1, 2): 3}) == b'{"1,2":3}'


def test_include():
Expand Down
Loading