Skip to content
Merged
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,15 @@ Define enums with deprecated members that raise deprecation warnings when
accessed:

```python
from frequenz.core.enum import Enum, DeprecatedMember
from frequenz.core.enum import Enum, deprecated_member, unique

@unique
class TaskStatus(Enum):
OPEN = 1
IN_PROGRESS = 2
PENDING = DeprecatedMember(1, "PENDING is deprecated, use OPEN instead")
DONE = DeprecatedMember(3, "DONE is deprecated, use FINISHED instead")
# Duplicate values are fine with `@unique` as long as they are deprecated
PENDING = deprecated_member(1, "PENDING is deprecated, use OPEN instead")
DONE = deprecated_member(3, "DONE is deprecated, use FINISHED instead")
FINISHED = 4

status1 = TaskStatus.PENDING # Warns: "PENDING is deprecated, use OPEN instead"
Expand Down
12 changes: 2 additions & 10 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
# Frequenz Core Library Release Notes

## Summary

<!-- Here goes a general summary of what this release is about -->

## Upgrading

<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
- If you used `enum.DeprecatedMember` directly anywhere, you should probably switch to using `enum.deprecated_member` instead, which will tag the member value with the appropriate type.

## New Features

<!-- Here goes the main new features and examples or instructions on how to use them -->

## Bug Fixes

<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
- A new `enum.deprecated_member` function has been added to create deprecated enum members with proper typing.
41 changes: 32 additions & 9 deletions src/frequenz/core/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,19 @@
"""Type variable for enum types."""


ValueT = TypeVar("ValueT")
"""Type variable for enum member values."""


class DeprecatedMemberWarning(DeprecationWarning):
"""Warning category for deprecated enum members."""


class DeprecatedMember:
"""Marker used in enum class bodies to declare deprecated members.
"""Class to mark members as deprecated.

This class should not be used directly, use
[`deprecated_member`][frequenz.core.enum.deprecated_member] instead.

Please read the [`Enum`][frequenz.core.enum.Enum] documentation for details and
examples.
Expand All @@ -48,8 +55,24 @@ def __init__(self, value: Any, message: str) -> None:
self.message = message


def deprecated_member(value: ValueT, message: str) -> ValueT:
"""Mark an enum member as deprecated.

Please read the [`Enum`][frequenz.core.enum.Enum] documentation for details and
examples.

Args:
value: The value of the enum member to mark as deprecated.
message: The deprecation message to be shown when the member is accessed.

Returns:
The wrapped value, to mark the enum member as deprecated.
"""
return cast(ValueT, DeprecatedMember(value, message))


Copy link
Contributor

@Marenz Marenz Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So will you deprecate direct usage of DeprecatedMember then? 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it. But I think it is too meta 😆

But when we go for 2.0.0 I think I would make it private, we really don't need to expose it at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh, so nice to have gotten issue number 100 for:

class DeprecatingEnumType(enum.EnumType):
"""Enum metaclass that supports `DeprecatedMember` wrappers.
"""Enum metaclass that supports deprecated members.

Tip:
Normally it is not necessary to use this class directly, use
Expand Down Expand Up @@ -163,29 +186,29 @@ class Enum(enum.Enum): # noqa
else:

class Enum(enum.Enum, metaclass=DeprecatingEnumType):
"""Base class for enums that support DeprecatedMember.
"""Base class for enums that support deprecated members.

This class extends the standard library's [`enum.Enum`][] to support marking
certain members as deprecated. Deprecated members can be accessed, but doing so
will emit a [`DeprecationWarning`][], specifically
a [`DeprecatedMemberWarning`][frequenz.core.enum.DeprecatedMemberWarning].

To declare a deprecated member, use the
[`DeprecatedMember`][frequenz.core.enum.DeprecatedMember] wrapper in the class body.
[`deprecated_member()`][frequenz.core.enum.deprecated_member] function.

When using the enum constructor (i.e. `MyEnum(value)`), a warning is only emitted if
the resolved member has no non-deprecated aliases. If there is at least one
non-deprecated alias for the member, no warning is emitted.

Example:
```python
from frequenz.core.enum import Enum, DeprecatedMember
from frequenz.core.enum import Enum, deprecated_member

class TaskStatus(Enum):
OPEN = 1
IN_PROGRESS = 2
PENDING = DeprecatedMember(1, "PENDING is deprecated, use OPEN instead")
DONE = DeprecatedMember(3, "DONE is deprecated, use FINISHED instead")
PENDING = deprecated_member(1, "PENDING is deprecated, use OPEN instead")
DONE = deprecated_member(3, "DONE is deprecated, use FINISHED instead")
FINISHED = 4

# Accessing deprecated members:
Expand Down Expand Up @@ -219,14 +242,14 @@ def unique(enumeration: type[EnumT]) -> type[EnumT]:

Example:
```python
from frequenz.core.enum import Enum, DeprecatedMember, unique
from frequenz.core.enum import Enum, deprecated_member, unique

@unique
class TaskStatus(Enum):
OPEN = 1
IN_PROGRESS = 2
# This is okay, as PENDING is a deprecated alias.
PENDING = DeprecatedMember(1, "Use OPEN instead")
PENDING = deprecated_member(1, "Use OPEN instead")
```

Args:
Expand Down
63 changes: 57 additions & 6 deletions tests/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@

import pytest

from frequenz.core.enum import DeprecatedMember, DeprecatedMemberWarning, Enum, unique
from frequenz.core.enum import (
DeprecatedMember,
DeprecatedMemberWarning,
Enum,
deprecated_member,
unique,
)


class _TestEnum(Enum):
"""A test enum with some deprecated members."""

OPEN = 1
IN_PROGRESS = 2
PENDING = DeprecatedMember(1, "Use OPEN instead")
DONE = DeprecatedMember(3, "Use FINISHED instead")
PENDING = deprecated_member(1, "Use OPEN instead")
DONE = deprecated_member(3, "Use FINISHED instead")
FINISHED = 4


Expand All @@ -37,6 +43,51 @@ def test_mypy_detects_deprecated_members() -> None:
_ = _TestEnum.I_DONT_EXIST # type: ignore[attr-defined]


def test_mypy_deprecated_member_correct_value_type() -> None:
"""Test that mypy sees the correct value type for deprecated members.
If mypy wouldn't detect this, it should complain about value not having the
correct type.
"""
_: int = _TestEnum.PENDING.value


def test_deprecated_member_helper_returns_wrapper() -> None:
"""Test the helper returns a DeprecatedMember wrapper preserving metadata."""
wrapper = deprecated_member(123, "Value 123 is deprecated")
assert isinstance(wrapper, DeprecatedMember)
assert wrapper.value == 123
assert wrapper.message == "Value 123 is deprecated"


def test_deprecated_member_helper_enum_usage() -> None:
"""Test that enums using the helper behave like those using DeprecatedMember directly."""

class _HelperEnum(Enum):
"""Enum that uses deprecated_member for deprecated aliases."""

ACTIVE = 1
LEGACY_ACTIVE = deprecated_member(1, "Use ACTIVE instead")
LEGACY_ONLY = deprecated_member(2, "Use something else")

with pytest.deprecated_call() as recorder:
_ = _HelperEnum.LEGACY_ACTIVE
_assert_deprecated_member(recorder, "Use ACTIVE instead")

with pytest.deprecated_call() as recorder:
_ = _HelperEnum["LEGACY_ACTIVE"]
_assert_deprecated_member(recorder, "Use ACTIVE instead")

assert _HelperEnum(1) is _HelperEnum.ACTIVE

with pytest.deprecated_call() as recorder:
member = _HelperEnum(2)
_assert_deprecated_member(recorder, "Use something else")

with pytest.deprecated_call():
assert member is _HelperEnum.LEGACY_ONLY


def test_attribute_access_warns() -> None:
"""Test accessing deprecated members as attributes triggers a deprecation warning."""
with pytest.deprecated_call() as recorder:
Expand Down Expand Up @@ -89,7 +140,7 @@ class _Status(Enum):

ACTIVE = 1
INACTIVE = 2
PENDING = DeprecatedMember(1, "Use ACTIVE instead")
PENDING = deprecated_member(1, "Use ACTIVE instead")

with pytest.deprecated_call():
assert _Status.PENDING is _Status.ACTIVE # type: ignore[comparison-overlap]
Expand Down Expand Up @@ -163,8 +214,8 @@ def test_unique_decorator_success_all_deprecated() -> None:
class _AllDeprecated(Enum):
"""An enum where all members are deprecated."""

OLD_A = DeprecatedMember(1, "Use something else")
OLD_B = DeprecatedMember(1, "Also use something else")
OLD_A = deprecated_member(1, "Use something else")
OLD_B = deprecated_member(1, "Also use something else")

with pytest.deprecated_call():
assert _AllDeprecated.OLD_A is _AllDeprecated.OLD_B # type: ignore[comparison-overlap]