Skip to content

Releases: frequenz-floss/frequenz-channels-python

v0.16.0

30 Jun 15:14
v0.16.0
19487b8
Compare
Choose a tag to compare

Frequenz Channels Release Notes

Summary

The minimum Python supported version was bumped to 3.11 and the Select class replaced by the new select() function.

Upgrading

  • The minimum supported Python version was bumped to 3.11, downstream projects will need to upgrade too to use this version.

  • The Select class was replaced by a new select() function, with the following improvements:

    • Type-safe: proper type hinting by using the new helper type guard selected_from().
    • Fixes potential starvation issues.
    • Simplifies the interface by providing values one-by-one.
    • Guarantees there are no dangling tasks left behind when used as an async context manager.

    This new function is an async iterator, and makes sure no dangling tasks are left behind after a select loop is done.

    Example:

    timer1 = Timer.periodic(datetime.timedelta(seconds=1))
    timer2 = Timer.timeout(datetime.timedelta(seconds=0.5))
    
    async for selected in select(timer1, timer2):
        if selected_from(selected, timer1):
            # Beware: `selected.value` might raise an exception, you can always
            # check for exceptions with `selected.exception` first or use
            # a try-except block. You can also quickly check if the receiver was
            # stopped and let any other unexpected exceptions bubble up.
            if selected.was_stopped():
                print("timer1 was stopped")
                continue
            print(f"timer1: now={datetime.datetime.now()} drift={selected.value}")
            timer2.stop()
        elif selected_from(selected, timer2):
            # Explicitly handling of exceptions
            match selected.exception:
                case ReceiverStoppedError():
                    print("timer2 was stopped")
                case Exception() as exception:
                    print(f"timer2: exception={exception}")
                case None:
                    # All good, no exception, we can use `selected.value` safely
                    print(
                        f"timer2: now={datetime.datetime.now()} "
                        f"drift={selected.value}"
                    )
                case _ as unhanded:
                    assert_never(unhanded)
        else:
            # This is not necessary, as select() will check for exhaustiveness, but
            # it is good practice to have it in case you forgot to handle a new
            # receiver added to `select()` at a later point in time.
            assert False

New Features

  • A new select() function was added, please look at the Upgrading section for details.

  • A new Event utility receiver was added.

    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.

    Example:

    import asyncio
    from frequenz.channels import Receiver
    from frequenz.channels.util import Event, select, selected_from
    
    other_receiver: Receiver[int] = ...
    exit_event = Event()
    
    async def exit_after_10_seconds() -> None:
        asyncio.sleep(10)
        exit_event.set()
    
    asyncio.ensure_future(exit_after_10_seconds())
    
    async for selected in select(exit_event, other_receiver):
        if selected_from(selected, exit_event):
            break
        if selected_from(selected, other_receiver):
            print(selected.value)
        else:
            assert False, "Unknow receiver selected"
  • The Timer class now has more descriptive __str__ and __repr__ methods.

What's Changed

New Contributors

Full Changelog: v0.15.1...v0.16.0

v0.15.1

23 May 14:23
v0.15.1
e23335b
Compare
Choose a tag to compare

Frequenz Channels Release Notes

Summary

This is a bugfix release that mainly uses more up to date dependencies and extend the range of supported dependencies. There is technically one breaking change though, but this is hardly used by anyone.

Upgrading

  • FileWatcher no longer accepts or sets None as the event_types argument. Instead, all available event types are now set by default while still providing the flexibility to customize the event types as needed.

Bug Fixes

  • Many documentation examples were fixed.

What's Changed

New Contributors

Full Changelog: v0.15.0...v0.15.1

v0.15.0

16 May 09:54
v0.15.0
7fd8e46
Compare
Choose a tag to compare

Frequenz Channels Release Notes

Summary

This release adds support to pass None values via channels and revamps the Timer class to support custom policies for handling missed ticks and use the loop monotonic clock. There is also a fix for the FileWatcher which includes a change in behavior when reporting changes for deleted files.

Upgrading

  • util.Timer was replaced by a more generic implementation that allows for customizable policies to handle missed ticks.

    If you were using Timer to implement timeouts, these two pieces of code should be almost equivalent:

    • Old:

      old_timer = Timer(1.0)
      triggered_datetime = old_timer.receive()
    • New:

      new_timer = Timer.timeout(timedelta(seconds=1.0))
      drift = new_timer.receive()
      triggered_datetime = datetime.now(timezone.utc) - drift

    They are not exactly the same because the triggered_datetime in the second case will not be exactly when the timer had triggered, but that shouldn't be relevant, the important part is when your code can actually react to the timer trigger and to know how much drift there was to be able to take corrective actions.

    Also the new Timer uses the asyncio loop monotonic clock and the old one used the wall clock (datetime.now()) to track time. This means that when using async-solipsism to test, the new Timer will always trigger immediately regardless of the state of the wall clock. This also means that we don't need to mock the wall clock with time-machine either now.

    With the previous timer one needed to create a separate task to run the timer, because otherwise it would block as it loops until the wall clock was at a specific time. Now the code will run like this:

    timer = Timer.timeout(timedelta(seconds=1.0))
    asyncio.sleep(0.5)  # Advances the loop monotonic clock by 0.5 seconds immediately
    await drift = timer.receive()  # Advances the clock by 0.5 immediately too
    assert drift == approx(timedelta(0))  # Because it could trigger exactly at the tick time
    
    # Simulates a delay in the timer trigger time
    asyncio.sleep(1.5)  # Advances the loop monotonic clock by 1.5 seconds immediately
    await drift = timer.receive()  # The timer should have triggered 0.5 seconds ago, so it doesn't even sleep
    assert drift == approx(timedelta(seconds=0.5))  # Now we should observe a drift of 0.5 seconds

    Note: Before replacing this code blindly in all uses of Timer.timeout(), please consider using the periodic timer constructor Timer.periodic() if you need a timer that triggers reliable on a periodic fashion, as the old Timer (and Timer.timeout()) accumulates drift, which might not be what you want.

  • FileWatcher now will emit events even if the file doesn't exist anymore.

    Because the underlying library has a considerable delay in triggering filesystem events, it can happen that, for example, a CREATE event is received but at the time of receiving the file doesn't exist anymore (because if was removed just after creation and before the event was triggered).

    Before the FileWatcher will only emit events when the file exists, but this doesn't work for DELETE events (clearly). Given the nature of this mechanism can lead to races easily, it is better to leave it to the user to decide when these situations happen and just report all events.

    Therefore, you should now check a file receiving an event really exist before trying to operate on it.

  • FileWatcher reports the type of event observed in addition to the file path.

    Previously, only the file path was reported. With this update, users can now determine if a file change is a creation, modification, or deletion.
    Note that this change may require updates to your existing code that relies on FileWatcher as the new interface returns a FileWatcher.Event instead of just the file path.

New Features

  • util.Timer was replaced by a more generic implementation that allows for customizable policies to handle missed ticks.

  • Passing None values via channels is now supported.

  • FileWatcher.Event was added to notify events when a file is created, modified, or deleted.

Bug Fixes

  • util.Select / util.Merge / util.MergeNamed: Cancel pending tasks in __del__ methods only if possible (the loop is not already closed).

  • FileWatcher will now report DELETE events correctly.

    Due to a bug, before this release DELETE events were only reported if the file was re-created before the event was triggered.

What's Changed

New Contributors

Full Changelog: v0.14.0...v0.15.0

v0.14.0

29 Mar 10:59
v0.14.0
6c164c4
Compare
Choose a tag to compare

Frequenz Channels Release Notes

Summary

The main change in this release is the revamp of exception handling in general. New exceptions were created and send() now raises an exception too when it fails.

Hopefully they are now used much more uniformly across the whole library.

Upgrading

  • The Sender.send() method now raises a SenderError instead of returning False. The SenderError will typically have a ChannelClosedError and the underlying reason as a chained exception.

  • The Receiver.ready() method (and related receive() and __anext__ when used as an async iterator) now raises a ReceiverError and in particular a ReceiverStoppedError when the receiver has no more messages to receive.

    Receiver.consume() doesn't raise any exceptions.

    Receivers raising EOFError now raise ReceiverInvalidatedError instead.

  • For channels which senders raise an error when the channel is closed or which receivers stop receiving when the channel is closed, the SenderError and ReceiverStoppedError are chained with a __cause__ that is a ChannelClosedError with the channel that was closed.

  • ChannelClosedError now requires the argument channel (before it was optional).

  • Now exceptions are not raised in Receiver.ready() but in Receiver.consume() (receive() or the async iterator anext).

New Features

  • New exceptions were added:

    • Error: A base exception from which all exceptions from this library inherit.

    • SendError: Raised for errors when sending messages.

    • ReceiverError: Raised for errors when receiving messages.

    • ReceiverClosedError: Raised when a receiver don't have more messages to receive.

    • ReceiverInvalidatedError: Raised when a receiver was invalidated (for example it was converted into a Peekable).

What's Changed

New Contributors

Full Changelog: v0.13.0...v0.14.0

v0.13.0

27 Jan 14:25
v0.13.0
d6d717b
Compare
Choose a tag to compare

Frequenz Channels Release Notes

New Features

  • Add method to stop Merge and MergeNamed.

What's Changed

Full Changelog: v0.12.0...v0.13.0

v0.12.0

23 Jan 14:19
v0.12.0
921a3a2
Compare
Choose a tag to compare

Frequenz Channels Release Notes

Summary

Upgrading

New Features

  • Add method to stop Select.

Bug Fixes

  • Deactivate Broadcast receivers that were transformed to peekable.

What's Changed

New Contributors

Full Changelog: v0.11.0...v0.12.0

v0.11.0

23 Nov 14:36
v0.11.0
61754af
Compare
Choose a tag to compare

Frequenz Channels Release Notes

Summary

The project has a new home!

https://frequenz-floss.github.io/frequenz-channels-python/

For now the documentation is pretty scarce but we will be improving it with time.

Upgrading (breaking changes)

  • You need to make sure to use timezone-aware datetime objects when using the timestamp returned by Timer, Otherwise you will get an exception.

  • Channels methods get_receiver() and get_sender() have been renamed to new_receiver() and new_sender() respectively. This is to make it more clear that new objects are being created.

  • The public API surface has been reduced considerably to make it more clear where to import symbols. You should update your imports. The new symbol locations are:

    • 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
  • The class BufferedReceiver was removed because the interface was really intended for channel implementations. Users are not supposed to enqueue messages to receiver but just receive from them. If you used it you can implement it yourself.

  • The class BidirectionalHandle was moved to Bidirectional.Handle.

  • The class EventType was moved to FileWatcher.EventType.

New Features

  • Python 3.11 is now supported!

Bug Fixes

  • Broadcast receivers now get cleaned up once they go out of scope.

  • Timer now returns timezone-aware datetime objects using UTC as timezone.

What's Changed

Full Changelog: v0.10.0...v0.11.0

v0.10.1

04 Nov 10:35
v0.10.1
a9e3d63
Compare
Choose a tag to compare

frequenz-channels Release Notes

Summary

This is the first public open source release based on the internal SDK version v0.9.0. There are no breaking changes in this release, only changes to the project structure, metadata, and automation. Packages are also now uploaded to PyPI as frequenz-channels, so this project now can be installed normally via pip:

python -m pip install frequenz-channels

The GitHub issues were also improved, adding templates for reporting issues and requesting features. Users are also pointed to the Discussion forums when trying to open an issue if they have questions instead. Also many labels are assigned automatically on issue and pull request creation.

What's Changed

Full Changelog: v0.10.0...v0.10.1

v0.10.0

23 Sep 13:04
v0.10.0
2f38f34
Compare
Choose a tag to compare

frequenz-channels Release Notes

Summary

This is the first public open source release based on the internal SDK version v0.9.0. There are no breaking changes in this release, only changes to the project structure, metadata, and automation. Packages are also now uploaded to PyPI as frequenz-channels, so this project now can be installed normally via pip:

python -m pip install frequenz-channels

The GitHub issues were also improved, adding templates for reporting issues and requesting features. Users are also pointed to the Discussion forums when trying to open an issue if they have questions instead. Also many labels are assigned automatically on issue and pull request creation.

What's Changed

New Contributors

Full Changelog: https://github.com/frequenz-floss/frequenz-channels-python/commits/v0.10.0

v0.10.0-rc0

23 Sep 13:01
v0.10.0-rc0
2f38f34
Compare
Choose a tag to compare
v0.10.0-rc0 Pre-release
Pre-release

frequenz-channels Release Notes

Summary

This is the first public open source release based on the internal SDK version v0.9.0. There are no breaking changes in this release, only changes to the project structure, metadata, and automation. Packages are also now uploaded to PyPI as frequenz-channels, so this project now can be installed normally via pip:

python -m pip install frequenz-channels

The GitHub issues were also improved, adding templates for reporting issues and requesting features. Users are also pointed to the Discussion forums when trying to open an issue if they have questions instead. Also many labels are assigned automatically on issue and pull request creation.

What's Changed

New Contributors

Full Changelog: https://github.com/frequenz-floss/frequenz-channels-python/commits/v0.10.0-rc0