14
14
even in minor or patch releases.
15
15
"""
16
16
17
+ from contextvars import ContextVar
17
18
from typing import Any , Type
18
19
19
- from marshmallow import Schema , ValidationError , fields
20
+ from marshmallow import Schema , ValidationError
21
+ from marshmallow .fields import Field
20
22
21
23
from .._apparent_power import ApparentPower
22
24
from .._current import Current
29
31
from .._temperature import Temperature
30
32
from .._voltage import Voltage
31
33
34
+ serialize_as_string_default : ContextVar [bool ] = ContextVar (
35
+ "serialize_as_string_default" , default = False
36
+ )
37
+ """Context variable to control the default serialization format for quantities.
32
38
33
- class _QuantityField (fields .Field ):
39
+ If True, quantities are serialized as strings with units.
40
+ If False, quantities are serialized as floats.
41
+
42
+ This can be overridden on a per-field basis using the `serialize_as_string`
43
+ metadata attribute.
44
+ """
45
+
46
+
47
+ class _QuantityField (Field [Quantity ]):
34
48
"""Custom field for Quantity objects supporting per-field serialization configuration.
35
49
36
50
This class handles serialization and deserialization of ALL Quantity
@@ -57,24 +71,34 @@ class _QuantityField(fields.Field):
57
71
field_type : Type [Quantity ] | None = None
58
72
"""The specific Quantity subclass."""
59
73
74
+ def __init__ (self , * args : Any , ** kwargs : Any ) -> None :
75
+ """Initialize the field."""
76
+ self .serialize_as_string_override = kwargs .pop ("serialize_as_string" , None )
77
+ super ().__init__ (* args , ** kwargs )
78
+
60
79
def _serialize (
61
- self , value : Quantity , attr : str | None , obj : Any , ** kwargs : Any
80
+ self , value : Quantity | None , attr : str | None , obj : Any , ** kwargs : Any
62
81
) -> Any :
63
82
"""Serialize the Quantity object based on per-field configuration."""
64
83
if self .field_type is None or not issubclass (self .field_type , Quantity ):
65
84
raise TypeError (
66
85
"field_type must be set to a Quantity subclass in the subclass."
67
86
)
87
+ if value is None :
88
+ return None
68
89
69
- assert self .parent is not None
90
+ if not isinstance (value , Quantity ):
91
+ raise TypeError (
92
+ f"Expected a Quantity object, but got { type (value ).__name__ } ."
93
+ )
70
94
71
95
# Determine the serialization format
72
- default = (
73
- False
74
- if self .parent .context is None
75
- else self .parent .context .get ("serialize_as_string_default" , False )
96
+ default = serialize_as_string_default .get ()
97
+ serialize_as_string = (
98
+ self .serialize_as_string_override
99
+ if self .serialize_as_string_override is not None
100
+ else default
76
101
)
77
- serialize_as_string = self .metadata .get ("serialize_as_string" , default )
78
102
79
103
if serialize_as_string :
80
104
# Use the Quantity's native string representation (includes unit)
@@ -177,7 +201,7 @@ class VoltageField(_QuantityField):
177
201
field_type = Voltage
178
202
179
203
180
- QUANTITY_FIELD_CLASSES : dict [type [Quantity ], type [fields . Field ]] = {
204
+ QUANTITY_FIELD_CLASSES : dict [type [Quantity ], type [Field [ Any ] ]] = {
181
205
ApparentPower : ApparentPowerField ,
182
206
Current : CurrentField ,
183
207
Energy : EnergyField ,
@@ -208,8 +232,10 @@ class QuantitySchema(Schema):
208
232
from marshmallow_dataclass import class_schema
209
233
from marshmallow.validate import Range
210
234
from frequenz.quantities import Percentage
211
- from frequenz.quantities.experimental.marshmallow import QuantitySchema
212
- from typing import cast
235
+ from frequenz.quantities.experimental.marshmallow import (
236
+ QuantitySchema,
237
+ serialize_as_string_default,
238
+ )
213
239
214
240
@dataclass
215
241
class Config:
@@ -245,29 +271,24 @@ class Config:
245
271
},
246
272
)
247
273
248
- @classmethod
249
- def load(cls, config: dict[str, Any]) -> "Config":
250
- schema = class_schema(cls, base_schema=QuantitySchema)(
251
- serialize_as_string_default=True # type: ignore[call-arg]
252
- )
253
- return cast(Config, schema.load(config))
274
+ config_obj = Config()
275
+ Schema = class_schema(Config, base_schema=QuantitySchema)
276
+ schema = Schema()
277
+
278
+ # Default serialization (as float)
279
+ result = schema.dump(config_obj)
280
+ assert result["percentage_serialized_as_schema_default"] == 25.0
281
+
282
+ # Override default serialization to string
283
+ serialize_as_string_default.set(True)
284
+ result = schema.dump(config_obj)
285
+ assert result["percentage_serialized_as_schema_default"] == "25.0 %"
286
+ serialize_as_string_default.set(False) # Reset context
287
+
288
+ # Per-field configuration always takes precedence
289
+ assert result["percentage_always_as_string"] == "25.0 %"
290
+ assert result["percentage_always_as_float"] == 25.0
254
291
```
255
292
"""
256
293
257
- TYPE_MAPPING : dict [type [Quantity ], type [fields .Field ]] = QUANTITY_FIELD_CLASSES
258
-
259
- def __init__ (
260
- self , * args : Any , serialize_as_string_default : bool = False , ** kwargs : Any
261
- ) -> None :
262
- """
263
- Initialize the schema with a default serialization format.
264
-
265
- Args:
266
- *args: Additional positional arguments.
267
- serialize_as_string_default: Default serialization format for quantities.
268
- If True, quantities are serialized as strings with units.
269
- If False, quantities are serialized as floats.
270
- **kwargs: Additional keyword arguments.
271
- """
272
- super ().__init__ (* args , ** kwargs )
273
- self .context ["serialize_as_string_default" ] = serialize_as_string_default
294
+ TYPE_MAPPING : dict [type , type [Field [Any ]]] = QUANTITY_FIELD_CLASSES
0 commit comments