Skip to content

id

frequenz.core.id ¤

Provides strongly-typed unique identifiers for entities.

This module offers a base class, BaseId, which can be subclassed to create distinct ID types for different components or concepts within a system.

These IDs ensure type safety, meaning that an ID for one type of entity (e.g., a sensor) cannot be mistakenly used where an ID for another type (e.g., a microgrid) is expected.

Creating Custom ID Types¤

To define a new ID type, create a class that inherits from BaseId and provide a unique str_prefix as a keyword argument in the class definition. This prefix is used in the string representation of the ID and must be unique across all ID types.

Note

The str_prefix must be unique across all ID types. If you try to use a prefix that is already registered, a warning will be logged when defining the class.

To encourage consistency, the class name must end with the suffix "Id" (e.g., MyNewId). This check can be bypassed by passing allow_custom_name=True when defining the class (e.g., class MyCustomName(BaseId, str_prefix="MCN", allow_custom_name=True):).

Tip

Use the @typing.final decorator to prevent subclassing of ID classes.

Creating a standard ID type
from typing import final
from frequenz.core.id import BaseId

@final
class InverterId(BaseId, str_prefix="INV"):
    """A unique identifier for an inverter."""

inv_id = InverterId(123)
print(inv_id)       # Output: INV123
print(int(inv_id))  # Output: 123
Creating an ID type with a non-standard name
from typing import final
from frequenz.core.id import BaseId

@final
class CustomNameForId(BaseId, str_prefix="CST", allow_custom_name=True):
    """An ID with a custom name, not ending in 'Id'."""

custom_id = CustomNameForId(456)
print(custom_id)       # Output: CST456
print(int(custom_id))  # Output: 456

Classes¤

frequenz.core.id.BaseId ¤

A base class for unique identifiers.

Subclasses must provide a unique str_prefix keyword argument during definition, which is used in the string representation of the ID.

By default, subclass names must end with "Id". This can be overridden by passing allow_custom_name=True during class definition.

For more information and examples, see the module's documentation.

Source code in src/frequenz/core/id.py
class BaseId:
    """A base class for unique identifiers.

    Subclasses must provide a unique `str_prefix` keyword argument during
    definition, which is used in the string representation of the ID.

    By default, subclass names must end with "Id". This can be overridden by
    passing `allow_custom_name=True` during class definition.

    For more information and examples, see the [module's
    documentation][frequenz.core.id].
    """

    _id: int
    _str_prefix: ClassVar[str]
    _registered_prefixes: ClassVar[set[str]] = set()

    def __new__(cls, *_: Any, **__: Any) -> Self:
        """Create a new instance of the ID class, only if it is a subclass of BaseId."""
        if cls is BaseId:
            raise TypeError("BaseId cannot be instantiated directly. Use a subclass.")
        return super().__new__(cls)

    def __init_subclass__(
        cls,
        *,
        str_prefix: str,
        allow_custom_name: bool = False,
        **kwargs: Any,
    ) -> None:
        """Initialize a subclass, set its string prefix, and perform checks.

        Args:
            str_prefix: The string prefix for the ID type (e.g., "MID").
                Must be unique across all ID types.
            allow_custom_name: If True, bypasses the check that the class name
                must end with "Id". Defaults to False.
            **kwargs: Forwarded to the parent's __init_subclass__.

        Raises:
            TypeError: If `allow_custom_name` is False and the class name
                does not end with "Id".
        """
        super().__init_subclass__(**kwargs)

        if str_prefix in BaseId._registered_prefixes:
            # We want to raise an exception here, but currently can't due to
            # https://github.com/frequenz-floss/frequenz-repo-config-python/issues/421
            _logger.warning(
                "Prefix '%s' is already registered. ID prefixes must be unique.",
                str_prefix,
            )
        BaseId._registered_prefixes.add(str_prefix)

        if not allow_custom_name and not cls.__name__.endswith("Id"):
            raise TypeError(
                f"Class name '{cls.__name__}' for an ID class must end with 'Id' "
                "(e.g., 'SomeId'), or use `allow_custom_name=True`."
            )

        cls._str_prefix = str_prefix

    def __init__(self, id_: int, /) -> None:
        """Initialize this instance.

        Args:
            id_: The numeric unique identifier.

        Raises:
            ValueError: If the ID is negative.
        """
        if id_ < 0:
            raise ValueError(f"{type(self).__name__} can't be negative.")
        self._id = id_

    @property
    def str_prefix(self) -> str:
        """The prefix used for the string representation of this ID."""
        return self._str_prefix

    def __int__(self) -> int:
        """Return the numeric ID of this instance."""
        return self._id

    def __eq__(self, other: object) -> bool:
        """Check if this instance is equal to another object.

        Equality is defined as being of the exact same type and having the same
        underlying ID.

        Args:
            other: The object to compare against.

        Returns:
            True if `other` is of the same type and has the same underlying ID,
            `NotImplemented` if `other` is of a different type.
        """
        # pylint thinks this is not an unidiomatic typecheck, but in this case
        # it is not. isinstance() returns True for subclasses, which is not
        # what we want here, as different ID types should never be equal.
        # pylint: disable-next=unidiomatic-typecheck
        if type(other) is not type(self):
            return NotImplemented
        # We already checked type(other) is type(self), but mypy doesn't
        # understand that, so we need to cast it to Self.
        other_id = cast(Self, other)
        return self._id == other_id._id

    def __lt__(self, other: object) -> bool:
        """Check if this instance is less than another object.

        Comparison is only defined between instances of the exact same type.

        Args:
            other: The object to compare against.

        Returns:
            True if this instance is less than `other`, `NotImplemented` if
            `other` is of a different type.
        """
        # pylint: disable-next=unidiomatic-typecheck
        if type(other) is not type(self):
            return NotImplemented
        other_id = cast(Self, other)
        return self._id < other_id._id

    def __hash__(self) -> int:
        """Return the hash of this instance.

        The hash is based on the exact type and the underlying ID to ensure
        that IDs of different types but with the same numeric value have different hashes.

        Returns:
            The hash of this instance.
        """
        return hash((type(self), self._id))

    def __repr__(self) -> str:
        """Return the string representation of this instance."""
        return f"{type(self).__name__}({self._id!r})"

    def __str__(self) -> str:
        """Return the short string representation of this instance."""
        return f"{self._str_prefix}{self._id}"
Attributes¤
str_prefix property ¤
str_prefix: str

The prefix used for the string representation of this ID.

Methods:¤
__eq__ ¤
__eq__(other: object) -> bool

Check if this instance is equal to another object.

Equality is defined as being of the exact same type and having the same underlying ID.

PARAMETER DESCRIPTION
other

The object to compare against.

TYPE: object

RETURNS DESCRIPTION
bool

True if other is of the same type and has the same underlying ID,

bool

NotImplemented if other is of a different type.

Source code in src/frequenz/core/id.py
def __eq__(self, other: object) -> bool:
    """Check if this instance is equal to another object.

    Equality is defined as being of the exact same type and having the same
    underlying ID.

    Args:
        other: The object to compare against.

    Returns:
        True if `other` is of the same type and has the same underlying ID,
        `NotImplemented` if `other` is of a different type.
    """
    # pylint thinks this is not an unidiomatic typecheck, but in this case
    # it is not. isinstance() returns True for subclasses, which is not
    # what we want here, as different ID types should never be equal.
    # pylint: disable-next=unidiomatic-typecheck
    if type(other) is not type(self):
        return NotImplemented
    # We already checked type(other) is type(self), but mypy doesn't
    # understand that, so we need to cast it to Self.
    other_id = cast(Self, other)
    return self._id == other_id._id
__hash__ ¤
__hash__() -> int

Return the hash of this instance.

The hash is based on the exact type and the underlying ID to ensure that IDs of different types but with the same numeric value have different hashes.

RETURNS DESCRIPTION
int

The hash of this instance.

Source code in src/frequenz/core/id.py
def __hash__(self) -> int:
    """Return the hash of this instance.

    The hash is based on the exact type and the underlying ID to ensure
    that IDs of different types but with the same numeric value have different hashes.

    Returns:
        The hash of this instance.
    """
    return hash((type(self), self._id))
__init__ ¤
__init__(id_: int) -> None

Initialize this instance.

PARAMETER DESCRIPTION
id_

The numeric unique identifier.

TYPE: int

RAISES DESCRIPTION
ValueError

If the ID is negative.

Source code in src/frequenz/core/id.py
def __init__(self, id_: int, /) -> None:
    """Initialize this instance.

    Args:
        id_: The numeric unique identifier.

    Raises:
        ValueError: If the ID is negative.
    """
    if id_ < 0:
        raise ValueError(f"{type(self).__name__} can't be negative.")
    self._id = id_
__init_subclass__ ¤
__init_subclass__(
    *,
    str_prefix: str,
    allow_custom_name: bool = False,
    **kwargs: Any
) -> None

Initialize a subclass, set its string prefix, and perform checks.

PARAMETER DESCRIPTION
str_prefix

The string prefix for the ID type (e.g., "MID"). Must be unique across all ID types.

TYPE: str

allow_custom_name

If True, bypasses the check that the class name must end with "Id". Defaults to False.

TYPE: bool DEFAULT: False

**kwargs

Forwarded to the parent's init_subclass.

TYPE: Any DEFAULT: {}

RAISES DESCRIPTION
TypeError

If allow_custom_name is False and the class name does not end with "Id".

Source code in src/frequenz/core/id.py
def __init_subclass__(
    cls,
    *,
    str_prefix: str,
    allow_custom_name: bool = False,
    **kwargs: Any,
) -> None:
    """Initialize a subclass, set its string prefix, and perform checks.

    Args:
        str_prefix: The string prefix for the ID type (e.g., "MID").
            Must be unique across all ID types.
        allow_custom_name: If True, bypasses the check that the class name
            must end with "Id". Defaults to False.
        **kwargs: Forwarded to the parent's __init_subclass__.

    Raises:
        TypeError: If `allow_custom_name` is False and the class name
            does not end with "Id".
    """
    super().__init_subclass__(**kwargs)

    if str_prefix in BaseId._registered_prefixes:
        # We want to raise an exception here, but currently can't due to
        # https://github.com/frequenz-floss/frequenz-repo-config-python/issues/421
        _logger.warning(
            "Prefix '%s' is already registered. ID prefixes must be unique.",
            str_prefix,
        )
    BaseId._registered_prefixes.add(str_prefix)

    if not allow_custom_name and not cls.__name__.endswith("Id"):
        raise TypeError(
            f"Class name '{cls.__name__}' for an ID class must end with 'Id' "
            "(e.g., 'SomeId'), or use `allow_custom_name=True`."
        )

    cls._str_prefix = str_prefix
__int__ ¤
__int__() -> int

Return the numeric ID of this instance.

Source code in src/frequenz/core/id.py
def __int__(self) -> int:
    """Return the numeric ID of this instance."""
    return self._id
__lt__ ¤
__lt__(other: object) -> bool

Check if this instance is less than another object.

Comparison is only defined between instances of the exact same type.

PARAMETER DESCRIPTION
other

The object to compare against.

TYPE: object

RETURNS DESCRIPTION
bool

True if this instance is less than other, NotImplemented if

bool

other is of a different type.

Source code in src/frequenz/core/id.py
def __lt__(self, other: object) -> bool:
    """Check if this instance is less than another object.

    Comparison is only defined between instances of the exact same type.

    Args:
        other: The object to compare against.

    Returns:
        True if this instance is less than `other`, `NotImplemented` if
        `other` is of a different type.
    """
    # pylint: disable-next=unidiomatic-typecheck
    if type(other) is not type(self):
        return NotImplemented
    other_id = cast(Self, other)
    return self._id < other_id._id
__new__ ¤
__new__(*_: Any, **__: Any) -> Self

Create a new instance of the ID class, only if it is a subclass of BaseId.

Source code in src/frequenz/core/id.py
def __new__(cls, *_: Any, **__: Any) -> Self:
    """Create a new instance of the ID class, only if it is a subclass of BaseId."""
    if cls is BaseId:
        raise TypeError("BaseId cannot be instantiated directly. Use a subclass.")
    return super().__new__(cls)
__repr__ ¤
__repr__() -> str

Return the string representation of this instance.

Source code in src/frequenz/core/id.py
def __repr__(self) -> str:
    """Return the string representation of this instance."""
    return f"{type(self).__name__}({self._id!r})"
__str__ ¤
__str__() -> str

Return the short string representation of this instance.

Source code in src/frequenz/core/id.py
def __str__(self) -> str:
    """Return the short string representation of this instance."""
    return f"{self._str_prefix}{self._id}"