Skip to content

Commit f3c1eee

Browse files
committed
Make power distributor's Results dataclasses
The Result class is an ABC but without any abtractmethods, a lot of boilerplate can be removed by converting it to a dataclass and the same for all subclasses. This also uses mixins to reduce code duplication even further and making sure result classes use consistent property names where it makes sense. We still need to keep `Result` a class, but after the minimum supported Python version is 3.10+ we can make `Result` a type union, so there is not way to instantiate if by mistake, but still we can use `isinstance(result, Success)` for example to what concrete type a result it is, or even better use the `match` syntax. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 35bf9b4 commit f3c1eee

File tree

2 files changed

+78
-99
lines changed

2 files changed

+78
-99
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727
grid_power = microgrid.logical_meter().grid_power()
2828
```
29+
* The `Result` class (and subclasses) for the `PowerDistributingActor` are now dataclasses, so logging them will produce a more detailed output.
2930

3031
## Bug Fixes
3132

src/frequenz/sdk/actor/power_distributing/result.py

Lines changed: 77 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -3,122 +3,100 @@
33

44
"""Results from PowerDistributingActor."""
55

6-
from abc import ABC
7-
from typing import Set
6+
from __future__ import annotations
7+
8+
import dataclasses
89

910
from .request import Request
1011

1112

12-
class Result(ABC):
13-
"""Base class for the power distributor result."""
14-
15-
def __init__(self, request: Request) -> None:
16-
"""Create class instance.
17-
18-
Args:
19-
request: The user's request to which this message responds.
20-
"""
21-
self.request: Request = request
22-
23-
24-
class Success(Result):
25-
"""Send if setting power for all batteries succeed."""
26-
27-
def __init__(
28-
self,
29-
request: Request,
30-
succeeded_power: int,
31-
succeeded_batteries: Set[int],
32-
excess_power: int,
33-
) -> None:
34-
"""Create class instance.
35-
36-
Args:
37-
request: The user's request to which this message responds.
38-
succeed_power: Part of the requested power that was successfully set.
39-
used_batteries: Subset of the requested batteries, that were used to
40-
realize the request.
41-
excess_power: Part of the requested power that could not be fulfilled,
42-
because it was outside available power bounds.
43-
"""
44-
super().__init__(request)
45-
self.succeed_power: int = succeeded_power
46-
self.used_batteries: Set[int] = succeeded_batteries
47-
self.excess_power: int = excess_power
48-
49-
50-
class PartialFailure(Result):
51-
"""Send if any battery failed and didn't perform the request."""
52-
53-
# It is very simple class with only data so it should be ok to disable pylint.
54-
# All these results should be dataclass but in python < 3.10 it is risky
55-
# to derive after dataclass.
56-
def __init__( # pylint: disable=too-many-arguments
57-
self,
58-
request: Request,
59-
succeeded_power: int,
60-
succeeded_batteries: Set[int],
61-
failed_power: int,
62-
failed_batteries: Set[int],
63-
excess_power: int,
64-
) -> None:
65-
"""Create class instance.
66-
67-
Args:
68-
request: The user's request to which this message responds.
69-
succeed_power: Part of the requested power that was successfully set.
70-
succeed_batteries: Subset of the requested batteries for which the request
71-
succeed.
72-
failed_power: Part of the requested power that failed.
73-
failed_batteries: Subset of the requested batteries for which the request
74-
failed.
75-
excess_power: Part of the requested power that could not be fulfilled,
76-
because it was outside available power bounds.
77-
"""
78-
super().__init__(request)
79-
self.succeed_power: int = succeeded_power
80-
self.succeed_batteries: Set[int] = succeeded_batteries
81-
self.failed_power: int = failed_power
82-
self.failed_batteries: Set[int] = failed_batteries
83-
self.excess_power: int = excess_power
13+
@dataclasses.dataclass
14+
class _BaseResultMixin:
15+
"""Base mixin class for reporting power distribution results."""
8416

17+
request: Request
18+
"""The user's request to which this message responds."""
19+
20+
21+
# When moving to Python 3.10+ we should replace this with an union type:
22+
# Result = Success | PartialFailure | Error | OutOfBound | Ignored
23+
# For now it can't be done because before 3.10 isinstance(result, Success)
24+
# doesn't work, so it is hard to figure out what type of result you got in
25+
# a forward compatible way.
26+
# When moving we should use the _BaseResultMixin as a base class for all
27+
# results.
28+
@dataclasses.dataclass
29+
class Result(_BaseResultMixin):
30+
"""Power distribution result."""
31+
32+
33+
@dataclasses.dataclass
34+
class _BaseSuccessMixin:
35+
"""Result returned when setting the power succeed for all batteries."""
36+
37+
succeeded_power: int
38+
"""The part of the requested power that was successfully set."""
39+
40+
succeeded_batteries: set[int]
41+
"""The subset of batteries for which power was set successfully."""
42+
43+
excess_power: int
44+
"""The part of the requested power that could not be fulfilled.
45+
46+
This happens when the requested power is outside the available power bounds.
47+
"""
8548

86-
class Error(Result):
87-
"""Error occurred and power was not set."""
8849

89-
def __init__(self, request: Request, msg: str) -> None:
90-
"""Create class instance.
50+
# We need to put the _BaseSuccessMixin before Result in the inheritance list to
51+
# make sure that the Result attributes appear before the _BaseSuccessMixin,
52+
# otherwise the request attribute will be last in the dataclass constructor
53+
# because of how MRO works.
9154

92-
Args:
93-
request: The user's request to which this message responds.
94-
msg: Error message explaining why error happened.
95-
"""
96-
super().__init__(request)
97-
self.msg: str = msg
9855

56+
@dataclasses.dataclass
57+
class Success(_BaseSuccessMixin, Result): # Order matters here. See above.
58+
"""Result returned when setting the power succeed for all batteries."""
9959

60+
61+
@dataclasses.dataclass
62+
class PartialFailure(_BaseSuccessMixin, Result):
63+
"""Result returned when any battery failed to perform the request."""
64+
65+
failed_power: int
66+
"""The part of the requested power that failed to be set."""
67+
68+
failed_batteries: set[int]
69+
"""The subset of batteries for which the request failed."""
70+
71+
72+
@dataclasses.dataclass
73+
class Error(Result):
74+
"""Result returned when an error occurred and power was not set at all."""
75+
76+
msg: str
77+
"""The error message explaining why error happened."""
78+
79+
80+
@dataclasses.dataclass
10081
class OutOfBound(Result):
101-
"""Send if power was not set because requested power was not within bounds.
82+
"""Result returned when the power was not set because it was out of bounds.
10283
103-
This message is send only if Request.adjust_power = False.
84+
This result happens when the originating request was done with
85+
`adjust_power = False` and the requested power is not within the batteries bounds.
10486
"""
10587

106-
def __init__(self, request: Request, bound: int) -> None:
107-
"""Create class instance.
88+
bound: int
89+
"""The total power bound for the requested batteries.
10890
109-
Args:
110-
request: The user's request to which this message responds.
111-
bound: Total power bound for the requested batteries.
112-
If requested power < 0, then this value is lower bound.
113-
Otherwise it is upper bound.
114-
"""
115-
super().__init__(request)
116-
self.bound: int = bound
91+
If the requested power negative, then this value is the lower bound.
92+
Otherwise it is upper bound.
93+
"""
11794

11895

96+
@dataclasses.dataclass
11997
class Ignored(Result):
120-
"""Send if request was ignored.
98+
"""Result returned when the request was ignored.
12199
122-
Request was ignored because new request for the same subset of batteries
123-
was received.
100+
The request can be ignored when a new request for the same subset of
101+
batteries was received.
124102
"""

0 commit comments

Comments
 (0)