Skip to content

Commit dec98e1

Browse files
authored
Clean up public API (#55)
This PR hides all internal modules by prepending a `_` to the module name. It also expose anything that is not a channel (or core channel classes) in a `util` package. So this is how classes are exposed: * `frequenz.channels.Anycast` * `frequenz.channels.Broadcast` * `frequenz.channels.Anycast` * `frequenz.channels.Bidirectional` * `frequenz.channels.Broadcast` * `frequenz.channels.Peekable` * `frequenz.channels.Receiver` * `frequenz.channels.Sender` * `frequenz.channels.util.Merge` * `frequenz.channels.util.MergeNamed` * `frequenz.channels.util.FileWatcher` * `frequenz.channels.util.Select` * `frequenz.channels.util.Timer` This also completely removes `BufferedReceiver` and moves `BidirectionalHandle` to `Bidirectional.Handle` and `EventType` to `FileWatcher.EventType`. While at it, add also a short summary of each module contents.
2 parents 7fb7cd8 + 951b28f commit dec98e1

24 files changed

+301
-189
lines changed

.github/labeler.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@
3030
"part:channels":
3131
- any:
3232
- "src/frequenz/channels/**"
33-
- "!src/frequenz/channels/select.py"
3433
- "!src/frequenz/channels/util/**"
3534

3635
"part:receivers":
37-
- "src/frequenz/channels/util/**"
36+
- any:
37+
- "src/frequenz/channels/util/**"
38+
- "!src/frequenz/channels/util/_select.py"
3839

3940
"part:select":
40-
- "src/frequenz/channels/select.py"
41+
- "src/frequenz/channels/util/_select.py"

RELEASE_NOTES.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,33 @@ time.
1818
`new_receiver()` and `new_sender()` respectively. This is to make it more
1919
clear that new objects are being created.
2020

21+
* The public API surface has been reduced considerably to make it more clear
22+
where to import symbols. You should update your imports. The new symbol
23+
locations are:
24+
25+
* `frequenz.channels.Anycast`
26+
* `frequenz.channels.Broadcast`
27+
* `frequenz.channels.Anycast`
28+
* `frequenz.channels.Bidirectional`
29+
* `frequenz.channels.Broadcast`
30+
* `frequenz.channels.Peekable`
31+
* `frequenz.channels.Receiver`
32+
* `frequenz.channels.Sender`
33+
* `frequenz.channels.util.Merge`
34+
* `frequenz.channels.util.MergeNamed`
35+
* `frequenz.channels.util.FileWatcher`
36+
* `frequenz.channels.util.Select`
37+
* `frequenz.channels.util.Timer`
38+
39+
* The class `BufferedReceiver` was removed because the interface was really
40+
intended for channel implementations. Users are not supposed to enqueue
41+
messages to receiver but just receive from them. If you used it you can
42+
implement it yourself.
43+
44+
* The class `BidirectionalHandle` was moved to `Bidirectional.Handle`.
45+
46+
* The class `EventType` was moved to `FileWatcher.EventType`.
47+
2148
## New Features
2249

2350
<!-- Here goes the main new features and examples or instructions on how to use them -->

docs/mkdocstrings_autoapi.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,30 @@
88
"""
99

1010
from pathlib import Path
11+
from typing import Tuple
1112

1213
import mkdocs_gen_files
1314

1415
SRC_PATH = "src"
1516
DST_PATH = "reference"
1617

18+
19+
def is_internal(path_parts: Tuple[str, ...]) -> bool:
20+
"""Tell if the path is internal judging by the parts.
21+
22+
Args:
23+
path_parts: Path.parts of the path to check.
24+
25+
Returns:
26+
True if the path is internal.
27+
"""
28+
29+
def with_underscore_not_init(part: str) -> bool:
30+
return part.startswith("_") and part != "__init__"
31+
32+
return any(p for p in path_parts if with_underscore_not_init(p))
33+
34+
1735
# type ignore because mkdocs_gen_files uses a very weird module-level
1836
# __getattr__() which messes up the type system
1937
nav = mkdocs_gen_files.Nav() # type: ignore
@@ -24,15 +42,17 @@
2442
doc_path = path.relative_to(SRC_PATH).with_suffix(".md")
2543
full_doc_path = Path(DST_PATH, doc_path)
2644
parts = tuple(module_path.parts)
45+
if is_internal(parts):
46+
continue
2747
if parts[-1] == "__init__":
2848
doc_path = doc_path.with_name("index.md")
2949
full_doc_path = full_doc_path.with_name("index.md")
3050
parts = parts[:-1]
3151

3252
nav[parts] = doc_path.as_posix()
3353

34-
with mkdocs_gen_files.open(full_doc_path, "w") as fd:
35-
fd.write(f"::: {'.'.join(parts)}\n")
54+
with mkdocs_gen_files.open(full_doc_path, "w") as output_file:
55+
output_file.write(f"::: {'.'.join(parts)}\n")
3656

3757
mkdocs_gen_files.set_edit_path(full_doc_path, Path("..") / path)
3858

src/frequenz/channels/__init__.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,64 @@
11
# License: MIT
22
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
33

4-
"""Channel implementations."""
5-
6-
from frequenz.channels.anycast import Anycast
7-
from frequenz.channels.base_classes import BufferedReceiver, Peekable, Receiver, Sender
8-
from frequenz.channels.bidirectional import Bidirectional, BidirectionalHandle
9-
from frequenz.channels.broadcast import Broadcast
10-
from frequenz.channels.merge import Merge
11-
from frequenz.channels.merge_named import MergeNamed
12-
from frequenz.channels.select import Select
13-
from frequenz.channels.utils.file_watcher import FileWatcher
14-
from frequenz.channels.utils.timer import Timer
4+
"""Frequenz Channels.
5+
6+
This package contains
7+
[channel](https://en.wikipedia.org/wiki/Channel_(programming)) implementations.
8+
9+
Channels:
10+
11+
* [Anycast][frequenz.channels.Anycast]: A channel that supports multiple
12+
senders and multiple receivers. A message sent through a sender will be
13+
received by exactly one receiver.
14+
15+
* [Bidirectional][frequenz.channels.Bidirectional]: A channel providing
16+
a `client` and a `service` handle to send and receive bidirectionally.
17+
18+
* [Broadcast][frequenz.channels.Broadcast]: A channel to broadcast messages
19+
from multiple senders to multiple receivers. Each message sent through any of
20+
the senders is received by all of the receivers.
21+
22+
Other base classes:
23+
24+
* [Peekable][frequenz.channels.Peekable]: An object to allow users to get
25+
a peek at the latest value in the channel, without consuming anything.
26+
27+
* [Receiver][frequenz.channels.Receiver]: An object that can wait for and
28+
consume messages from a channel.
29+
30+
* [Sender][frequenz.channels.Sender]: An object that can send messages to
31+
a channel.
32+
33+
Utilities:
34+
35+
* [util][frequenz.channels.util]: A module with utilities, like special
36+
receivers that implement timers, file watchers, merge receivers, or wait for
37+
messages in multiple channels.
38+
39+
Exception classes:
40+
41+
* [ChannelError][frequenz.channels.ChannelError]: Base class for all errors
42+
related to channels.
43+
44+
* [ChannelClosedError][frequenz.channels.ChannelClosedError]: Error raised when
45+
trying to operate (send, receive, etc.) through a closed channel.
46+
"""
47+
48+
from . import util
49+
from ._anycast import Anycast
50+
from ._base_classes import ChannelClosedError, ChannelError, Peekable, Receiver, Sender
51+
from ._bidirectional import Bidirectional
52+
from ._broadcast import Broadcast
1553

1654
__all__ = [
1755
"Anycast",
1856
"Bidirectional",
19-
"BidirectionalHandle",
2057
"Broadcast",
21-
"BufferedReceiver",
22-
"FileWatcher",
23-
"Merge",
24-
"MergeNamed",
58+
"ChannelClosedError",
59+
"ChannelError",
2560
"Peekable",
2661
"Receiver",
27-
"Select",
2862
"Sender",
29-
"Timer",
63+
"util",
3064
]
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
from collections import deque
1010
from typing import Deque, Generic, Optional
1111

12-
from frequenz.channels.base_classes import ChannelClosedError
13-
from frequenz.channels.base_classes import Receiver as BaseReceiver
14-
from frequenz.channels.base_classes import Sender as BaseSender
15-
from frequenz.channels.base_classes import T
12+
from ._base_classes import ChannelClosedError
13+
from ._base_classes import Receiver as BaseReceiver
14+
from ._base_classes import Sender as BaseSender
15+
from ._base_classes import T
1616

1717

1818
class Anycast(Generic[T]):
@@ -28,9 +28,9 @@ class Anycast(Generic[T]):
2828
thread-safe.
2929
3030
When there are multiple channel receivers, they can be awaited
31-
simultaneously using [Select][frequenz.channels.Select],
32-
[Merge][frequenz.channels.Merge] or
33-
[MergeNamed][frequenz.channels.MergeNamed].
31+
simultaneously using [Select][frequenz.channels.util.Select],
32+
[Merge][frequenz.channels.util.Merge] or
33+
[MergeNamed][frequenz.channels.util.MergeNamed].
3434
3535
Example:
3636
``` python
Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -164,18 +164,6 @@ def peek(self) -> Optional[T]:
164164
"""
165165

166166

167-
class BufferedReceiver(Receiver[T]):
168-
"""A channel receiver with a buffer."""
169-
170-
@abstractmethod
171-
def enqueue(self, msg: T) -> None:
172-
"""Put a message into this buffered receiver's queue.
173-
174-
Args:
175-
msg: The message to be added to the queue.
176-
"""
177-
178-
179167
class _Map(Receiver[U], Generic[T, U]):
180168
"""Apply a transform function on a channel receiver.
181169
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""An abstraction to provide bi-directional communication between actors."""
5+
6+
from __future__ import annotations
7+
8+
from typing import Generic, TypeVar
9+
10+
from ._base_classes import Receiver, Sender, T, U
11+
from ._broadcast import Broadcast
12+
13+
V = TypeVar("V")
14+
W = TypeVar("W")
15+
16+
17+
class Bidirectional(Generic[T, U]):
18+
"""A wrapper class for simulating bidirectional channels."""
19+
20+
class Handle(Sender[V], Receiver[W]):
21+
"""A handle to a [Bidirectional][frequenz.channels.Bidirectional] instance.
22+
23+
It can be used to send/receive values between the client and service.
24+
"""
25+
26+
def __init__(self, sender: Sender[V], receiver: Receiver[W]) -> None:
27+
"""Create a `Bidirectional.Handle` instance.
28+
29+
Args:
30+
sender: A sender to send values with.
31+
receiver: A receiver to receive values from.
32+
"""
33+
self._sender = sender
34+
self._receiver = receiver
35+
36+
async def send(self, msg: V) -> bool:
37+
"""Send a value to the other side.
38+
39+
Args:
40+
msg: The value to send.
41+
42+
Returns:
43+
Whether the send was successful or not.
44+
"""
45+
return await self._sender.send(msg)
46+
47+
async def ready(self) -> None:
48+
"""Wait until the receiver is ready with a value."""
49+
await self._receiver.ready() # pylint: disable=protected-access
50+
51+
def consume(self) -> W:
52+
"""Return the latest value once `_ready` is complete.
53+
54+
Returns:
55+
The next value that was received.
56+
"""
57+
return self._receiver.consume() # pylint: disable=protected-access
58+
59+
def __init__(self, client_id: str, service_id: str) -> None:
60+
"""Create a `Bidirectional` instance.
61+
62+
Args:
63+
client_id: A name for the client, used to name the channels.
64+
service_id: A name for the service end of the channels.
65+
"""
66+
self._client_id = client_id
67+
self._request_channel: Broadcast[T] = Broadcast(f"req_{service_id}_{client_id}")
68+
self._response_channel: Broadcast[U] = Broadcast(
69+
f"resp_{service_id}_{client_id}"
70+
)
71+
72+
self._client_handle = Bidirectional.Handle(
73+
self._request_channel.new_sender(),
74+
self._response_channel.new_receiver(),
75+
)
76+
self._service_handle = Bidirectional.Handle(
77+
self._response_channel.new_sender(),
78+
self._request_channel.new_receiver(),
79+
)
80+
81+
@property
82+
def client_handle(self) -> Bidirectional.Handle[T, U]:
83+
"""Get a `Handle` for the client side to use.
84+
85+
Returns:
86+
Object to send/receive messages with.
87+
"""
88+
return self._client_handle
89+
90+
@property
91+
def service_handle(self) -> Bidirectional.Handle[U, T]:
92+
"""Get a `Handle` for the service side to use.
93+
94+
Returns:
95+
Object to send/receive messages with.
96+
"""
97+
return self._service_handle
Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
from typing import Deque, Dict, Generic, Optional
1313
from uuid import UUID, uuid4
1414

15-
from frequenz.channels.base_classes import BufferedReceiver, ChannelClosedError
16-
from frequenz.channels.base_classes import Peekable as BasePeekable
17-
from frequenz.channels.base_classes import Sender as BaseSender
18-
from frequenz.channels.base_classes import T
15+
from ._base_classes import ChannelClosedError
16+
from ._base_classes import Peekable as BasePeekable
17+
from ._base_classes import Receiver as BaseReceiver
18+
from ._base_classes import Sender as BaseSender
19+
from ._base_classes import T
1920

2021
logger = logging.Logger(__name__)
2122

@@ -32,9 +33,9 @@ class Broadcast(Generic[T]):
3233
are thread-safe. Because of this, `Broadcast` channels are thread-safe.
3334
3435
When there are multiple channel receivers, they can be awaited
35-
simultaneously using [Select][frequenz.channels.Select],
36-
[Merge][frequenz.channels.Merge] or
37-
[MergeNamed][frequenz.channels.MergeNamed].
36+
simultaneously using [Select][frequenz.channels.util.Select],
37+
[Merge][frequenz.channels.util.Merge] or
38+
[MergeNamed][frequenz.channels.util.MergeNamed].
3839
3940
Example:
4041
``` python
@@ -192,7 +193,7 @@ async def send(self, msg: T) -> bool:
192193
return True
193194

194195

195-
class Receiver(BufferedReceiver[T]):
196+
class Receiver(BaseReceiver[T]):
196197
"""A receiver to receive messages from the broadcast channel.
197198
198199
Should not be created directly, but through the

0 commit comments

Comments
 (0)