Skip to content

enum

frequenz.core.enum ¤

Enum utilities with support for deprecated members.

This module provides an Enum base class that extends the standard library's enum.Enum to support marking certain members as deprecated.

See the class documentation for details and examples.

Attributes¤

frequenz.core.enum.EnumT module-attribute ¤

EnumT = TypeVar('EnumT', bound=enum.Enum)

Type variable for enum types.

frequenz.core.enum.ValueT module-attribute ¤

ValueT = TypeVar('ValueT')

Type variable for enum member values.

Classes¤

frequenz.core.enum.DeprecatedMember ¤

Class to mark members as deprecated.

This class should not be used directly, use deprecated_member instead.

Please read the Enum documentation for details and examples.

Source code in src/frequenz/core/enum.py
class DeprecatedMember:
    """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.
    """

    # Using slots is just an optimization to make the class more lightweight and avoid
    # the creation of a `__dict__` for each instance and its corresponding lookup.
    __slots__ = ("value", "message", "new_name", "since")

    # One overload with `message` and one with only `new_name`, so a call with
    # neither matches none of them.
    @overload
    def __init__(  # noqa: D107
        self,
        value: Any,
        message: str,
        *,
        new_name: str | None = None,
        since: str | None = None,
    ) -> None: ...

    @overload
    def __init__(  # noqa: D107
        self,
        value: Any,
        message: None = None,
        *,
        new_name: str,
        since: str | None = None,
    ) -> None: ...

    def __init__(  # noqa: DOC502
        self,
        value: Any,
        message: str | None = None,
        *,
        new_name: str | None = None,
        since: str | None = None,
    ) -> None:
        """Initialize this instance.

        At least one of `message` and `new_name` must be given.

        Args:
            value: The value of the enum member to mark as deprecated.
            message: The warning message to emit when the member is reached, or
                `None` to generate it from `new_name`.
            new_name: The name of the member to use instead, in the same enum.
            since: The version the member is deprecated since, such as `"v1.2.0"`,
                for the documentation only.

        Raises:
            TypeError: If neither `message` nor `new_name` is given, or if
                `new_name` or `since` is not a string.
            ValueError: If `new_name` is not an identifier, or if `since` is empty.
        """
        self.value: Any = value
        """The value of the deprecated member."""

        self.message: str | None = message
        """The warning message, or `None` if it is generated from `new_name`.

        A generated message is only known once the enum class is created, since it
        includes the class' fully qualified name, and is not stored here.
        """

        self.new_name: str | None = new_name
        """The name of the member to use instead, in the same enum, if given."""

        self.since: str | None = since
        """The version the member is deprecated since, if given.

        It is only used for the documentation, never in the warning.
        """

        self._validate()

    def _validate(self) -> None:
        """Check the fields.

        `message` is not checked, as it never was before `new_name` and `since`
        were added, so existing code passing something else keeps working.

        Raises:
            TypeError: If neither `message` nor `new_name` is given, or if
                `new_name` or `since` is not a string.
            ValueError: If `new_name` is not an identifier, or if `since` is empty.
        """
        if self.message is None and self.new_name is None:
            raise TypeError(
                "a deprecated member needs message, new_name or both, got neither"
            )
        if self.new_name is not None:
            if not isinstance(self.new_name, str):
                raise TypeError(f"new_name must be a str, got {self.new_name!r}")
            if not self.new_name.isidentifier():
                raise ValueError(
                    f"new_name must be an identifier, got {self.new_name!r}"
                )
        if self.since is not None:
            if not isinstance(self.since, str):
                raise TypeError(f"since must be a str, got {self.since!r}")
            if not self.since.strip():
                raise ValueError("since must not be empty")
Attributes¤
message instance-attribute ¤
message: str | None = message

The warning message, or None if it is generated from new_name.

A generated message is only known once the enum class is created, since it includes the class' fully qualified name, and is not stored here.

new_name instance-attribute ¤
new_name: str | None = new_name

The name of the member to use instead, in the same enum, if given.

since instance-attribute ¤
since: str | None = since

The version the member is deprecated since, if given.

It is only used for the documentation, never in the warning.

value instance-attribute ¤
value: Any = value

The value of the deprecated member.

Methods:¤
__init__ ¤
__init__(
    value: Any,
    message: str,
    *,
    new_name: str | None = None,
    since: str | None = None
) -> None
__init__(
    value: Any,
    message: None = None,
    *,
    new_name: str,
    since: str | None = None
) -> None
__init__(
    value: Any,
    message: str | None = None,
    *,
    new_name: str | None = None,
    since: str | None = None
) -> None

Initialize this instance.

At least one of message and new_name must be given.

PARAMETER DESCRIPTION
value

The value of the enum member to mark as deprecated.

TYPE: Any

message

The warning message to emit when the member is reached, or None to generate it from new_name.

TYPE: str | None DEFAULT: None

new_name

The name of the member to use instead, in the same enum.

TYPE: str | None DEFAULT: None

since

The version the member is deprecated since, such as "v1.2.0", for the documentation only.

TYPE: str | None DEFAULT: None

RAISES DESCRIPTION
TypeError

If neither message nor new_name is given, or if new_name or since is not a string.

ValueError

If new_name is not an identifier, or if since is empty.

Source code in src/frequenz/core/enum.py
def __init__(  # noqa: DOC502
    self,
    value: Any,
    message: str | None = None,
    *,
    new_name: str | None = None,
    since: str | None = None,
) -> None:
    """Initialize this instance.

    At least one of `message` and `new_name` must be given.

    Args:
        value: The value of the enum member to mark as deprecated.
        message: The warning message to emit when the member is reached, or
            `None` to generate it from `new_name`.
        new_name: The name of the member to use instead, in the same enum.
        since: The version the member is deprecated since, such as `"v1.2.0"`,
            for the documentation only.

    Raises:
        TypeError: If neither `message` nor `new_name` is given, or if
            `new_name` or `since` is not a string.
        ValueError: If `new_name` is not an identifier, or if `since` is empty.
    """
    self.value: Any = value
    """The value of the deprecated member."""

    self.message: str | None = message
    """The warning message, or `None` if it is generated from `new_name`.

    A generated message is only known once the enum class is created, since it
    includes the class' fully qualified name, and is not stored here.
    """

    self.new_name: str | None = new_name
    """The name of the member to use instead, in the same enum, if given."""

    self.since: str | None = since
    """The version the member is deprecated since, if given.

    It is only used for the documentation, never in the warning.
    """

    self._validate()

frequenz.core.enum.DeprecatedMemberWarning ¤

Bases: DeprecationWarning

Warning category for deprecated enum members.

Source code in src/frequenz/core/enum.py
class DeprecatedMemberWarning(DeprecationWarning):
    """Warning category for deprecated enum members."""

frequenz.core.enum.DeprecatingEnumType ¤

Bases: EnumType

Enum metaclass that supports deprecated members.

Tip

Normally it is not necessary to use this class directly, use Enum instead.

Behavior:

  • In the class body, members may be declared as NAME = deprecated_member(value, ...). See deprecated_member() and DeprecatedMember for details.
  • During class creation, these wrappers are replaced with value so that a normal enum member or alias is created by EnumType.
  • Once the class is created, each new_name is checked to be a member that is not deprecated, and the warnings of members without a message are generated from it, now that the fully qualified name of the class is known.
  • The deprecated names are recorded so that:

    • MyEnum.NAME warns (attribute access by name)
    • MyEnum["NAME"] warns (lookup by name)
    • MyEnum(value) warns only if the resolved member has no non-deprecated aliases (all names for that member are deprecated).
Source code in src/frequenz/core/enum.py
class DeprecatingEnumType(enum.EnumType):
    """Enum metaclass that supports deprecated members.

    Tip:
        Normally it is not necessary to use this class directly, use
        [`Enum`][frequenz.core.enum.Enum] instead.

    Behavior:

    - In the class body, members may be declared as
      `NAME = deprecated_member(value, ...)`. See
      [`deprecated_member()`][frequenz.core.enum.deprecated_member] and
      [`DeprecatedMember`][frequenz.core.enum.DeprecatedMember] for details.
    - During class creation, these wrappers are replaced with `value` so that
      a normal enum member or alias is created by [`EnumType`][enum.EnumType].
    - Once the class is created, each `new_name` is checked to be a member that is
      not deprecated, and the warnings of members without a `message` are
      generated from it, now that the fully qualified name of the class is known.
    - The deprecated names are recorded so that:

        * `MyEnum.NAME` warns (attribute access by name)
        * `MyEnum["NAME"]` warns (lookup by name)
        * `MyEnum(value)` warns **only if** the resolved member has **no**
            non-deprecated aliases (all names for that member are deprecated).
    """

    def __new__(  # pylint: disable=too-many-locals
        mcs,
        name: str,
        bases: tuple[type[EnumT], ...],
        classdict: Mapping[str, Any],
        **kw: Any,
    ) -> type[EnumT]:
        """Create the new enum class rewriting deprecated members.

        Members declared as
        [`DeprecatedMember`][frequenz.core.enum.DeprecatedMember] are replaced
        with their value.
        """
        deprecated: dict[str, DeprecatedMember] = {}
        prepared = super().__prepare__(name, bases, **kw)

        # Unwrap DeprecatedMembers and record them as deprecated
        for key, value in classdict.items():
            if isinstance(value, DeprecatedMember):
                deprecated[key] = value
                prepared[key] = value.value
            else:
                prepared[key] = value

        cls = cast(type[EnumT], super().__new__(mcs, name, bases, prepared, **kw))
        deprecated_names = _deprecation_messages(cls, deprecated)

        # Build alias groups: member -> list of names
        member_to_names: dict[EnumT, list[str]] = {}
        member: EnumT
        for member_name, member in cls.__members__.items():
            member_to_names.setdefault(member, []).append(member_name)

        warned_by_member: dict[EnumT, str] = {}
        for member, names in member_to_names.items():
            # warn on value only if all alias names are deprecated
            deprecated_aliases = [n for n in names if n in deprecated_names]
            if deprecated_aliases and len(deprecated_aliases) == len(names):
                warned_by_member[member] = deprecated_names[deprecated_aliases[0]]

        # Inject maps quietly
        type.__setattr__(cls, "__deprecated_names__", deprecated_names)
        type.__setattr__(cls, "__deprecated_value_map__", warned_by_member)

        return cls

    @staticmethod
    def _name_map(cls_: type[Any]) -> Mapping[str, str]:
        """Map from member names to deprecation messages."""
        return cast(
            Mapping[str, str],
            type.__getattribute__(cls_, "__dict__").get("__deprecated_names__", {}),
        )

    @staticmethod
    def _value_map(cls_: type[Any]) -> Mapping[Any, str]:
        """Map from enum members to deprecation messages."""
        return cast(
            Mapping[Any, str],
            type.__getattribute__(cls_, "__dict__").get("__deprecated_value_map__", {}),
        )

    def __getattribute__(cls, name: str) -> Any:
        """Resolve `name` to a member, warning if the member is deprecated."""
        if name in ("__deprecated_names__", "__deprecated_value_map__"):
            return type.__getattribute__(cls, name)
        deprecated = DeprecatingEnumType._name_map(cls)
        if name in deprecated:
            warnings.warn(deprecated[name], DeprecatedMemberWarning, stacklevel=2)
        return super().__getattribute__(name)

    def __getitem__(cls, name: str) -> Any:
        """Resolve `name` to a member, warning if the member is deprecated."""
        deprecated = DeprecatingEnumType._name_map(cls)
        if name in deprecated:
            warnings.warn(deprecated[name], DeprecatedMemberWarning, stacklevel=2)
        return super().__getitem__(name)

    def __call__(cls, value: Any, *args: Any, **kwargs: Any) -> Any:
        """Resolve `value` to a member, warning if the member is purely deprecated."""
        member = super().__call__(value, *args, **kwargs)
        value_map: Mapping[Any, str] = DeprecatingEnumType._value_map(cls)
        msg = value_map.get(member)
        if msg is not None:
            warnings.warn(msg, DeprecatedMemberWarning, stacklevel=2)
        return member
Methods:¤
__call__ ¤
__call__(value: Any, *args: Any, **kwargs: Any) -> Any

Resolve value to a member, warning if the member is purely deprecated.

Source code in src/frequenz/core/enum.py
def __call__(cls, value: Any, *args: Any, **kwargs: Any) -> Any:
    """Resolve `value` to a member, warning if the member is purely deprecated."""
    member = super().__call__(value, *args, **kwargs)
    value_map: Mapping[Any, str] = DeprecatingEnumType._value_map(cls)
    msg = value_map.get(member)
    if msg is not None:
        warnings.warn(msg, DeprecatedMemberWarning, stacklevel=2)
    return member
__getattribute__ ¤
__getattribute__(name: str) -> Any

Resolve name to a member, warning if the member is deprecated.

Source code in src/frequenz/core/enum.py
def __getattribute__(cls, name: str) -> Any:
    """Resolve `name` to a member, warning if the member is deprecated."""
    if name in ("__deprecated_names__", "__deprecated_value_map__"):
        return type.__getattribute__(cls, name)
    deprecated = DeprecatingEnumType._name_map(cls)
    if name in deprecated:
        warnings.warn(deprecated[name], DeprecatedMemberWarning, stacklevel=2)
    return super().__getattribute__(name)
__getitem__ ¤
__getitem__(name: str) -> Any

Resolve name to a member, warning if the member is deprecated.

Source code in src/frequenz/core/enum.py
def __getitem__(cls, name: str) -> Any:
    """Resolve `name` to a member, warning if the member is deprecated."""
    deprecated = DeprecatingEnumType._name_map(cls)
    if name in deprecated:
        warnings.warn(deprecated[name], DeprecatedMemberWarning, stacklevel=2)
    return super().__getitem__(name)
__new__ ¤
__new__(
    mcs,
    name: str,
    bases: tuple[type[EnumT], ...],
    classdict: Mapping[str, Any],
    **kw: Any
) -> type[EnumT]

Create the new enum class rewriting deprecated members.

Members declared as DeprecatedMember are replaced with their value.

Source code in src/frequenz/core/enum.py
def __new__(  # pylint: disable=too-many-locals
    mcs,
    name: str,
    bases: tuple[type[EnumT], ...],
    classdict: Mapping[str, Any],
    **kw: Any,
) -> type[EnumT]:
    """Create the new enum class rewriting deprecated members.

    Members declared as
    [`DeprecatedMember`][frequenz.core.enum.DeprecatedMember] are replaced
    with their value.
    """
    deprecated: dict[str, DeprecatedMember] = {}
    prepared = super().__prepare__(name, bases, **kw)

    # Unwrap DeprecatedMembers and record them as deprecated
    for key, value in classdict.items():
        if isinstance(value, DeprecatedMember):
            deprecated[key] = value
            prepared[key] = value.value
        else:
            prepared[key] = value

    cls = cast(type[EnumT], super().__new__(mcs, name, bases, prepared, **kw))
    deprecated_names = _deprecation_messages(cls, deprecated)

    # Build alias groups: member -> list of names
    member_to_names: dict[EnumT, list[str]] = {}
    member: EnumT
    for member_name, member in cls.__members__.items():
        member_to_names.setdefault(member, []).append(member_name)

    warned_by_member: dict[EnumT, str] = {}
    for member, names in member_to_names.items():
        # warn on value only if all alias names are deprecated
        deprecated_aliases = [n for n in names if n in deprecated_names]
        if deprecated_aliases and len(deprecated_aliases) == len(names):
            warned_by_member[member] = deprecated_names[deprecated_aliases[0]]

    # Inject maps quietly
    type.__setattr__(cls, "__deprecated_names__", deprecated_names)
    type.__setattr__(cls, "__deprecated_value_map__", warned_by_member)

    return cls

frequenz.core.enum.Enum ¤

Bases: Enum

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.

To declare a deprecated member, use the 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
from frequenz.core.enum import Enum, deprecated_member

class TaskStatus(Enum):
    OPEN = 1
    IN_PROGRESS = 2
    FINISHED = 3
    PENDING = deprecated_member(1, new_name="OPEN", since="v1.2.0")
    CANCELLED = deprecated_member(
        4,
        "mypkg.TaskStatus.CANCELLED is deprecated. "
        "Cancelled tasks are deleted now.",
    )
    """The task was cancelled.

    Deprecated:
        Deprecated since v1.3.0. Cancelled tasks are deleted now.
    """

# Accessing deprecated members by name warns:
status1 = TaskStatus.PENDING
assert status1 is TaskStatus.OPEN

status2 = TaskStatus["PENDING"]
assert status2 is TaskStatus.OPEN

# No warning, 1 resolves to OPEN, which is not deprecated
status3 = TaskStatus(1)
assert status3 is TaskStatus.OPEN

# Warns, as all the names for 4 are deprecated
status4 = TaskStatus(4)
assert status4.name == "CANCELLED"

PENDING is a plain rename, so new_name is enough: reaching it warns with mypkg.TaskStatus.PENDING is deprecated. Use mypkg.TaskStatus.OPEN instead., mypkg being the module defining the enum, and griffe-frequenz-core documents it with a Deprecated since v1.2.0. Use [`OPEN`][mypkg.TaskStatus.OPEN] instead. notice.

CANCELLED has no member to point at, so it gives its own message, starting with its fully qualified name. That replaces the warning only, so its notice is written by hand, as a Deprecated: admonition in the member's docstring. That notice says the version too, so since is not needed.

Source code in src/frequenz/core/enum.py
class Enum(enum.Enum, metaclass=DeprecatingEnumType):
    '''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
    [`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, deprecated_member

        class TaskStatus(Enum):
            OPEN = 1
            IN_PROGRESS = 2
            FINISHED = 3
            PENDING = deprecated_member(1, new_name="OPEN", since="v1.2.0")
            CANCELLED = deprecated_member(
                4,
                "mypkg.TaskStatus.CANCELLED is deprecated. "
                "Cancelled tasks are deleted now.",
            )
            """The task was cancelled.

            Deprecated:
                Deprecated since v1.3.0. Cancelled tasks are deleted now.
            """

        # Accessing deprecated members by name warns:
        status1 = TaskStatus.PENDING
        assert status1 is TaskStatus.OPEN

        status2 = TaskStatus["PENDING"]
        assert status2 is TaskStatus.OPEN

        # No warning, 1 resolves to OPEN, which is not deprecated
        status3 = TaskStatus(1)
        assert status3 is TaskStatus.OPEN

        # Warns, as all the names for 4 are deprecated
        status4 = TaskStatus(4)
        assert status4.name == "CANCELLED"
        ```

        `PENDING` is a plain rename, so `new_name` is enough: reaching it warns
        with `mypkg.TaskStatus.PENDING is deprecated. Use
        mypkg.TaskStatus.OPEN instead.`, `mypkg` being the module defining
        the enum, and
        [`griffe-frequenz-core`](https://github.com/frequenz-floss/griffe-frequenz-core)
        documents it with a ``Deprecated since v1.2.0. Use
        [`OPEN`][mypkg.TaskStatus.OPEN] instead.`` notice.

        `CANCELLED` has no member to point at, so it gives its own `message`,
        starting with its fully qualified name. That replaces the warning
        only, so its notice is written by hand, as a `Deprecated:` admonition
        in the member's docstring. That notice says the version too, so
        `since` is not needed.
    '''

    __deprecated_names__: ClassVar[Mapping[str, str]]
    __deprecated_value_map__: ClassVar[Mapping[Self, str]]

Functions:¤

frequenz.core.enum.deprecated_member ¤

deprecated_member(
    value: ValueT,
    message: str,
    *,
    new_name: str | None = None,
    since: str | None = None
) -> ValueT
deprecated_member(
    value: ValueT,
    message: None = None,
    *,
    new_name: str,
    since: str | None = None
) -> ValueT
deprecated_member(
    value: ValueT,
    message: str | None = None,
    *,
    new_name: str | None = None,
    since: str | None = None
) -> ValueT

Mark an enum member as deprecated.

At least one of message and new_name must be given. With new_name alone, reaching the member warns with <Enum>.<NAME> is deprecated. Use <Enum>.<new_name> instead., with fully qualified names, which is what the deprecations guide asks for. message replaces that warning.

since is not part of the warning: it is only there for the documentation. griffe-frequenz-core reads it, together with new_name, to generate the member's Deprecated: notice, so message changes the warning only. To change the notice, write a Deprecated: admonition by hand in the member's docstring, which replaces the generated one.

When the enum class is created, new_name is checked to be another member of the same enum, which is not deprecated itself.

Please read the Enum documentation for details and examples.

PARAMETER DESCRIPTION
value

The value of the enum member to mark as deprecated.

TYPE: ValueT

message

The warning message to emit when the member is reached, or None to generate it from new_name.

TYPE: str | None DEFAULT: None

new_name

The name of the member to use instead, in the same enum.

TYPE: str | None DEFAULT: None

since

The version the member is deprecated since, such as "v1.2.0", for the documentation only.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
ValueT

The wrapped value, to mark the enum member as deprecated.

RAISES DESCRIPTION
TypeError

If neither message nor new_name is given, or if new_name or since is not a string.

ValueError

If new_name is not an identifier, or if since is empty.

Source code in src/frequenz/core/enum.py
def deprecated_member(  # noqa: DOC502
    value: ValueT,
    message: str | None = None,
    *,
    new_name: str | None = None,
    since: str | None = None,
) -> ValueT:
    """Mark an enum member as deprecated.

    At least one of `message` and `new_name` must be given. With `new_name` alone,
    reaching the member warns with `<Enum>.<NAME> is deprecated. Use
    <Enum>.<new_name> instead.`, with fully qualified names, which is what the
    deprecations guide asks for. `message` replaces that warning.

    `since` is not part of the warning: it is only there for the documentation.
    [`griffe-frequenz-core`](https://github.com/frequenz-floss/griffe-frequenz-core)
    reads it, together with `new_name`, to generate the member's `Deprecated:`
    notice, so `message` changes the warning only. To change the notice, write a
    `Deprecated:` admonition by hand in the member's docstring, which replaces the
    generated one.

    When the enum class is created, `new_name` is checked to be another member of
    the same enum, which is not deprecated itself.

    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 warning message to emit when the member is reached, or `None`
            to generate it from `new_name`.
        new_name: The name of the member to use instead, in the same enum.
        since: The version the member is deprecated since, such as `"v1.2.0"`, for
            the documentation only.

    Returns:
        The wrapped value, to mark the enum member as deprecated.

    Raises:
        TypeError: If neither `message` nor `new_name` is given, or if `new_name`
            or `since` is not a string.
        ValueError: If `new_name` is not an identifier, or if `since` is empty.
    """
    # The overloads of `deprecated_member()` already checked the combination, which
    # mypy can't tell from the `| None` types here.
    member = DeprecatedMember(
        value, message, new_name=new_name, since=since  # type: ignore[arg-type]
    )
    return cast(ValueT, member)

frequenz.core.enum.unique ¤

unique(enumeration: type[EnumT]) -> type[EnumT]

Class decorator for enums that ensures unique non-deprecated values.

This works similarly to @enum.unique, but it only enforces uniqueness for members that are not deprecated. This allows deprecated members to be aliases for non-deprecated members without causing a ValueError.

If you need strict uniqueness for all deprecated and non-deprecated members, use @enum.unique instead.

Example
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 = deprecated_member(1, new_name="OPEN", since="v1.2.0")
PARAMETER DESCRIPTION
enumeration

The enum class to decorate.

TYPE: type[EnumT]

RETURNS DESCRIPTION
type[EnumT]

The decorated enum class.

RAISES DESCRIPTION
ValueError

If duplicate values are found among non-deprecated members.

Source code in src/frequenz/core/enum.py
def unique(enumeration: type[EnumT]) -> type[EnumT]:
    """Class decorator for enums that ensures unique non-deprecated values.

    This works similarly to [`@enum.unique`][enum.unique], but it only enforces
    uniqueness for members that are not deprecated. This allows deprecated members to
    be aliases for non-deprecated members without causing a `ValueError`.

    If you need strict uniqueness for all deprecated and non-deprecated members, use
    [`@enum.unique`][enum.unique] instead.

    Example:
        ```python
        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 = deprecated_member(1, new_name="OPEN", since="v1.2.0")
        ```

    Args:
        enumeration: The enum class to decorate.

    Returns:
        The decorated enum class.

    Raises:
        ValueError: If duplicate values are found among non-deprecated members.
    """
    # Retrieve the map of deprecated names created by the metaclass.
    deprecated_names = enumeration.__dict__.get("__deprecated_names__", {})

    duplicates = []
    seen_values: dict[Any, str] = {}
    for member_name, member in enumeration.__members__.items():
        # Ignore members that are marked as deprecated.
        if member_name in deprecated_names:
            continue

        value = member.value
        if value in seen_values:
            duplicates.append((member_name, seen_values[value]))
        else:
            seen_values[value] = member_name

    if duplicates:
        alias_details = ", ".join(
            f"{name!r} -> {alias!r}" for name, alias in duplicates
        )
        raise ValueError(
            f"duplicate values found in {enumeration.__name__}: {alias_details}"
        )

    return enumeration