forked from frequenz-floss/frequenz-channels-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_oneshot.py
More file actions
142 lines (102 loc) · 4.23 KB
/
Copy path_oneshot.py
File metadata and controls
142 lines (102 loc) · 4.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# License: MIT
# Copyright © 2026 Frequenz Energy-as-a-Service GmbH
"""A channel that can send a single message."""
from __future__ import annotations
import asyncio
import typing
from ._generic import ChannelMessageT
from ._receiver import Receiver, ReceiverStoppedError
from ._sender import Sender, SenderClosedError
class _Empty:
"""A sentinel indicating that no message has been sent."""
_EMPTY = _Empty()
class _Oneshot(typing.Generic[ChannelMessageT]):
"""Internal representation of a one-shot channel.
A one-shot channel is a channel that can only send one message. After the first
message is sent, the sender is closed and any further attempts to send a message
will raise a `SenderClosedError`.
"""
def __init__(self) -> None:
"""Create a new one-shot channel."""
self.message: ChannelMessageT | _Empty = _EMPTY
self.closed: bool = False
self.drained: bool = False
self.event: asyncio.Event = asyncio.Event()
class OneshotSender(Sender[ChannelMessageT]):
"""A sender for a one-shot channel."""
def __init__(self, channel: _Oneshot[ChannelMessageT]) -> None:
"""Initialize this sender."""
self._channel = channel
async def send(self, message: ChannelMessageT, /) -> None:
"""Send a message through this sender."""
if self._channel.closed:
raise SenderClosedError(self)
self._channel.message = message
self._channel.closed = True
self._channel.event.set()
async def aclose(self) -> None:
"""Close this sender."""
self._channel.closed = True
if isinstance(self._channel.message, _Empty):
self._channel.drained = True
self._channel.event.set()
class OneshotReceiver(Receiver[ChannelMessageT]):
"""A receiver for a one-shot channel."""
def __init__(self, channel: _Oneshot[ChannelMessageT]) -> None:
"""Initialize this receiver."""
self._channel = channel
async def ready(self) -> bool:
"""Check if a message is ready to be received.
Returns:
`True` if a message is ready to be received, `False` if the sender
is closed and no message will be sent.
"""
if self._channel.drained:
return False
while not self._channel.closed:
await self._channel.event.wait()
if isinstance(self._channel.message, _Empty):
return False
return True
def consume(self) -> ChannelMessageT:
"""Consume a message from this receiver.
Returns:
The message that was sent through this channel.
Raises:
ReceiverStoppedError: If the sender was closed without sending a message.
"""
if self._channel.drained:
raise ReceiverStoppedError(self)
assert not isinstance(
self._channel.message, _Empty
), "`consume()` must be preceded by a call to `ready()`."
self._channel.drained = True
self._channel.event.clear()
return self._channel.message
class OneshotChannel(
tuple[OneshotSender[ChannelMessageT], OneshotReceiver[ChannelMessageT]]
):
"""A channel that can send a single message.
A one-shot channel is a channel that can only send one message. After the first
message is sent, the sender is closed and any further attempts to send a message
will raise a `SenderClosedError`.
# Example
This example demonstrates how to use a one-shot channel to send a message
from one task to another.
```python
import asyncio
from frequenz.channels import OneshotChannel, OneshotSender
async def send(sender: OneshotSender[int]) -> None:
await sender.send(42)
async def main() -> None:
sender, receiver = OneshotChannel[int]()
async with asyncio.TaskGroup() as tg:
tg.create_task(send(sender))
assert await receiver.receive() == 42
asyncio.run(main())
```
"""
def __new__(cls) -> OneshotChannel[ChannelMessageT]:
"""Create a new one-shot channel."""
channel = _Oneshot[ChannelMessageT]()
return tuple.__new__(cls, (OneshotSender(channel), OneshotReceiver(channel)))