Skip to content

Commit abac31c

Browse files
committed
Move common.enum_proto to common.proto
We will start to expose conversion functions, for which we will always use a sub-module called `proto` for these conversion functions, so we we do the same with `enum_proto`. The old version is kept as deprecated. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 86494bc commit abac31c

File tree

9 files changed

+153
-36
lines changed

9 files changed

+153
-36
lines changed

src/frequenz/client/common/enum_proto.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import enum
77
from typing import Literal, TypeVar, overload
88

9+
from typing_extensions import deprecated
10+
911
EnumT = TypeVar("EnumT", bound=enum.Enum)
1012
"""A type variable that is bound to an enum."""
1113

@@ -22,6 +24,10 @@ def enum_from_proto(
2224
) -> EnumT | int: ...
2325

2426

27+
@deprecated(
28+
"frequenz.client.common.enum_proto.enum_from_proto is deprecated. "
29+
"Please use frequenz.client.common.proto.enum_from_proto instead."
30+
)
2531
def enum_from_proto(
2632
value: int, enum_type: type[EnumT], *, allow_invalid: bool = True
2733
) -> EnumT | int:

src/frequenz/client/common/metric/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class Metric(enum.Enum):
142142
SENSOR_IRRADIANCE = PBMetric.METRIC_SENSOR_IRRADIANCE
143143

144144
@classmethod
145-
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
145+
@deprecated("Use `frequenz.client.common.proto.enum_from_proto` instead.")
146146
def from_proto(cls, metric: PBMetric.ValueType) -> Self:
147147
"""Convert a protobuf Metric value to Metric enum.
148148

src/frequenz/client/common/microgrid/components/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class ComponentCategory(enum.Enum):
105105
"""A Heating, Ventilation, and Air Conditioning (HVAC) system."""
106106

107107
@classmethod
108-
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
108+
@deprecated("Use `frequenz.client.common.proto.enum_from_proto` instead.")
109109
def from_proto(
110110
cls, component_category: PBComponentCategory.ValueType
111111
) -> ComponentCategory:
@@ -211,7 +211,7 @@ class ComponentStateCode(enum.Enum):
211211
"""The precharger circuit is closed, allowing full current to flow to the main circuit."""
212212

213213
@classmethod
214-
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
214+
@deprecated("Use `frequenz.client.common.proto.enum_from_proto` instead.")
215215
def from_proto(
216216
cls, component_state: PBComponentStateCode.ValueType
217217
) -> ComponentStateCode:
@@ -390,7 +390,7 @@ class ComponentErrorCode(enum.Enum):
390390
times."""
391391

392392
@classmethod
393-
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
393+
@deprecated("Use `frequenz.client.common.proto.enum_from_proto` instead.")
394394
def from_proto(
395395
cls, component_error_code: PBComponentErrorCode.ValueType
396396
) -> ComponentErrorCode:

src/frequenz/client/common/microgrid/electrical_components/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class ElectricalComponentCategory(enum.Enum):
8989
"""A heating, ventilation, and air conditioning (HVAC) system."""
9090

9191
@classmethod
92-
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
92+
@deprecated("Use `frequenz.client.common.proto.enum_from_proto` instead.")
9393
def from_proto(
9494
cls, component_category: PBElectricalComponentCategory.ValueType
9595
) -> ElectricalComponentCategory:
@@ -217,7 +217,7 @@ class ElectricalComponentStateCode(enum.Enum):
217217
"""The precharger circuit is closed, allowing full current to flow to the main circuit."""
218218

219219
@classmethod
220-
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
220+
@deprecated("Use `frequenz.client.common.proto.enum_from_proto` instead.")
221221
def from_proto(
222222
cls, component_state: PBElectricalComponentStateCode.ValueType
223223
) -> ElectricalComponentStateCode:
@@ -432,7 +432,7 @@ class ElectricalComponentDiagnosticCode(enum.Enum):
432432
times."""
433433

434434
@classmethod
435-
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
435+
@deprecated("Use `frequenz.client.common.proto.enum_from_proto` instead.")
436436
def from_proto(
437437
cls, component_error_code: PBElectricalComponentDiagnosticCode.ValueType
438438
) -> ElectricalComponentDiagnosticCode:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""General utilities for converting common types to/from protobuf types."""
5+
6+
from ._enum import enum_from_proto
7+
8+
__all__ = [
9+
"enum_from_proto",
10+
]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Conversion of protobuf int enums to Python enums."""
5+
6+
import enum
7+
from typing import Literal, TypeVar, overload
8+
9+
EnumT = TypeVar("EnumT", bound=enum.Enum)
10+
"""A type variable that is bound to an enum."""
11+
12+
13+
@overload
14+
def enum_from_proto(
15+
value: int, enum_type: type[EnumT], *, allow_invalid: Literal[False]
16+
) -> EnumT: ...
17+
18+
19+
@overload
20+
def enum_from_proto(
21+
value: int, enum_type: type[EnumT], *, allow_invalid: Literal[True] = True
22+
) -> EnumT | int: ...
23+
24+
25+
def enum_from_proto(
26+
value: int, enum_type: type[EnumT], *, allow_invalid: bool = True
27+
) -> EnumT | int:
28+
"""Convert a protobuf int enum value to a python enum.
29+
30+
Example:
31+
```python
32+
import enum
33+
34+
from proto import proto_pb2 # Just an example. pylint: disable=import-error
35+
36+
@enum.unique
37+
class SomeEnum(enum.Enum):
38+
# These values should match the protobuf enum values.
39+
UNSPECIFIED = 0
40+
SOME_VALUE = 1
41+
42+
enum_value = enum_from_proto(proto_pb2.SomeEnum.SOME_ENUM_SOME_VALUE, SomeEnum)
43+
# -> SomeEnum.SOME_VALUE
44+
45+
enum_value = enum_from_proto(42, SomeEnum)
46+
# -> 42
47+
48+
enum_value = enum_from_proto(
49+
proto_pb2.SomeEnum.SOME_ENUM_UNKNOWN_VALUE, SomeEnum, allow_invalid=False
50+
)
51+
# -> ValueError
52+
```
53+
54+
Args:
55+
value: The protobuf int enum value.
56+
enum_type: The python enum type to convert to.
57+
allow_invalid: If `True`, return the value as an `int` if the value is not
58+
a valid member of the enum (this allows for forward-compatibility with new
59+
enum values defined in the protocol but not added to the Python enum yet).
60+
If `False`, raise a `ValueError` if the value is not a valid member of the
61+
enum.
62+
63+
Returns:
64+
The resulting python enum value if the protobuf value is known, otherwise
65+
the input value converted to a plain `int`.
66+
67+
Raises:
68+
ValueError: If `allow_invalid` is `False` and the value is not a valid member
69+
of the enum.
70+
"""
71+
try:
72+
return enum_type(value)
73+
except ValueError:
74+
if allow_invalid:
75+
return value
76+
raise

tests/proto/test_enum.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for enum_from_proto utility."""
5+
6+
import enum
7+
8+
import pytest
9+
10+
from frequenz.client.common.proto import enum_from_proto
11+
12+
13+
class _TestEnum(enum.Enum):
14+
"""A test enum for enum_from_proto tests."""
15+
16+
ZERO = 0
17+
ONE = 1
18+
TWO = 2
19+
20+
21+
@pytest.mark.parametrize("enum_member", _TestEnum)
22+
def test_valid_allow_invalid(enum_member: _TestEnum) -> None:
23+
"""Test conversion of valid enum values."""
24+
assert enum_from_proto(enum_member.value, _TestEnum) == enum_member
25+
assert (
26+
enum_from_proto(enum_member.value, _TestEnum, allow_invalid=True) == enum_member
27+
)
28+
29+
30+
@pytest.mark.parametrize("value", [42, -1])
31+
def test_invalid_allow_invalid(value: int) -> None:
32+
"""Test unknown values with allow_invalid=True (default)."""
33+
assert enum_from_proto(value, _TestEnum) == value
34+
assert enum_from_proto(value, _TestEnum, allow_invalid=True) == value
35+
36+
37+
@pytest.mark.parametrize("enum_member", _TestEnum)
38+
def test_valid_disallow_invalid(enum_member: _TestEnum) -> None:
39+
"""Test unknown values with allow_invalid=False (should raise ValueError)."""
40+
assert (
41+
enum_from_proto(enum_member.value, _TestEnum, allow_invalid=False)
42+
== enum_member
43+
)
44+
45+
46+
@pytest.mark.parametrize("value", [42, -1])
47+
def test_invalid_disallow(value: int) -> None:
48+
"""Test unknown values with allow_invalid=False (should raise ValueError)."""
49+
with pytest.raises(ValueError, match=rf"^{value} is not a valid _TestEnum$"):
50+
enum_from_proto(value, _TestEnum, allow_invalid=False)

tests/test_enum_proto.py

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,7 @@ class _TestEnum(enum.Enum):
1919

2020

2121
@pytest.mark.parametrize("enum_member", _TestEnum)
22-
def test_valid_allow_invalid(enum_member: _TestEnum) -> None:
22+
def test_deprecated(enum_member: _TestEnum) -> None:
2323
"""Test conversion of valid enum values."""
24-
assert enum_from_proto(enum_member.value, _TestEnum) == enum_member
25-
assert (
26-
enum_from_proto(enum_member.value, _TestEnum, allow_invalid=True) == enum_member
27-
)
28-
29-
30-
@pytest.mark.parametrize("value", [42, -1])
31-
def test_invalid_allow_invalid(value: int) -> None:
32-
"""Test unknown values with allow_invalid=True (default)."""
33-
assert enum_from_proto(value, _TestEnum) == value
34-
assert enum_from_proto(value, _TestEnum, allow_invalid=True) == value
35-
36-
37-
@pytest.mark.parametrize("enum_member", _TestEnum)
38-
def test_valid_disallow_invalid(enum_member: _TestEnum) -> None:
39-
"""Test unknown values with allow_invalid=False (should raise ValueError)."""
40-
assert (
41-
enum_from_proto(enum_member.value, _TestEnum, allow_invalid=False)
42-
== enum_member
43-
)
44-
45-
46-
@pytest.mark.parametrize("value", [42, -1])
47-
def test_invalid_disallow(value: int) -> None:
48-
"""Test unknown values with allow_invalid=False (should raise ValueError)."""
49-
with pytest.raises(ValueError, match=rf"^{value} is not a valid _TestEnum$"):
50-
enum_from_proto(value, _TestEnum, allow_invalid=False)
24+
with pytest.deprecated_call():
25+
enum_from_proto(enum_member.value, _TestEnum)

tests/test_streaming.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
"""Tests for the frequenz.client.common.streaming package."""
55

6-
from frequenz.client.common.enum_proto import enum_from_proto
6+
from frequenz.client.common.proto import enum_from_proto
77
from frequenz.client.common.streaming import Event
88

99

0 commit comments

Comments
 (0)