Skip to content

Commit 5da4064

Browse files
authored
Add exclude_computed_fields serialization option (#1780)
1 parent 4ba86e8 commit 5da4064

File tree

12 files changed

+58
-24
lines changed

12 files changed

+58
-24
lines changed

python/pydantic_core/_pydantic_core.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ class SchemaSerializer:
304304
exclude_unset: bool = False,
305305
exclude_defaults: bool = False,
306306
exclude_none: bool = False,
307+
exclude_computed_fields: bool = False,
307308
round_trip: bool = False,
308309
warnings: bool | Literal['none', 'warn', 'error'] = True,
309310
fallback: Callable[[Any], Any] | None = None,
@@ -324,6 +325,7 @@ class SchemaSerializer:
324325
e.g. are not included in `__pydantic_fields_set__`.
325326
exclude_defaults: Whether to exclude fields that are equal to their default value.
326327
exclude_none: Whether to exclude fields that have a value of `None`.
328+
exclude_computed_fields: Whether to exclude computed fields.
327329
round_trip: Whether to enable serialization and validation round-trip support.
328330
warnings: How to handle invalid fields. False/"none" ignores them, True/"warn" logs errors,
329331
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
@@ -351,6 +353,7 @@ class SchemaSerializer:
351353
exclude_unset: bool = False,
352354
exclude_defaults: bool = False,
353355
exclude_none: bool = False,
356+
exclude_computed_fields: bool = False,
354357
round_trip: bool = False,
355358
warnings: bool | Literal['none', 'warn', 'error'] = True,
356359
fallback: Callable[[Any], Any] | None = None,
@@ -372,6 +375,7 @@ class SchemaSerializer:
372375
e.g. are not included in `__pydantic_fields_set__`.
373376
exclude_defaults: Whether to exclude fields that are equal to their default value.
374377
exclude_none: Whether to exclude fields that have a value of `None`.
378+
exclude_computed_fields: Whether to exclude computed fields.
375379
round_trip: Whether to enable serialization and validation round-trip support.
376380
warnings: How to handle invalid fields. False/"none" ignores them, True/"warn" logs errors,
377381
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].

python/pydantic_core/core_schema.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ def exclude_none(self) -> bool:
169169
"""The `exclude_none` argument set during serialization."""
170170
...
171171

172+
@property
173+
def exclude_computed_fields(self) -> bool:
174+
"""The `exclude_computed_fields` argument set during serialization."""
175+
...
176+
172177
@property
173178
def serialize_as_any(self) -> bool:
174179
"""The `serialize_as_any` argument set during serialization."""

src/serializers/computed_fields.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ impl ComputedFields {
127127
convert_error: impl FnOnce(PyErr) -> E,
128128
mut serialize: impl FnMut(ComputedFieldToSerialize<'a, 'py>) -> Result<(), E>,
129129
) -> Result<(), E> {
130-
if extra.round_trip {
131-
// Do not serialize computed fields
130+
// In round trip mode, exclude computed fields:
131+
if extra.round_trip || extra.exclude_computed_fields {
132132
return Ok(());
133133
}
134134

src/serializers/extra.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl SerializationState {
6060
false,
6161
false,
6262
exclude_none,
63+
false,
6364
round_trip,
6465
&self.config,
6566
&self.rec_guard,
@@ -86,6 +87,7 @@ pub(crate) struct Extra<'a> {
8687
pub exclude_unset: bool,
8788
pub exclude_defaults: bool,
8889
pub exclude_none: bool,
90+
pub exclude_computed_fields: bool,
8991
pub round_trip: bool,
9092
pub config: &'a SerializationConfig,
9193
pub rec_guard: &'a SerRecursionState,
@@ -112,6 +114,7 @@ impl<'a> Extra<'a> {
112114
exclude_unset: bool,
113115
exclude_defaults: bool,
114116
exclude_none: bool,
117+
exclude_computed_fields: bool,
115118
round_trip: bool,
116119
config: &'a SerializationConfig,
117120
rec_guard: &'a SerRecursionState,
@@ -128,6 +131,7 @@ impl<'a> Extra<'a> {
128131
exclude_unset,
129132
exclude_defaults,
130133
exclude_none,
134+
exclude_computed_fields,
131135
round_trip,
132136
config,
133137
rec_guard,
@@ -196,6 +200,7 @@ pub(crate) struct ExtraOwned {
196200
exclude_unset: bool,
197201
exclude_defaults: bool,
198202
exclude_none: bool,
203+
exclude_computed_fields: bool,
199204
round_trip: bool,
200205
config: SerializationConfig,
201206
rec_guard: SerRecursionState,
@@ -217,6 +222,7 @@ impl ExtraOwned {
217222
exclude_unset: extra.exclude_unset,
218223
exclude_defaults: extra.exclude_defaults,
219224
exclude_none: extra.exclude_none,
225+
exclude_computed_fields: extra.exclude_computed_fields,
220226
round_trip: extra.round_trip,
221227
config: extra.config.clone(),
222228
rec_guard: extra.rec_guard.clone(),
@@ -239,6 +245,7 @@ impl ExtraOwned {
239245
exclude_unset: self.exclude_unset,
240246
exclude_defaults: self.exclude_defaults,
241247
exclude_none: self.exclude_none,
248+
exclude_computed_fields: self.exclude_computed_fields,
242249
round_trip: self.round_trip,
243250
config: &self.config,
244251
rec_guard: &self.rec_guard,

src/serializers/fields.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ impl GeneralFieldsSerializer {
226226

227227
if extra.check.enabled()
228228
// If any of these are true we can't count fields
229-
&& !(extra.exclude_defaults || extra.exclude_unset || extra.exclude_none || exclude.is_some())
229+
&& !(extra.exclude_defaults || extra.exclude_unset || extra.exclude_none || extra.exclude_computed_fields || exclude.is_some())
230230
// Check for missing fields, we can't have extra fields here
231231
&& self.required_fields > used_req_fields
232232
{

src/serializers/infer.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ pub(crate) fn infer_to_python_known(
102102
extra.exclude_unset,
103103
extra.exclude_defaults,
104104
extra.exclude_none,
105+
extra.exclude_computed_fields,
105106
extra.round_trip,
106107
extra.rec_guard,
107108
extra.serialize_unknown,
@@ -496,6 +497,7 @@ pub(crate) fn infer_serialize_known<S: Serializer>(
496497
extra.exclude_unset,
497498
extra.exclude_defaults,
498499
extra.exclude_none,
500+
extra.exclude_computed_fields,
499501
extra.round_trip,
500502
extra.rec_guard,
501503
extra.serialize_unknown,

src/serializers/mod.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl SchemaSerializer {
6060
exclude_unset: bool,
6161
exclude_defaults: bool,
6262
exclude_none: bool,
63+
exclude_computed_fields: bool,
6364
round_trip: bool,
6465
rec_guard: &'a SerRecursionState,
6566
serialize_unknown: bool,
@@ -75,6 +76,7 @@ impl SchemaSerializer {
7576
exclude_unset,
7677
exclude_defaults,
7778
exclude_none,
79+
exclude_computed_fields,
7880
round_trip,
7981
&self.config,
8082
rec_guard,
@@ -108,8 +110,8 @@ impl SchemaSerializer {
108110

109111
#[allow(clippy::too_many_arguments)]
110112
#[pyo3(signature = (value, *, mode = None, include = None, exclude = None, by_alias = None,
111-
exclude_unset = false, exclude_defaults = false, exclude_none = false, round_trip = false, warnings = WarningsArg::Bool(true),
112-
fallback = None, serialize_as_any = false, context = None))]
113+
exclude_unset = false, exclude_defaults = false, exclude_none = false, exclude_computed_fields = false,
114+
round_trip = false, warnings = WarningsArg::Bool(true), fallback = None, serialize_as_any = false, context = None))]
113115
pub fn to_python(
114116
&self,
115117
py: Python,
@@ -121,6 +123,7 @@ impl SchemaSerializer {
121123
exclude_unset: bool,
122124
exclude_defaults: bool,
123125
exclude_none: bool,
126+
exclude_computed_fields: bool,
124127
round_trip: bool,
125128
warnings: WarningsArg,
126129
fallback: Option<&Bound<'_, PyAny>>,
@@ -142,6 +145,7 @@ impl SchemaSerializer {
142145
exclude_unset,
143146
exclude_defaults,
144147
exclude_none,
148+
exclude_computed_fields,
145149
round_trip,
146150
&rec_guard,
147151
false,
@@ -156,8 +160,8 @@ impl SchemaSerializer {
156160

157161
#[allow(clippy::too_many_arguments)]
158162
#[pyo3(signature = (value, *, indent = None, ensure_ascii = false, include = None, exclude = None, by_alias = None,
159-
exclude_unset = false, exclude_defaults = false, exclude_none = false, round_trip = false, warnings = WarningsArg::Bool(true),
160-
fallback = None, serialize_as_any = false, context = None))]
163+
exclude_unset = false, exclude_defaults = false, exclude_none = false, exclude_computed_fields = false,
164+
round_trip = false, warnings = WarningsArg::Bool(true), fallback = None, serialize_as_any = false, context = None))]
161165
pub fn to_json(
162166
&self,
163167
py: Python,
@@ -170,6 +174,7 @@ impl SchemaSerializer {
170174
exclude_unset: bool,
171175
exclude_defaults: bool,
172176
exclude_none: bool,
177+
exclude_computed_fields: bool,
173178
round_trip: bool,
174179
warnings: WarningsArg,
175180
fallback: Option<&Bound<'_, PyAny>>,
@@ -190,6 +195,7 @@ impl SchemaSerializer {
190195
exclude_unset,
191196
exclude_defaults,
192197
exclude_none,
198+
exclude_computed_fields,
193199
round_trip,
194200
&rec_guard,
195201
false,

src/serializers/type_serializers/function.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,8 @@ struct SerializationInfo {
559559
#[pyo3(get)]
560560
exclude_none: bool,
561561
#[pyo3(get)]
562+
exclude_computed_fields: bool,
563+
#[pyo3(get)]
562564
round_trip: bool,
563565
field_name: Option<String>,
564566
#[pyo3(get)]
@@ -583,6 +585,7 @@ impl SerializationInfo {
583585
exclude_unset: extra.exclude_unset,
584586
exclude_defaults: extra.exclude_defaults,
585587
exclude_none: extra.exclude_none,
588+
exclude_computed_fields: extra.exclude_none,
586589
round_trip: extra.round_trip,
587590
field_name: Some(field_name.to_string()),
588591
serialize_as_any: extra.serialize_as_any,
@@ -601,6 +604,7 @@ impl SerializationInfo {
601604
exclude_unset: extra.exclude_unset,
602605
exclude_defaults: extra.exclude_defaults,
603606
exclude_none: extra.exclude_none,
607+
exclude_computed_fields: extra.exclude_computed_fields,
604608
round_trip: extra.round_trip,
605609
field_name: None,
606610
serialize_as_any: extra.serialize_as_any,
@@ -651,14 +655,15 @@ impl SerializationInfo {
651655
d.set_item("exclude_unset", self.exclude_unset)?;
652656
d.set_item("exclude_defaults", self.exclude_defaults)?;
653657
d.set_item("exclude_none", self.exclude_none)?;
658+
d.set_item("exclude_computed_fields", self.exclude_computed_fields)?;
654659
d.set_item("round_trip", self.round_trip)?;
655660
d.set_item("serialize_as_any", self.serialize_as_any)?;
656661
Ok(d)
657662
}
658663

659664
fn __repr__(&self, py: Python) -> PyResult<String> {
660665
Ok(format!(
661-
"SerializationInfo(include={}, exclude={}, context={}, mode='{}', by_alias={}, exclude_unset={}, exclude_defaults={}, exclude_none={}, round_trip={}, serialize_as_any={})",
666+
"SerializationInfo(include={}, exclude={}, context={}, mode='{}', by_alias={}, exclude_unset={}, exclude_defaults={}, exclude_none={}, exclude_computed_fields={}, round_trip={}, serialize_as_any={})",
662667
match self.include {
663668
Some(ref include) => include.bind(py).repr()?.to_str()?.to_owned(),
664669
None => "None".to_owned(),
@@ -676,6 +681,7 @@ impl SerializationInfo {
676681
py_bool(self.exclude_unset),
677682
py_bool(self.exclude_defaults),
678683
py_bool(self.exclude_none),
684+
py_bool(self.exclude_computed_fields),
679685
py_bool(self.round_trip),
680686
py_bool(self.serialize_as_any),
681687
))

tests/serializers/test_functions.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def double(value, info):
7272
'exclude_unset': False,
7373
'exclude_defaults': False,
7474
'exclude_none': False,
75+
'exclude_computed_fields': False,
7576
'round_trip': False,
7677
'serialize_as_any': False,
7778
}
@@ -85,6 +86,7 @@ def double(value, info):
8586
'exclude_unset': False,
8687
'exclude_defaults': False,
8788
'exclude_none': False,
89+
'exclude_computed_fields': False,
8890
'round_trip': False,
8991
'serialize_as_any': False,
9092
}
@@ -97,6 +99,7 @@ def double(value, info):
9799
'exclude_unset': False,
98100
'exclude_defaults': False,
99101
'exclude_none': False,
102+
'exclude_computed_fields': False,
100103
'round_trip': False,
101104
'serialize_as_any': False,
102105
}
@@ -109,6 +112,7 @@ def double(value, info):
109112
'exclude_unset': True,
110113
'exclude_defaults': False,
111114
'exclude_none': False,
115+
'exclude_computed_fields': False,
112116
'round_trip': False,
113117
'serialize_as_any': False,
114118
}
@@ -123,6 +127,7 @@ def double(value, info):
123127
'exclude_unset': False,
124128
'exclude_defaults': False,
125129
'exclude_none': False,
130+
'exclude_computed_fields': False,
126131
'round_trip': False,
127132
'serialize_as_any': False,
128133
}
@@ -136,6 +141,7 @@ def double(value, info):
136141
'exclude_unset': False,
137142
'exclude_defaults': False,
138143
'exclude_none': False,
144+
'exclude_computed_fields': False,
139145
'round_trip': False,
140146
'serialize_as_any': False,
141147
}
@@ -231,27 +237,27 @@ def append_args(value, info):
231237
)
232238
assert s.to_python(123) == (
233239
"123 info=SerializationInfo(include=None, exclude=None, context=None, mode='python', by_alias=False, exclude_unset=False, "
234-
'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)'
240+
'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)'
235241
)
236242
assert s.to_python(123, mode='other') == (
237243
"123 info=SerializationInfo(include=None, exclude=None, context=None, mode='other', by_alias=False, exclude_unset=False, "
238-
'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)'
244+
'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)'
239245
)
240246
assert s.to_python(123, include={'x'}) == (
241247
"123 info=SerializationInfo(include={'x'}, exclude=None, context=None, mode='python', by_alias=False, exclude_unset=False, "
242-
'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)'
248+
'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)'
243249
)
244250
assert s.to_python(123, context='context') == (
245251
"123 info=SerializationInfo(include=None, exclude=None, context='context', mode='python', by_alias=False, exclude_unset=False, "
246-
'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)'
252+
'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)'
247253
)
248254
assert s.to_python(123, mode='json', exclude={1: {2}}) == (
249255
"123 info=SerializationInfo(include=None, exclude={1: {2}}, context=None, mode='json', by_alias=False, exclude_unset=False, "
250-
'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)'
256+
'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)'
251257
)
252258
assert s.to_json(123) == (
253259
b"\"123 info=SerializationInfo(include=None, exclude=None, context=None, mode='json', by_alias=False, exclude_unset=False, "
254-
b'exclude_defaults=False, exclude_none=False, round_trip=False, serialize_as_any=False)"'
260+
b'exclude_defaults=False, exclude_none=False, exclude_computed_fields=False, round_trip=False, serialize_as_any=False)"'
255261
)
256262

257263

tests/serializers/test_model.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@
22
import json
33
import platform
44
import warnings
5+
from functools import cached_property
56
from random import randint
67
from typing import Any, ClassVar
78

8-
try:
9-
from functools import cached_property
10-
except ImportError:
11-
cached_property = None
12-
139
import pytest
1410
from dirty_equals import IsJson
1511

@@ -504,7 +500,6 @@ def ser_x(self, v: Any, _) -> str:
504500
assert s.to_python(Model(x=1000)) == {'x': '1_000'}
505501

506502

507-
@pytest.mark.skipif(cached_property is None, reason='cached_property is not available')
508503
def test_field_serializer_cached_property():
509504
@dataclasses.dataclass
510505
class Model:
@@ -709,6 +704,9 @@ def area(self) -> bytes:
709704
assert s.to_python(Model(width=3, height=4), round_trip=True) == {'width': 3, 'height': 4}
710705
assert s.to_json(Model(width=3, height=4), round_trip=True) == b'{"width":3,"height":4}'
711706

707+
assert s.to_python(Model(width=3, height=4), exclude_computed_fields=True) == {'width': 3, 'height': 4}
708+
assert s.to_json(Model(width=3, height=4), exclude_computed_fields=True) == b'{"width":3,"height":4}'
709+
712710

713711
def test_property_alias():
714712
@dataclasses.dataclass
@@ -881,7 +879,6 @@ def area(self) -> int:
881879
assert s.to_json(Model(3, 4), exclude_none=True, by_alias=True) == b'{"width":3,"height":4,"Area":12}'
882880

883881

884-
@pytest.mark.skipif(cached_property is None, reason='cached_property is not available')
885882
def test_cached_property_alias():
886883
@dataclasses.dataclass
887884
class Model:
@@ -996,7 +993,6 @@ def b(self):
996993
assert s.to_json(Model(1), exclude={'b': [0]}) == b'{"a":1,"b":[2,"3"]}'
997994

998995

999-
@pytest.mark.skipif(cached_property is None, reason='cached_property is not available')
1000996
def test_property_setter():
1001997
class Square:
1002998
side: float

0 commit comments

Comments
 (0)