Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,58 @@

## Summary

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

## Upgrading

- `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.

### Marshmallow 4.x required

The `frequenz.quantities.experimental.marshmallow` module now requires marshmallow 4.x. Marshmallow 3.x is no longer supported.

### Marshmallow module API changes

The `frequenz.quantities.experimental.marshmallow` module has been updated for marshmallow 4.x. The old API is still supported but deprecated and will emit `DeprecationWarning`.

#### `QuantitySchema` constructor parameter `serialize_as_string_default` is deprecated

Old API (deprecated, still works):
```python
from marshmallow_dataclass import class_schema
from frequenz.quantities.experimental.marshmallow import QuantitySchema

schema = class_schema(Config, base_schema=QuantitySchema)(
serialize_as_string_default=True
)
result = schema.dump(config_obj)
```

New API (recommended):
```python
from marshmallow_dataclass import class_schema
from frequenz.quantities.experimental.marshmallow import (
QuantitySchema,
serialize_as_string_default,
)

serialize_as_string_default.set(True)
schema = class_schema(Config, base_schema=QuantitySchema)()
result = schema.dump(config_obj)
serialize_as_string_default.set(False) # Reset if needed
```

### Why this change was necessary

Marshmallow 4.0.0 introduced breaking changes including:
- `Field` is now a generic type (`Field[T]`)
- Changes to how schema context is accessed from fields

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.

## New Features

<!-- Here goes the main new features and examples or instructions on how to use them -->
- Support for marshmallow 4.x (dropping 3.x support)

## Bug Fixes

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ dev-pytest = [
]

marshmallow = [
"marshmallow >= 3.0.0, < 5",
"marshmallow >= 4.0.0, < 5",
"marshmallow-dataclass >= 8.0.0, < 9",
]

Expand Down
29 changes: 29 additions & 0 deletions src/frequenz/quantities/experimental/marshmallow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
even in minor or patch releases.
"""

import warnings
from contextvars import ContextVar
from typing import Any, Type

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

# Backwards compatibility: also check self.metadata for serialize_as_string
# (v1.0.0 API read it from self.metadata directly)
if self.serialize_as_string_override is None:
if "serialize_as_string" in self.metadata:
self.serialize_as_string_override = self.metadata["serialize_as_string"]

def _serialize(
self, value: Quantity | None, attr: str | None, obj: Any, **kwargs: Any
) -> Any:
Expand Down Expand Up @@ -292,3 +299,25 @@ class Config:
"""

TYPE_MAPPING: dict[type, type[Field[Any]]] = QUANTITY_FIELD_CLASSES

def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the schema.

Args:
*args: Positional arguments passed to the parent Schema.
**kwargs: Keyword arguments passed to the parent Schema.
The deprecated `serialize_as_string_default` parameter is accepted
for backwards compatibility but will emit a DeprecationWarning.
"""
# Extract deprecated parameter before passing to parent
serialize_as_string_value = kwargs.pop("serialize_as_string_default", None)
super().__init__(*args, **kwargs)
if serialize_as_string_value is not None:
warnings.warn(
"Passing 'serialize_as_string_default' to QuantitySchema constructor is "
"deprecated. Use the 'serialize_as_string_default' context variable instead: "
"serialize_as_string_default.set(True)",
DeprecationWarning,
stacklevel=2,
)
serialize_as_string_default.set(serialize_as_string_value)
50 changes: 49 additions & 1 deletion tests/experimental/test_marshmallow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

"""Test marshmallow fields and schema."""


from dataclasses import dataclass, field
from typing import Any, Self, cast

import pytest
from marshmallow_dataclass import class_schema

from frequenz.quantities import (
Expand Down Expand Up @@ -246,3 +246,51 @@ def test_config_schema_dump_default_string() -> None:
"voltage_always_string": "250 kV",
"temp_never_string": 10.0,
}


def test_deprecated_constructor_api() -> None:
"""Test that the deprecated constructor API still works but emits a warning."""

@dataclass
class _SimpleConfig:
"""Test config dataclass."""

pct: Percentage = field(default_factory=lambda: Percentage.from_percent(75.0))

schema_cls = class_schema(_SimpleConfig, base_schema=QuantitySchema)

with pytest.warns(
DeprecationWarning,
match="Passing 'serialize_as_string_default' to QuantitySchema constructor is deprecated",
):
schema = schema_cls(serialize_as_string_default=True) # type: ignore[call-arg]

try:
# Verify it still works
result = schema.dump(_SimpleConfig())
assert result["pct"] == "75 %"
finally:
# Reset the context variable to avoid affecting other tests
serialize_as_string_default.set(False)


def test_deprecated_constructor_api_false() -> None:
"""Test that the deprecated constructor API works with False value."""

@dataclass
class _SimpleConfig:
"""Test config dataclass."""

pct: Percentage = field(default_factory=lambda: Percentage.from_percent(75.0))

schema_cls = class_schema(_SimpleConfig, base_schema=QuantitySchema)

with pytest.warns(
DeprecationWarning,
match="Passing 'serialize_as_string_default' to QuantitySchema constructor is deprecated",
):
schema = schema_cls(serialize_as_string_default=False) # type: ignore[call-arg]

# Verify it still works (no need to reset since we're setting to False which is the default)
result = schema.dump(_SimpleConfig())
assert result["pct"] == 75.0