diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index 875c99da8..74fde428f 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -304,6 +304,7 @@ class SchemaSerializer: exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal['none', 'warn', 'error'] = True, fallback: Callable[[Any], Any] | None = None, @@ -324,6 +325,7 @@ class SchemaSerializer: e.g. are not included in `__pydantic_fields_set__`. exclude_defaults: Whether to exclude fields that are equal to their default value. exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. round_trip: Whether to enable serialization and validation round-trip support. warnings: How to handle invalid fields. False/"none" ignores them, True/"warn" logs errors, "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. @@ -351,6 +353,7 @@ class SchemaSerializer: exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal['none', 'warn', 'error'] = True, fallback: Callable[[Any], Any] | None = None, @@ -372,6 +375,7 @@ class SchemaSerializer: e.g. are not included in `__pydantic_fields_set__`. exclude_defaults: Whether to exclude fields that are equal to their default value. exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. round_trip: Whether to enable serialization and validation round-trip support. warnings: How to handle invalid fields. False/"none" ignores them, True/"warn" logs errors, "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index d03dded87..dc8f55b7c 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -169,6 +169,11 @@ def exclude_none(self) -> bool: """The `exclude_none` argument set during serialization.""" ... + @property + def exclude_computed_fields(self) -> bool: + """The `exclude_computed_fields` argument set during serialization.""" + ... + @property def serialize_as_any(self) -> bool: """The `serialize_as_any` argument set during serialization.""" diff --git a/src/serializers/computed_fields.rs b/src/serializers/computed_fields.rs index 9870a6682..ff83800bc 100644 --- a/src/serializers/computed_fields.rs +++ b/src/serializers/computed_fields.rs @@ -127,8 +127,8 @@ impl ComputedFields { convert_error: impl FnOnce(PyErr) -> E, mut serialize: impl FnMut(ComputedFieldToSerialize<'a, 'py>) -> Result<(), E>, ) -> Result<(), E> { - if extra.round_trip { - // Do not serialize computed fields + // In round trip mode, exclude computed fields: + if extra.round_trip || extra.exclude_computed_fields { return Ok(()); } diff --git a/src/serializers/extra.rs b/src/serializers/extra.rs index 0b1036bd5..f940e4dea 100644 --- a/src/serializers/extra.rs +++ b/src/serializers/extra.rs @@ -60,6 +60,7 @@ impl SerializationState { false, false, exclude_none, + false, round_trip, &self.config, &self.rec_guard, @@ -86,6 +87,7 @@ pub(crate) struct Extra<'a> { pub exclude_unset: bool, pub exclude_defaults: bool, pub exclude_none: bool, + pub exclude_computed_fields: bool, pub round_trip: bool, pub config: &'a SerializationConfig, pub rec_guard: &'a SerRecursionState, @@ -112,6 +114,7 @@ impl<'a> Extra<'a> { exclude_unset: bool, exclude_defaults: bool, exclude_none: bool, + exclude_computed_fields: bool, round_trip: bool, config: &'a SerializationConfig, rec_guard: &'a SerRecursionState, @@ -128,6 +131,7 @@ impl<'a> Extra<'a> { exclude_unset, exclude_defaults, exclude_none, + exclude_computed_fields, round_trip, config, rec_guard, @@ -196,6 +200,7 @@ pub(crate) struct ExtraOwned { exclude_unset: bool, exclude_defaults: bool, exclude_none: bool, + exclude_computed_fields: bool, round_trip: bool, config: SerializationConfig, rec_guard: SerRecursionState, @@ -217,6 +222,7 @@ impl ExtraOwned { exclude_unset: extra.exclude_unset, exclude_defaults: extra.exclude_defaults, exclude_none: extra.exclude_none, + exclude_computed_fields: extra.exclude_computed_fields, round_trip: extra.round_trip, config: extra.config.clone(), rec_guard: extra.rec_guard.clone(), @@ -239,6 +245,7 @@ impl ExtraOwned { exclude_unset: self.exclude_unset, exclude_defaults: self.exclude_defaults, exclude_none: self.exclude_none, + exclude_computed_fields: self.exclude_computed_fields, round_trip: self.round_trip, config: &self.config, rec_guard: &self.rec_guard, diff --git a/src/serializers/fields.rs b/src/serializers/fields.rs index 907078e00..f6c4ffc1a 100644 --- a/src/serializers/fields.rs +++ b/src/serializers/fields.rs @@ -226,7 +226,7 @@ impl GeneralFieldsSerializer { if extra.check.enabled() // If any of these are true we can't count fields - && !(extra.exclude_defaults || extra.exclude_unset || extra.exclude_none || exclude.is_some()) + && !(extra.exclude_defaults || extra.exclude_unset || extra.exclude_none || extra.exclude_computed_fields || exclude.is_some()) // Check for missing fields, we can't have extra fields here && self.required_fields > used_req_fields { diff --git a/src/serializers/infer.rs b/src/serializers/infer.rs index 285c2f99a..acd94c3d1 100644 --- a/src/serializers/infer.rs +++ b/src/serializers/infer.rs @@ -102,6 +102,7 @@ pub(crate) fn infer_to_python_known( extra.exclude_unset, extra.exclude_defaults, extra.exclude_none, + extra.exclude_computed_fields, extra.round_trip, extra.rec_guard, extra.serialize_unknown, @@ -496,6 +497,7 @@ pub(crate) fn infer_serialize_known( extra.exclude_unset, extra.exclude_defaults, extra.exclude_none, + extra.exclude_computed_fields, extra.round_trip, extra.rec_guard, extra.serialize_unknown, diff --git a/src/serializers/mod.rs b/src/serializers/mod.rs index acecaf749..3056f8fba 100644 --- a/src/serializers/mod.rs +++ b/src/serializers/mod.rs @@ -60,6 +60,7 @@ impl SchemaSerializer { exclude_unset: bool, exclude_defaults: bool, exclude_none: bool, + exclude_computed_fields: bool, round_trip: bool, rec_guard: &'a SerRecursionState, serialize_unknown: bool, @@ -75,6 +76,7 @@ impl SchemaSerializer { exclude_unset, exclude_defaults, exclude_none, + exclude_computed_fields, round_trip, &self.config, rec_guard, @@ -108,8 +110,8 @@ impl SchemaSerializer { #[allow(clippy::too_many_arguments)] #[pyo3(signature = (value, *, mode = None, include = None, exclude = None, by_alias = None, - exclude_unset = false, exclude_defaults = false, exclude_none = false, round_trip = false, warnings = WarningsArg::Bool(true), - fallback = None, serialize_as_any = false, context = None))] + exclude_unset = false, exclude_defaults = false, exclude_none = false, exclude_computed_fields = false, + round_trip = false, warnings = WarningsArg::Bool(true), fallback = None, serialize_as_any = false, context = None))] pub fn to_python( &self, py: Python, @@ -121,6 +123,7 @@ impl SchemaSerializer { exclude_unset: bool, exclude_defaults: bool, exclude_none: bool, + exclude_computed_fields: bool, round_trip: bool, warnings: WarningsArg, fallback: Option<&Bound<'_, PyAny>>, @@ -142,6 +145,7 @@ impl SchemaSerializer { exclude_unset, exclude_defaults, exclude_none, + exclude_computed_fields, round_trip, &rec_guard, false, @@ -156,8 +160,8 @@ impl SchemaSerializer { #[allow(clippy::too_many_arguments)] #[pyo3(signature = (value, *, indent = None, ensure_ascii = false, include = None, exclude = None, by_alias = None, - exclude_unset = false, exclude_defaults = false, exclude_none = false, round_trip = false, warnings = WarningsArg::Bool(true), - fallback = None, serialize_as_any = false, context = None))] + exclude_unset = false, exclude_defaults = false, exclude_none = false, exclude_computed_fields = false, + round_trip = false, warnings = WarningsArg::Bool(true), fallback = None, serialize_as_any = false, context = None))] pub fn to_json( &self, py: Python, @@ -170,6 +174,7 @@ impl SchemaSerializer { exclude_unset: bool, exclude_defaults: bool, exclude_none: bool, + exclude_computed_fields: bool, round_trip: bool, warnings: WarningsArg, fallback: Option<&Bound<'_, PyAny>>, @@ -190,6 +195,7 @@ impl SchemaSerializer { exclude_unset, exclude_defaults, exclude_none, + exclude_computed_fields, round_trip, &rec_guard, false, diff --git a/src/serializers/type_serializers/function.rs b/src/serializers/type_serializers/function.rs index 9a351694c..6504aee77 100644 --- a/src/serializers/type_serializers/function.rs +++ b/src/serializers/type_serializers/function.rs @@ -559,6 +559,8 @@ struct SerializationInfo { #[pyo3(get)] exclude_none: bool, #[pyo3(get)] + exclude_computed_fields: bool, + #[pyo3(get)] round_trip: bool, field_name: Option, #[pyo3(get)] @@ -583,6 +585,7 @@ impl SerializationInfo { exclude_unset: extra.exclude_unset, exclude_defaults: extra.exclude_defaults, exclude_none: extra.exclude_none, + exclude_computed_fields: extra.exclude_none, round_trip: extra.round_trip, field_name: Some(field_name.to_string()), serialize_as_any: extra.serialize_as_any, @@ -601,6 +604,7 @@ impl SerializationInfo { exclude_unset: extra.exclude_unset, exclude_defaults: extra.exclude_defaults, exclude_none: extra.exclude_none, + exclude_computed_fields: extra.exclude_computed_fields, round_trip: extra.round_trip, field_name: None, serialize_as_any: extra.serialize_as_any, @@ -651,6 +655,7 @@ impl SerializationInfo { d.set_item("exclude_unset", self.exclude_unset)?; d.set_item("exclude_defaults", self.exclude_defaults)?; d.set_item("exclude_none", self.exclude_none)?; + d.set_item("exclude_computed_fields", self.exclude_computed_fields)?; d.set_item("round_trip", self.round_trip)?; d.set_item("serialize_as_any", self.serialize_as_any)?; Ok(d) @@ -658,7 +663,7 @@ impl SerializationInfo { fn __repr__(&self, py: Python) -> PyResult { Ok(format!( - "SerializationInfo(include={}, exclude={}, context={}, mode='{}', by_alias={}, exclude_unset={}, exclude_defaults={}, exclude_none={}, round_trip={}, serialize_as_any={})", + "SerializationInfo(include={}, exclude={}, context={}, mode='{}', by_alias={}, exclude_unset={}, exclude_defaults={}, exclude_none={}, exclude_computed_fields={}, round_trip={}, serialize_as_any={})", match self.include { Some(ref include) => include.bind(py).repr()?.to_str()?.to_owned(), None => "None".to_owned(), @@ -676,6 +681,7 @@ impl SerializationInfo { py_bool(self.exclude_unset), py_bool(self.exclude_defaults), py_bool(self.exclude_none), + py_bool(self.exclude_computed_fields), py_bool(self.round_trip), py_bool(self.serialize_as_any), )) diff --git a/tests/serializers/test_functions.py b/tests/serializers/test_functions.py index ec1ced855..c33cae7b9 100644 --- a/tests/serializers/test_functions.py +++ b/tests/serializers/test_functions.py @@ -72,6 +72,7 @@ def double(value, info): 'exclude_unset': False, 'exclude_defaults': False, 'exclude_none': False, + 'exclude_computed_fields': False, 'round_trip': False, 'serialize_as_any': False, } @@ -85,6 +86,7 @@ def double(value, info): 'exclude_unset': False, 'exclude_defaults': False, 'exclude_none': False, + 'exclude_computed_fields': False, 'round_trip': False, 'serialize_as_any': False, } @@ -97,6 +99,7 @@ def double(value, info): 'exclude_unset': False, 'exclude_defaults': False, 'exclude_none': False, + 'exclude_computed_fields': False, 'round_trip': False, 'serialize_as_any': False, } @@ -109,6 +112,7 @@ def double(value, info): 'exclude_unset': True, 'exclude_defaults': False, 'exclude_none': False, + 'exclude_computed_fields': False, 'round_trip': False, 'serialize_as_any': False, } @@ -123,6 +127,7 @@ def double(value, info): 'exclude_unset': False, 'exclude_defaults': False, 'exclude_none': False, + 'exclude_computed_fields': False, 'round_trip': False, 'serialize_as_any': False, } @@ -136,6 +141,7 @@ def double(value, info): 'exclude_unset': False, 'exclude_defaults': False, 'exclude_none': False, + 'exclude_computed_fields': False, 'round_trip': False, 'serialize_as_any': False, } @@ -231,27 +237,27 @@ def append_args(value, info): ) assert s.to_python(123) == ( "123 info=SerializationInfo(include=None, exclude=None, context=None, mode='python', by_alias=False, exclude_unset=False, " - 'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)' + 'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)' ) assert s.to_python(123, mode='other') == ( "123 info=SerializationInfo(include=None, exclude=None, context=None, mode='other', by_alias=False, exclude_unset=False, " - 'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)' + 'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)' ) assert s.to_python(123, include={'x'}) == ( "123 info=SerializationInfo(include={'x'}, exclude=None, context=None, mode='python', by_alias=False, exclude_unset=False, " - 'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)' + 'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)' ) assert s.to_python(123, context='context') == ( "123 info=SerializationInfo(include=None, exclude=None, context='context', mode='python', by_alias=False, exclude_unset=False, " - 'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)' + 'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)' ) assert s.to_python(123, mode='json', exclude={1: {2}}) == ( "123 info=SerializationInfo(include=None, exclude={1: {2}}, context=None, mode='json', by_alias=False, exclude_unset=False, " - 'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)' + 'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)' ) assert s.to_json(123) == ( b"\"123 info=SerializationInfo(include=None, exclude=None, context=None, mode='json', by_alias=False, exclude_unset=False, " - b'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)"' + b'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)"' ) diff --git a/tests/serializers/test_model.py b/tests/serializers/test_model.py index 74712685f..963584c63 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -2,14 +2,10 @@ import json import platform import warnings +from functools import cached_property from random import randint from typing import Any, ClassVar -try: - from functools import cached_property -except ImportError: - cached_property = None - import pytest from dirty_equals import IsJson @@ -504,7 +500,6 @@ def ser_x(self, v: Any, _) -> str: assert s.to_python(Model(x=1000)) == {'x': '1_000'} -@pytest.mark.skipif(cached_property is None, reason='cached_property is not available') def test_field_serializer_cached_property(): @dataclasses.dataclass class Model: @@ -709,6 +704,9 @@ def area(self) -> bytes: assert s.to_python(Model(width=3, height=4), round_trip=True) == {'width': 3, 'height': 4} assert s.to_json(Model(width=3, height=4), round_trip=True) == b'{"width":3,"height":4}' + assert s.to_python(Model(width=3, height=4), exclude_computed_fields=True) == {'width': 3, 'height': 4} + assert s.to_json(Model(width=3, height=4), exclude_computed_fields=True) == b'{"width":3,"height":4}' + def test_property_alias(): @dataclasses.dataclass @@ -881,7 +879,6 @@ def area(self) -> int: assert s.to_json(Model(3, 4), exclude_none=True, by_alias=True) == b'{"width":3,"height":4,"Area":12}' -@pytest.mark.skipif(cached_property is None, reason='cached_property is not available') def test_cached_property_alias(): @dataclasses.dataclass class Model: @@ -996,7 +993,6 @@ def b(self): assert s.to_json(Model(1), exclude={'b': [0]}) == b'{"a":1,"b":[2,"3"]}' -@pytest.mark.skipif(cached_property is None, reason='cached_property is not available') def test_property_setter(): class Square: side: float diff --git a/tests/test.rs b/tests/test.rs index 90f8062d3..88042d337 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -151,6 +151,7 @@ dump_json_input_2 = {'a': 'something'} false, false, false, + false, WarningsArg::Bool(false), None, false, @@ -173,6 +174,7 @@ dump_json_input_2 = {'a': 'something'} false, false, false, + false, WarningsArg::Bool(false), None, false, diff --git a/tests/test_typing.py b/tests/test_typing.py index 20a50a368..593b35fd8 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -233,7 +233,7 @@ def f(input: Any, info: core_schema.SerializationInfo, /) -> str: ) assert s.to_python(123) == ( "SerializationInfo(include=None, exclude=None, context=None, mode='python', by_alias=False, exclude_unset=False, " - 'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)' + 'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)' ) @@ -254,7 +254,7 @@ def f( assert s.to_python(123, mode='json') == ( 'SerializationCallable(serializer=str) ' "SerializationInfo(include=None, exclude=None, context=None, mode='json', by_alias=False, exclude_unset=False, " - 'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)' + 'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)' )