Skip to content

Commit bb481ba

Browse files
committed
Add backwards-compatible marshmallow 4.x support
- Add deprecation warning for QuantitySchema(serialize_as_string_default=...) constructor parameter, use serialize_as_string_default context variable instead - Maintain backwards compatibility with old metadata API - Update RELEASE_NOTES.md with upgrade guide Signed-off-by: Mathias L. Baumann <mathias.baumann@frequenz.com>
1 parent cc89bbf commit bb481ba

File tree

3 files changed

+115
-4
lines changed

3 files changed

+115
-4
lines changed

RELEASE_NOTES.md

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,52 @@
22

33
## Summary
44

5-
<!-- Here goes a general summary of what this release is about -->
5+
This release adds support for marshmallow 4.x. The old API is still supported but deprecated.
66

77
## Upgrading
88

9-
- `typing-extensions` minimal version was bumped to 4.6.0 to be compatible with Python3.12. You might need to upgrade it in your project too.
9+
### Marshmallow module API changes
10+
11+
The `frequenz.quantities.experimental.marshmallow` module has been updated to support marshmallow 4.x. The old API is still supported but deprecated and will emit `DeprecationWarning`.
12+
13+
#### `QuantitySchema` constructor parameter `serialize_as_string_default` is deprecated
14+
15+
Old API (deprecated, still works):
16+
```python
17+
from marshmallow_dataclass import class_schema
18+
from frequenz.quantities.experimental.marshmallow import QuantitySchema
19+
20+
schema = class_schema(Config, base_schema=QuantitySchema)(
21+
serialize_as_string_default=True
22+
)
23+
result = schema.dump(config_obj)
24+
```
25+
26+
New API (recommended):
27+
```python
28+
from marshmallow_dataclass import class_schema
29+
from frequenz.quantities.experimental.marshmallow import (
30+
QuantitySchema,
31+
serialize_as_string_default,
32+
)
33+
34+
serialize_as_string_default.set(True)
35+
schema = class_schema(Config, base_schema=QuantitySchema)()
36+
result = schema.dump(config_obj)
37+
serialize_as_string_default.set(False) # Reset if needed
38+
```
39+
40+
### Why this change was necessary
41+
42+
Marshmallow 4.0.0 introduced breaking changes including:
43+
- `Field` is now a generic type (`Field[T]`)
44+
- Changes to how schema context is accessed from fields
45+
46+
The previous implementation relied on `self.parent.context` to access the `serialize_as_string_default` setting, which no longer works reliably with marshmallow 4.x. The new implementation uses Python's `contextvars.ContextVar` instead, which is a cleaner and more explicit approach.
1047

1148
## New Features
1249

13-
<!-- Here goes the main new features and examples or instructions on how to use them -->
50+
- Support for marshmallow 4.x (in addition to 3.x)
1451

1552
## Bug Fixes
1653

src/frequenz/quantities/experimental/marshmallow.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
even in minor or patch releases.
1515
"""
1616

17+
import warnings
1718
from contextvars import ContextVar
1819
from typing import Any, Type
1920

@@ -76,6 +77,12 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
7677
self.serialize_as_string_override = kwargs.pop("serialize_as_string", None)
7778
super().__init__(*args, **kwargs)
7879

80+
# Backwards compatibility: also check self.metadata for serialize_as_string
81+
# (v1.0.0 API read it from self.metadata directly)
82+
if self.serialize_as_string_override is None:
83+
if "serialize_as_string" in self.metadata:
84+
self.serialize_as_string_override = self.metadata["serialize_as_string"]
85+
7986
def _serialize(
8087
self, value: Quantity | None, attr: str | None, obj: Any, **kwargs: Any
8188
) -> Any:
@@ -292,3 +299,29 @@ class Config:
292299
"""
293300

294301
TYPE_MAPPING: dict[type, type[Field[Any]]] = QUANTITY_FIELD_CLASSES
302+
303+
def __init__(
304+
self, *args: Any, serialize_as_string_default: bool | None = None, **kwargs: Any
305+
) -> None:
306+
"""Initialize the schema.
307+
308+
Args:
309+
*args: Positional arguments passed to the parent Schema.
310+
serialize_as_string_default: Deprecated. Use the
311+
`serialize_as_string_default` context variable instead.
312+
If provided, sets the context variable for backwards compatibility.
313+
**kwargs: Keyword arguments passed to the parent Schema.
314+
"""
315+
super().__init__(*args, **kwargs)
316+
if serialize_as_string_default is not None:
317+
warnings.warn(
318+
"Passing 'serialize_as_string_default' to QuantitySchema constructor is "
319+
"deprecated. Use the 'serialize_as_string_default' context variable instead: "
320+
"serialize_as_string_default.set(True)",
321+
DeprecationWarning,
322+
stacklevel=2,
323+
)
324+
# Import the module-level context variable
325+
from . import marshmallow as _module
326+
327+
_module.serialize_as_string_default.set(serialize_as_string_default)

tests/experimental/test_marshmallow.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
"""Test marshmallow fields and schema."""
55

6-
6+
import warnings
77
from dataclasses import dataclass, field
88
from typing import Any, Self, cast
99

10+
import pytest
1011
from marshmallow_dataclass import class_schema
1112

1213
from frequenz.quantities import (
@@ -246,3 +247,43 @@ def test_config_schema_dump_default_string() -> None:
246247
"voltage_always_string": "250 kV",
247248
"temp_never_string": 10.0,
248249
}
250+
251+
252+
def test_deprecated_constructor_api() -> None:
253+
"""Test that the deprecated constructor API still works but emits a warning."""
254+
255+
@dataclass
256+
class SimpleConfig:
257+
pct: Percentage = field(default_factory=lambda: Percentage.from_percent(75.0))
258+
259+
Schema = class_schema(SimpleConfig, base_schema=QuantitySchema)
260+
261+
with pytest.warns(
262+
DeprecationWarning,
263+
match="Passing 'serialize_as_string_default' to QuantitySchema constructor is deprecated",
264+
):
265+
schema = Schema(serialize_as_string_default=True) # type: ignore[call-arg]
266+
267+
# Verify it still works
268+
result = schema.dump(SimpleConfig())
269+
assert result["pct"] == "75 %" # type: ignore[index]
270+
271+
272+
def test_deprecated_constructor_api_false() -> None:
273+
"""Test that the deprecated constructor API works with False value."""
274+
275+
@dataclass
276+
class SimpleConfig:
277+
pct: Percentage = field(default_factory=lambda: Percentage.from_percent(75.0))
278+
279+
Schema = class_schema(SimpleConfig, base_schema=QuantitySchema)
280+
281+
with pytest.warns(
282+
DeprecationWarning,
283+
match="Passing 'serialize_as_string_default' to QuantitySchema constructor is deprecated",
284+
):
285+
schema = Schema(serialize_as_string_default=False) # type: ignore[call-arg]
286+
287+
# Verify it still works
288+
result = schema.dump(SimpleConfig())
289+
assert result["pct"] == 75.0 # type: ignore[index]

0 commit comments

Comments
 (0)