Skip to content

Releases: frequenz-floss/frequenz-channels-python

v0.16.1

30 Nov 14:05
v0.16.1
54721a8
Compare
Choose a tag to compare

Frequenz Channels Release Notes

Bug Fixes

  • Timer: Fix bug that was causing calls to reset() to not reset the timer, if the timer was already being awaited.

What's Changed

Full Changelog: v0.16.0...v0.16.1

v1.0.0-beta.2

01 Nov 16:30
v1.0.0-beta.2
5d41385
Compare
Choose a tag to compare
v1.0.0-beta.2 Pre-release
Pre-release

Frequenz channels Release Notes

Summary

This release only have a CI fix that prevented v1.0.0-beta.1 to be automatically published. There are no user-visible changes.

What's Changed

Full Changelog: v1.0.0-beta.1...v1.0.0-beta.2

v1.0.0-beta.1

02 Nov 09:46
v1.0.0-beta.1
1dab247
Compare
Choose a tag to compare
v1.0.0-beta.1 Pre-release
Pre-release

Frequenz channels Release Notes

Summary

The Timer now can be started with a delay and some channel attributes were made private.

Upgrading

  • Anycast

    • The following public properties were removed (made private): limit, closed, deque, send_cv, recv_cv.
  • Broadcast

    • The following public properties were removed (made private): name, closed, recv_cv, receivers.

New Features

  • The arm64 architecture is now officially supported.

  • The documentation was improved to:

    • Show signatures with types.
    • Show the inherited members.
    • Documentation for pre-releases are now published.
    • Show the full tag name as the documentation version.
    • All development branches now have their documentation published (there is no next version anymore).
    • Fix the order of the documentation versions.
  • Broadcast

    • Added a resend_latest read-write property to get/set whether the latest message should be resent to new receivers.
  • Timer

    • Timer(), Timer.timeout(), Timer.periodic() and Timer.reset() now take an optional start_delay option to make the timer start after some delay.

      This can be useful, for example, if the timer needs to be aligned to a particular time. The alternative to this would be to sleep() for the time needed to align the timer, but if the sleep() call gets delayed because the event loop is busy, then a re-alignment is needed and this could go on for a while. The only way to guarantee a certain alignment (with a reasonable precision) is to delay the timer start.

What's Changed

Read more

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