|
2 | 2 |
|
3 | 3 | ## Summary |
4 | 4 |
|
5 | | -<!-- Here goes a general summary of what this release is about --> |
| 5 | +The minimum Python supported version was bumped to 3.11 and the `Select` class replaced by the new `Selector`. |
6 | 6 |
|
7 | 7 | ## Upgrading |
8 | 8 |
|
9 | 9 | * The minimum supported Python version was bumped to 3.11, downstream projects will need to upgrade too to use this version. |
10 | 10 |
|
| 11 | +* The `Select` class was replaced by a new `Selector` class, with the following improvements: |
| 12 | + |
| 13 | + * Type-safe: proper type hinting by using the new helper type guard `selected_from()`. |
| 14 | + * Fixes potential starvation issues. |
| 15 | + * Simplifies the interface by providing values one-by-one. |
| 16 | + * Guarantees there are no dangling tasks left behind when used as an async context manager. |
| 17 | + |
| 18 | + This new class acts as an [async context manager](https://docs.python.org/3/reference/datamodel.html#async-context-managers) and an [async iterator](https://docs.python.org/3.11/library/collections.abc.html#collections.abc.AsyncIterator), and makes sure no dangling tasks are left behind after a select loop is done. |
| 19 | + |
| 20 | + Example: |
| 21 | + ```python |
| 22 | + timer1 = Timer.periodic(datetime.timedelta(seconds=1)) |
| 23 | + timer2 = Timer.timeout(datetime.timedelta(seconds=0.5)) |
| 24 | + |
| 25 | + async with Selector(timer1, timer2) as selector: |
| 26 | + async for selected in selector: |
| 27 | + if selected_from(selected, timer1): |
| 28 | + # Beware: `selected.value` might raise an exception, you can always |
| 29 | + # check for exceptions with `selected.exception` first or use |
| 30 | + # a try-except block. You can also quickly check if the receiver was |
| 31 | + # stopped and let any other unexpected exceptions bubble up. |
| 32 | + if selected.was_stopped(): |
| 33 | + print("timer1 was stopped") |
| 34 | + continue |
| 35 | + print(f"timer1: now={datetime.datetime.now()} drift={selected.value}") |
| 36 | + timer2.stop() |
| 37 | + elif selected_from(selected, timer2): |
| 38 | + # Explicitly handling of exceptions |
| 39 | + match selected.exception: |
| 40 | + case ReceiverStoppedError(): |
| 41 | + print("timer2 was stopped") |
| 42 | + case Exception() as exception: |
| 43 | + print(f"timer2: exception={exception}") |
| 44 | + case None: |
| 45 | + # All good, no exception, we can use `selected.value` safely |
| 46 | + print( |
| 47 | + f"timer2: now={datetime.datetime.now()} " |
| 48 | + f"drift={selected.value}" |
| 49 | + ) |
| 50 | + case _ as unhanded: |
| 51 | + assert_never(unhanded) |
| 52 | + else: |
| 53 | + # This is not necessary, as select() will check for exhaustiveness, but |
| 54 | + # it is good practice to have it in case you forgot to handle a new |
| 55 | + # receiver added to `select()` at a later point in time. |
| 56 | + assert False |
| 57 | + ``` |
| 58 | + |
11 | 59 | ## New Features |
12 | 60 |
|
13 | | -<!-- Here goes the main new features and examples or instructions on how to use them --> |
| 61 | +* A new `Selector` class was added, please look at the *Upgrading* section for details. |
| 62 | + |
| 63 | +* A new `Event` utility receiver was added. |
| 64 | + |
| 65 | + This receiver can be made ready manually. It is mainly useful for testing but can also become handy in scenarios where a simple, on-off signal needs to be sent to a select loop for example. |
| 66 | + |
| 67 | + Example: |
| 68 | + |
| 69 | + ```python |
| 70 | + import asyncio |
| 71 | + from frequenz.channels import Receiver |
| 72 | + from frequenz.channels.util import Event, select, selected_from |
| 73 | + |
| 74 | + other_receiver: Receiver[int] = ... |
| 75 | + exit_event = Event() |
| 76 | + |
| 77 | + async def exit_after_10_seconds() -> None: |
| 78 | + asyncio.sleep(10) |
| 79 | + exit_event.set() |
| 80 | + |
| 81 | + asyncio.ensure_future(exit_after_10_seconds()) |
14 | 82 |
|
15 | | -## Bug Fixes |
| 83 | + async with Selector(exit_event, other_receiver) as selector: |
| 84 | + async for selected in selector: |
| 85 | + if selected_from(selected, exit_event): |
| 86 | + break |
| 87 | + if selected_from(selected, other_receiver): |
| 88 | + print(selected.value) |
| 89 | + else: |
| 90 | + assert False, "Unknow receiver selected" |
| 91 | + ``` |
16 | 92 |
|
17 | | -<!-- Here goes notable bug fixes that are worth a special mention or explanation --> |
| 93 | +* The `Timer` class now has more descriptive `__str__` and `__repr__` methods. |
0 commit comments