-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Power Manager _Report
, Report
and BatteryPoolReport
have the wrong hierarchy
#823
Comments
FYI @shsms |
This has nothing to do with the use of the This can be resolved with: frequenz-floss/frequenz-channels-python#262 |
Actually it needs this too, once the channel PR is merged: diff --git a/src/frequenz/sdk/_internal/_channels.py b/src/frequenz/sdk/_internal/_channels.py
index d739470d..d3b5a68a 100644
--- a/src/frequenz/sdk/_internal/_channels.py
+++ b/src/frequenz/sdk/_internal/_channels.py
@@ -9,14 +9,14 @@ import typing
from frequenz.channels import Receiver
-T = typing.TypeVar("T")
+_T_co = typing.TypeVar("_T_co", covariant=True)
-class ReceiverFetcher(typing.Generic[T], typing.Protocol):
+class ReceiverFetcher(typing.Protocol[_T_co]):
"""An interface that just exposes a `new_receiver` method."""
@abc.abstractmethod
- def new_receiver(self, *, maxsize: int = 50) -> Receiver[T]:
+ def new_receiver(self, *, maxsize: int = 50) -> Receiver[_T_co]:
"""Get a receiver from the channel.
Args:
@@ -27,21 +27,21 @@ class ReceiverFetcher(typing.Generic[T], typing.Protocol):
"""
-class LatestValueCache(typing.Generic[T]):
+class LatestValueCache(typing.Generic[_T_co]):
"""A cache that stores the latest value in a receiver."""
- def __init__(self, receiver: Receiver[T]) -> None:
+ def __init__(self, receiver: Receiver[_T_co]) -> None:
"""Create a new cache.
Args:
receiver: The receiver to cache.
"""
self._receiver = receiver
- self._latest_value: T | None = None
+ self._latest_value: _T_co | None = None
self._task = asyncio.create_task(self._run())
@property
- def latest_value(self) -> T | None:
+ def latest_value(self) -> _T_co | None:
"""Get the latest value in the cache."""
return self._latest_value |
I still don't see how this fixes this particular issue. Here This is why I think in this case our best option is to use type unions instead. We don't really need a hierarchy at all, we want battery pools to return receivers for Maybe I am misunderstanding the requirements because I'm not super familiar with how the power manager works... |
There was no |
I don't get that at all. If there is no As shown in the issue description, the hierarchy is currently: class Report(typing.Protocol):
...
class _Report(Report):
...
class BatteryPoolReport(Report):
...
channel = Broadcast[_Report]() I still don't see how can I legally get a |
Because that's what protocols allow, and that is the reason they exist.
You can think of them as an implicit ABC from the eyes of the type checker, that we are able to declare after the fact. |
Ok, this is the new part, right now the protocol class is Ok, now i see what you mean. I still think this is not the typical use case for class BatteryPoolReport(Protocol):
...
class _Report:
... Again, I think this is not intuitive (I use myself as a proof of it 😂) and still wonder what do you have against: class BatteryPoolReport:
...
Report = BatteryPoolReport | ...
...
channel = Broadcast[Report]() This is because the producing end of these reports don't know anything about which type of pool is producing reports for? |
The channel registry is using `Any` as the message type, which is not type safe, as it completely *disables* type checking for the messages. This commit makes the channel registry type-aware, so channels are stored with their message type and the registry checks that the same channel is not used for different message types. This also makes the registry just a plain container for channels, the wrapper methods to create new senders and receivers, and to configure the `resend_latest` flag are removed. The `ReceiverFetcher` abstraction is also not needed if we just return the channel directly, as the channel itself is a `ReceiverFetcher`. Also the method to close and remove a channel is made public and the name more explicit, as it is used in normal code paths. The new registry only provide 2 main methods: * `get_or_create()`: Get or create a channel for the given key, doing the type checking to make sure the requested message type matches the existing channel message type if it already exists. * `close_and_remove()`: Close and remove the channel for the given key. This change uncovered 5 issues: * `MockMigrogrid/Resampler: Fail on unhandled exceptions` (in this PR, but more generally #826) * `Fix checks in tests` (in this PR) * `Fix missing conversion to `QuantityT` (workaroud in this PR, but should be better solved by #821) * #807 * #823 Fixes #806.
BTW, if we are using Protocol I am surprised we need covariance. I thought covariance only applied to inheritance, but maybe is about sub-typing more generally and |
yup, it is because we don't want to add another async layer and additional allocation to rewrap the Also I think this is a perfect use case for Protocol, it might not be typical but that's also most things in the SDK. |
of course, that's how inheritance works in Python, but in hindsight, I shouldn't have agreed to mix inheritance with Protocol, because it does make the whole thing murky and opaque. |
Yeah, the argument for that was to make it more explicit that a class is implementing a protocol, and the nice side effect is that mypy will complain if the protocol changes but the downstream class "implementing" it is not. I miss the unintended side effect of making the downstream class a protocol too. |
this happens also when there is no common base class. |
Not exactly, at least IIRC the error just says that class X is not compatible with protocol Y, it doesn't say in which way, what's missing or incompatible. |
$ cat p.py
from typing import Protocol
class P(Protocol):
def f(self) -> None:
...
class C:
def g(self) -> None:
pass
def h(p: P) -> None:
pass
h(C())
$ mypy p.py
p.py:14: error: Argument 1 to "h" has incompatible type "C"; expected "P" [arg-type]
Found 1 error in 1 file (checked 1 source file) |
_Report
, Report
and BatteryPoolReport
have the wrong hierarchy_Report
, Report
and *PoolReport
have the wrong hierarchy
_Report
, Report
and *PoolReport
have the wrong hierarchy_Report
, Report
and BatteryPoolReport
have the wrong hierarchy
So now the Footnotes |
|
What happened?
The current hierarchy is:
The problem is
BatteryPoolReport
is a_Report
, and_Report
is the concrete class sent through channels, so there are instances when a_Report
needs to be converted to aBatteryPoolReport
, for which we just need to go behind the back of the type system.What did you expect instead?
If I have a
Receiver[_Report]
, I expect to be able to safely return aBatteryPoolReport
from it without any casting.Affected version(s)
No response
Affected part(s)
Battery power distribution (part:power-distribution)
Extra information
So, the naive solution is:
But of course this doesn't work in the whole context of
Report
. In the current code, there are no otherReport
s that are notBatteryPoolReport
, so I'm not sure how the end use of this hierarchy should look like. If it is just documentation, I'm not sure it is worth hiding_Report
, I think it would be simpler to just keep it generic and then have something like:Note
When applied to different components, the attributes of this class have different meanings.
BatteryPool
: component IDs are battery IDs. Yada yada yadaAlso I wonder if we can solve this by using completely different types for different
Report
types, that have no inheritance relationship, are all concrete classes and instead makeReport = BatteryPoolReport | PvPoolReport | ...
. Then we can use thematch
syntax to know what we received from the channel.I guess we also need to mix all these different reports in the same channel because we are going to use the same power manager for all, because ideally we should just use one concrete type per channel and then we don't need the hierarchy or the type alias at all.
The text was updated successfully, but these errors were encountered: