Skip to content

Commit

Permalink
adding to_jsonable_python method (#500)
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin authored Mar 30, 2023
1 parent eab7e5d commit b384a37
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 4 deletions.
2 changes: 2 additions & 0 deletions pydantic_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ValidationError,
__version__,
to_json,
to_jsonable_python,
)
from .core_schema import CoreConfig, CoreSchema, CoreSchemaType

Expand Down Expand Up @@ -48,6 +49,7 @@
'PydanticSerializationError',
'PydanticSerializationUnexpectedValue',
'to_json',
'to_jsonable_python',
)


Expand Down
11 changes: 11 additions & 0 deletions pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ def to_json(
bytes_mode: Literal['utf8', 'base64'] = 'utf8',
serialize_unknown: bool = False,
) -> bytes: ...
def to_jsonable_python(
value: Any,
*,
include: IncEx = None,
exclude: IncEx = None,
exclude_none: bool = False,
round_trip: bool = False,
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
bytes_mode: Literal['utf8', 'base64'] = 'utf8',
serialize_unknown: bool = False,
) -> Any: ...

class Url:
@property
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ pub use self::url::{PyMultiHostUrl, PyUrl};
pub use args_kwargs::ArgsKwargs;
pub use build_tools::SchemaError;
pub use errors::{list_all_errors, PydanticCustomError, PydanticKnownError, PydanticOmit, ValidationError};
pub use serializers::{to_json, PydanticSerializationError, PydanticSerializationUnexpectedValue, SchemaSerializer};
pub use serializers::{
to_json, to_jsonable_python, PydanticSerializationError, PydanticSerializationUnexpectedValue, SchemaSerializer,
};
pub use validators::SchemaValidator;

pub fn get_version() -> String {
Expand Down Expand Up @@ -56,6 +58,7 @@ fn _pydantic_core(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<ArgsKwargs>()?;
m.add_class::<SchemaSerializer>()?;
m.add_function(wrap_pyfunction!(to_json, m)?)?;
m.add_function(wrap_pyfunction!(to_jsonable_python, m)?)?;
m.add_function(wrap_pyfunction!(list_all_errors, m)?)?;
Ok(())
}
8 changes: 7 additions & 1 deletion src/serializers/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,13 @@ pub(crate) fn infer_to_python_known(
}
PyList::new(py, items).into_py(py)
}
ObType::Unknown => return Err(unknown_type_error(value)),
ObType::Unknown => {
return if extra.serialize_unknown {
Ok(serialize_unknown(value).into_py(py))
} else {
Err(unknown_type_error(value))
};
}
},
_ => match ob_type {
ObType::Tuple => {
Expand Down
37 changes: 37 additions & 0 deletions src/serializers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,43 @@ pub fn to_json(
Ok(py_bytes.into())
}

#[allow(clippy::too_many_arguments)]
#[pyfunction]
#[pyo3(signature = (value, *, include = None, exclude = None, exclude_none = false, round_trip = false,
timedelta_mode = None, bytes_mode = None, serialize_unknown = false))]
pub fn to_jsonable_python(
py: Python,
value: &PyAny,
include: Option<&PyAny>,
exclude: Option<&PyAny>,
exclude_none: Option<bool>,
round_trip: Option<bool>,
timedelta_mode: Option<&str>,
bytes_mode: Option<&str>,
serialize_unknown: Option<bool>,
) -> PyResult<PyObject> {
let warnings = CollectWarnings::new(None);
let rec_guard = SerRecursionGuard::default();
let config = SerializationConfig::from_args(timedelta_mode, bytes_mode)?;
let extra = Extra::new(
py,
&SerMode::Json,
&[],
None,
&warnings,
None,
None,
exclude_none,
round_trip,
&config,
&rec_guard,
serialize_unknown,
);
let v = infer::infer_to_python(value, include, exclude, &extra)?;
warnings.final_check(py)?;
Ok(v)
}

/// this is ugly, but would be much better if extra could be stored in `GeneralSerializeContext`
/// then `GeneralSerializeContext` got a `serialize_infer` method, but I couldn't get it to work
pub(crate) struct GeneralSerializeContext {
Expand Down
18 changes: 16 additions & 2 deletions tests/test_json.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import re

import pytest
from dirty_equals import IsList

from pydantic_core import SchemaValidator, ValidationError, to_json
from pydantic_core import PydanticSerializationError, SchemaValidator, ValidationError, to_json, to_jsonable_python

from .conftest import Err

Expand Down Expand Up @@ -184,12 +185,25 @@ def __str__(self):
def test_to_json():
assert to_json([1, 2]) == b'[1,2]'
assert to_json([1, 2], indent=2) == b'[\n 1,\n 2\n]'
assert to_json([1, b'x']) == b'[1,"x"]'

with pytest.raises(ValueError, match='Unable to serialize unknown type:'):
with pytest.raises(PydanticSerializationError, match=r'Unable to serialize unknown type: <.+\.Foobar'):
to_json(Foobar())

assert to_json(Foobar(), serialize_unknown=True) == b'"Foobar.__str__"'

# kwargs required
with pytest.raises(TypeError, match=r'to_json\(\) takes 1 positional arguments but 2 were given'):
to_json([1, 2], 2)


def test_to_jsonable_python():
assert to_jsonable_python([1, 2]) == [1, 2]
assert to_jsonable_python({1, 2}) == IsList(1, 2, check_order=False)
assert to_jsonable_python([1, b'x']) == [1, 'x']
assert to_jsonable_python([0, 1, 2, 3, 4], exclude={1, 3}) == [0, 2, 4]

with pytest.raises(PydanticSerializationError, match=r'Unable to serialize unknown type: <.+\.Foobar'):
to_jsonable_python(Foobar())

assert to_jsonable_python(Foobar(), serialize_unknown=True) == 'Foobar.__str__'

0 comments on commit b384a37

Please sign in to comment.