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 ValueError
will be raised 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 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.
"""
# 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.
"""
# 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.
"""
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
The prefix used for the string representation of this ID.
Functions
__eq__
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.
Source code in 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.
"""
# 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__
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.
Source code in 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.
"""
return hash((type(self), self._id))
|
__init__
__init__(id_: int) -> None
Initialize this instance.
PARAMETER |
DESCRIPTION |
id_
|
The numeric unique identifier.
TYPE:
int
|
Source code in 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 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__
Return the numeric ID of this instance.
Source code in frequenz/core/id.py
| def __int__(self) -> int:
"""Return the numeric ID of this instance."""
return self._id
|
__lt__
Check if this instance is less than another object.
Comparison is only defined between instances of the exact same type.
Source code in 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.
"""
# 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__
Create a new instance of the ID class, only if it is a subclass of BaseId.
Source code in 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__
Return the string representation of this instance.
Source code in frequenz/core/id.py
| def __repr__(self) -> str:
"""Return the string representation of this instance."""
return f"{type(self).__name__}({self._id!r})"
|
__str__
Return the short string representation of this instance.
Source code in frequenz/core/id.py
| def __str__(self) -> str:
"""Return the short string representation of this instance."""
return f"{self._str_prefix}{self._id}"
|