Skip to content

Commit

Permalink
Rename OnlyIfPrevious to WithPrevious (#357)
Browse files Browse the repository at this point in the history
The new name makes is more clear that this predicate is adding an extra
condition to the message filtering based on the previous message.

Fixes #355.
  • Loading branch information
llucax authored Dec 4, 2024
2 parents 4c7f2c1 + 474a1df commit 56ae688
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 213 deletions.
3 changes: 1 addition & 2 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@

### Experimental

- A new predicate, `OnlyIfPrevious`, to `filter()` messages based on the previous message.
- A new special case of `OnlyIfPrevious`, `ChangedOnly`, to skip messages if they are equal to the previous message.
- A new composable predicate, `WithPrevious`, to filter messages based on the previous message.
5 changes: 2 additions & 3 deletions src/frequenz/channels/experimental/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@
"""

from ._pipe import Pipe
from ._predicates import ChangedOnly, OnlyIfPrevious
from ._relay_sender import RelaySender
from ._with_previous import WithPrevious

__all__ = [
"ChangedOnly",
"OnlyIfPrevious",
"WithPrevious",
"Pipe",
"RelaySender",
]
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# License: MIT
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH

"""Predicates to be used in conjuntion with `Receiver.filter()`."""
"""Composable predicate to cache and compare with the previous message."""


from typing import Callable, Final, Generic, TypeGuard
Expand All @@ -20,48 +20,43 @@ def __str__(self) -> str:
_SENTINEL: Final[_Sentinel] = _Sentinel()


class OnlyIfPrevious(Generic[ChannelMessageT]):
"""A predicate to check if a message has a particular relationship with the previous one.
class WithPrevious(Generic[ChannelMessageT]):
"""A composable predicate to build predicates that can use also the previous message.
This predicate can be used to filter out messages based on a custom condition on the
This predicate can be used to filter messages based on a custom condition on the
previous and current messages. This can be useful in cases where you want to
process messages only if they satisfy a particular condition with respect to the
previous message.
Tip:
If you want to use `==` as predicate, you can use the
[`ChangedOnly`][frequenz.channels.experimental.ChangedOnly] predicate.
Example: Receiving only messages that are not the same instance as the previous one.
Example: Receiving only messages that are different from the previous one.
```python
from frequenz.channels import Broadcast
from frequenz.channels.experimental import OnlyIfPrevious
from frequenz.channels.experimental import WithPrevious
channel = Broadcast[int | bool](name="example")
receiver = channel.new_receiver().filter(OnlyIfPrevious(lambda old, new: old is not new))
channel = Broadcast[int](name="example")
receiver = channel.new_receiver().filter(WithPrevious(lambda old, new: old != new))
sender = channel.new_sender()
# This message will be received as it is the first message.
await sender.send(1)
assert await receiver.receive() == 1
# This message will be skipped as it is the same instance as the previous one.
# This message will be skipped as it equals to the previous one.
await sender.send(1)
# This message will be received as it is a different instance from the previous
# one.
await sender.send(True)
assert await receiver.receive() is True
# This message will be received as it is a different from the previous one.
await sender.send(0)
assert await receiver.receive() == 0
```
Example: Receiving only messages if they are bigger than the previous one.
```python
from frequenz.channels import Broadcast
from frequenz.channels.experimental import OnlyIfPrevious
from frequenz.channels.experimental import WithPrevious
channel = Broadcast[int](name="example")
receiver = channel.new_receiver().filter(
OnlyIfPrevious(lambda old, new: new > old, first_is_true=False)
WithPrevious(lambda old, new: new > old, first_is_true=False)
)
sender = channel.new_sender()
Expand Down Expand Up @@ -90,6 +85,7 @@ class OnlyIfPrevious(Generic[ChannelMessageT]):
def __init__(
self,
predicate: Callable[[ChannelMessageT, ChannelMessageT], bool],
/,
*,
first_is_true: bool = True,
) -> None:
Expand Down Expand Up @@ -127,58 +123,3 @@ def __str__(self) -> str:
def __repr__(self) -> str:
"""Return a string representation of this instance."""
return f"<{type(self).__name__}: {self._predicate!r} first_is_true={self._first_is_true!r}>"


class ChangedOnly(OnlyIfPrevious[object]):
"""A predicate to check if a message is different from the previous one.
This predicate can be used to filter out messages that are the same as the previous
one. This can be useful in cases where you want to avoid processing duplicate
messages.
Warning:
This predicate uses the `!=` operator to compare messages, which includes all
the weirdnesses of Python's equality comparison (e.g., `1 == 1.0`, `True == 1`,
`True == 1.0`, `False == 0` are all `True`).
If you need to use a different comparison, you can create a custom predicate
using [`OnlyIfPrevious`][frequenz.channels.experimental.OnlyIfPrevious].
Example:
```python
from frequenz.channels import Broadcast
from frequenz.channels.experimental import ChangedOnly
channel = Broadcast[int](name="skip_duplicates_test")
receiver = channel.new_receiver().filter(ChangedOnly())
sender = channel.new_sender()
# This message will be received as it is the first message.
await sender.send(1)
assert await receiver.receive() == 1
# This message will be skipped as it is the same as the previous one.
await sender.send(1)
# This message will be received as it is different from the previous one.
await sender.send(2)
assert await receiver.receive() == 2
```
"""

def __init__(self, *, first_is_true: bool = True) -> None:
"""Initialize this instance.
Args:
first_is_true: Whether the first message should be considered as different
from the previous one. Defaults to `True`.
"""
super().__init__(lambda old, new: old != new, first_is_true=first_is_true)

def __str__(self) -> str:
"""Return a string representation of this instance."""
return f"{type(self).__name__}"

def __repr__(self) -> str:
"""Return a string representation of this instance."""
return f"{type(self).__name__}(first_is_true={self._first_is_true!r})"
121 changes: 0 additions & 121 deletions tests/experimental/test_predicates_changed_only.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# License: MIT
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH

"""Tests for the OnlyIfPrevious implementation."""
"""Tests for the WithPrevious implementation."""

from dataclasses import dataclass
from typing import Callable, Generic, TypeVar

import pytest

from frequenz.channels.experimental import OnlyIfPrevious
from frequenz.channels.experimental import WithPrevious

_T = TypeVar("_T")


@dataclass(frozen=True, kw_only=True)
class PredicateTestCase(Generic[_T]):
"""Test case for testing OnlyIfPrevious behavior with different predicates."""
"""Test case for testing WithPrevious behavior with different predicates."""

title: str
messages: list[_T]
Expand Down Expand Up @@ -113,12 +113,12 @@ def is_not_same_instance(old: object, new: object) -> bool:
ids=lambda test_case: test_case.title,
)
def test_only_if_previous(test_case: PredicateTestCase[_T]) -> None:
"""Test the OnlyIfPrevious with different predicates and sequences.
"""Test the WithPrevious with different predicates and sequences.
Args:
test_case: The test case containing the input values and expected results.
"""
only_if_previous = OnlyIfPrevious(
only_if_previous = WithPrevious(
test_case.predicate,
first_is_true=test_case.first_is_true,
)
Expand All @@ -127,9 +127,9 @@ def test_only_if_previous(test_case: PredicateTestCase[_T]) -> None:


def test_only_if_previous_state_independence() -> None:
"""Test that multiple OnlyIfPrevious instances maintain independent state."""
only_if_previous1 = OnlyIfPrevious(is_greater)
only_if_previous2 = OnlyIfPrevious(is_greater)
"""Test that multiple WithPrevious instances maintain independent state."""
only_if_previous1 = WithPrevious(is_greater)
only_if_previous2 = WithPrevious(is_greater)

# First message should be accepted (first_is_true default is True)
assert only_if_previous1(1) is True
Expand All @@ -141,17 +141,17 @@ def test_only_if_previous_state_independence() -> None:


def test_only_if_previous_str_representation() -> None:
"""Test the string representation of OnlyIfPrevious."""
only_if_previous = OnlyIfPrevious(is_greater)
assert str(only_if_previous) == "OnlyIfPrevious:is_greater"
"""Test the string representation of WithPrevious."""
only_if_previous = WithPrevious(is_greater)
assert str(only_if_previous) == "WithPrevious:is_greater"
assert (
repr(only_if_previous) == f"<OnlyIfPrevious: {is_greater!r} first_is_true=True>"
repr(only_if_previous) == f"<WithPrevious: {is_greater!r} first_is_true=True>"
)


def test_only_if_previous_sentinel_str() -> None:
"""Test the string representation of the sentinel value."""
only_if_previous = OnlyIfPrevious(always_true)
only_if_previous = WithPrevious(always_true)

# Access the private attribute for testing purposes
# pylint: disable=protected-access
Expand Down

0 comments on commit 56ae688

Please sign in to comment.