Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ exclude =
gen_resources,
build,
extend-ignore=F401,E704,E226

# Allow assigning lambdas in tests
per-file-ignores =
tests/*:E731
37 changes: 23 additions & 14 deletions src/pybind/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,28 @@
_U = TypeVar("_U")


def trim_and_call(listener: Callable, *parameters):
parameter_count = count_non_default_parameters(listener)
def _is_positional_parameter(param: Parameter) -> bool:
return param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD)


def count_total_parameters(function: Callable) -> int:
parameters = inspect.signature(function).parameters
return sum(1 for parameter in parameters.values() if _is_positional_parameter(parameter))


def trim_and_call(observer: Callable, *parameters):
parameter_count = count_total_parameters(observer)
trimmed_parameters = parameters[:parameter_count]
listener(*trimmed_parameters)
observer(*trimmed_parameters)


def _is_required_parameter(param: Parameter) -> bool:
return param.default == param.empty and param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD)
def _is_required_positional_parameter(param: Parameter) -> bool:
return param.default == param.empty and _is_positional_parameter(param)


def count_non_default_parameters(function: Callable) -> int:
parameters = inspect.signature(function).parameters
return sum(1 for param in parameters.values() if _is_required_parameter(param))
return sum(1 for param in parameters.values() if _is_required_positional_parameter(param))


def assert_parameter_max_count(callable_: Callable, max_count: int) -> None:
Expand Down Expand Up @@ -52,8 +61,8 @@ def unobserve(self, observer: Observer) -> None:
self._observers.remove(observer)

def __call__(self) -> None:
for listener in self._observers:
listener()
for observer in self._observers:
observer()


class ValueEvent(Generic[_S], ValueObservable[_S], ValueEmitter[_S]):
Expand All @@ -71,8 +80,8 @@ def unobserve(self, observer: Observer | ValueObserver[_S]) -> None:
self._observers.remove(observer)

def __call__(self, value: _S) -> None:
for listener in self._observers:
trim_and_call(listener, value)
for observer in self._observers:
trim_and_call(observer, value)


class BiEvent(Generic[_S, _T], BiObservable[_S, _T], BiEmitter[_S, _T]):
Expand All @@ -89,8 +98,8 @@ def unobserve(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T])
self._observers.remove(observer)

def __call__(self, value_0: _S, value_1: _T) -> None:
for listener in self._observers:
trim_and_call(listener, value_0, value_1)
for observer in self._observers:
trim_and_call(observer, value_0, value_1)


class TriEvent(Generic[_S, _T, _U], TriObservable[_S, _T, _U], TriEmitter[_S, _T, _U]):
Expand All @@ -107,5 +116,5 @@ def unobserve(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T]
self._observers.remove(observer)

def __call__(self, value_0: _S, value_1: _T, value_2: _U) -> None:
for listener in self._observers:
trim_and_call(listener, value_0, value_1, value_2)
for observer in self._observers:
trim_and_call(observer, value_0, value_1, value_2)
51 changes: 51 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from unittest.mock import Mock


class Observer(Mock):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class NoParametersObserver(Observer):
def __call__(self):
super().__call__()


class OneParameterObserver(Observer):
def __call__(self, param0):
super().__call__(param0)


class OneDefaultParameterObserver(Observer):
def __call__(self, param0="default"):
super().__call__(param0)


class TwoParametersObserver(Observer):
def __call__(self, param0, param1):
super().__call__(param0, param1)


class OneRequiredOneDefaultParameterObserver(Observer):
def __call__(self, param0, param1="default"):
super().__call__(param0, param1)


class TwoDefaultParametersObserver(Observer):
def __call__(self, param0="default0", param1="default1"):
super().__call__(param0, param1)


class ThreeParametersObserver(Observer):
def __call__(self, param0, param1, param2):
super().__call__(param0, param1, param2)


class ThreeDefaultParametersObserver(Observer):
def __call__(self, param0="default0", param1="default1", param2="default2"):
super().__call__(param0=param0, param1=param1, param2=param2)


class TwoRequiredOneDefaultParameterObserver(Observer):
def __call__(self, param0, param1, param2="default2"):
super().__call__(param0, param1, param2)
213 changes: 213 additions & 0 deletions tests/test_bi_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import pytest
from pybind.event import BiEvent
from conftest import NoParametersObserver, OneParameterObserver, OneDefaultParameterObserver, \
OneRequiredOneDefaultParameterObserver, TwoParametersObserver, TwoDefaultParametersObserver


def test_bi_event_observe_and_call_no_parameters_mock_observer():
event = BiEvent[str, int]()
observer = NoParametersObserver()

event.observe(observer)
event("test_value", 42)

observer.assert_called_once_with()


def test_bi_event_observe_and_call_one_parameter_mock_observer():
event = BiEvent[str, int]()
observer = OneParameterObserver()

event.observe(observer)
event("test_value", 42)

observer.assert_called_once_with("test_value")


def test_bi_event_observe_and_call_one_default_parameter_mock_observer():
event = BiEvent[str, int]()
observer = OneDefaultParameterObserver()

event.observe(observer)
event("test_value", 42)

observer.assert_called_once_with("test_value")


def test_bi_event_observe_and_call_one_required_one_default_parameter_mock_observer():
event = BiEvent[str, int]()
observer = OneRequiredOneDefaultParameterObserver()

event.observe(observer)
event("test_value", 42)

observer.assert_called_once_with("test_value", 42)


def test_bi_event_observe_and_call_two_parameters_mock_observer():
event = BiEvent[str, int]()
observer = TwoParametersObserver()

event.observe(observer)
event("test_value", 42)

observer.assert_called_once_with("test_value", 42)


def test_bi_event_observe_and_call_two_default_parameters_mock_observer():
event = BiEvent[str, int]()
observer = TwoDefaultParametersObserver()

event.observe(observer)
event("test_value", 42)

observer.assert_called_once_with("test_value", 42)


def test_bi_event_unobserve_mock_observer():
event = BiEvent[str, int]()
observer = TwoParametersObserver()

event.observe(observer)
event.unobserve(observer)
event("test", 42)

observer.assert_not_called()


def test_bi_event_multiple_mock_observers():
event = BiEvent[str, int]()
observer0 = NoParametersObserver()
observer1 = OneParameterObserver()
observer2 = TwoParametersObserver()

event.observe(observer0)
event.observe(observer1)
event.observe(observer2)
event("hello", 123)

observer0.assert_called_once_with()
observer1.assert_called_once_with("hello")
observer2.assert_called_once_with("hello", 123)


def test_bi_event_function_observer_with_too_many_parameters():
event = BiEvent[str, int]()

def bad_observer(param0, param1, param2):
pass

with pytest.raises(ValueError):
event.observe(bad_observer)


def test_bi_event_observe_and_call_lambda_no_parameters():
event = BiEvent[str, int]()
called = []

event.observe(lambda: called.append(True))
event("test_value", 42)

assert called == [True]


def test_bi_event_observe_and_call_lambda_one_parameter():
event = BiEvent[str, int]()
calls = []

event.observe(lambda value0: calls.append(value0))
event("test_value", 42)

assert calls == ["test_value"]


def test_bi_event_observe_and_call_lambda_two_parameters():
event = BiEvent[str, int]()
calls = []

event.observe(lambda value0, value1: calls.append((value0, value1)))
event("test_value", 42)

assert calls == [("test_value", 42)]


def test_bi_event_unobserve_lambda():
event = BiEvent[str, int]()
calls = []
observer = lambda value0, value1: calls.append((value0, value1))

event.observe(observer)
event("test0", 0)
event.unobserve(observer)
event("test1", 1)

assert calls == [("test0", 0)]


def test_bi_event_lambda_with_too_many_parameters():
event = BiEvent[str, int]()

with pytest.raises(ValueError):
event.observe(lambda param0, param1, param2: None)


def test_bi_event_call_with_one_parameter():
event = BiEvent[str, int]()

with pytest.raises(TypeError):
event("param0")


def test_bi_event_call_with_three_parameters():
event = BiEvent[str, int]()

with pytest.raises(TypeError):
event("param0", 42, "param2")


def test_bi_event_unobserve_non_existent_observer():
event = BiEvent[str, int]()
observer = TwoParametersObserver()

with pytest.raises(ValueError):
event.unobserve(observer)


def test_bi_event_observe_same_observer_multiple_times():
event = BiEvent[str, int]()
observer = TwoParametersObserver()

event.observe(observer)
event.observe(observer)
event("test", 42)

assert observer.call_count == 2
observer.assert_called_with("test", 42)


def test_bi_event_call_with_no_observers():
event = BiEvent[str, int]()
event("test_value", 42) # Should not raise


def test_bi_event_observers_called_in_order():
event = BiEvent[str, int]()
call_order = []

event.observe(lambda value0, value1: call_order.append("first"))
event.observe(lambda value0, value1: call_order.append("second"))
event.observe(lambda value0, value1: call_order.append("third"))

event("test", 42)

assert call_order == ["first", "second", "third"]


def test_bi_event_call_with_none_values():
event = BiEvent[str | None, int | None]()
observer = TwoParametersObserver()

event.observe(observer)
event(None, None)

observer.assert_called_once_with(None, None)
24 changes: 2 additions & 22 deletions tests/test_events.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,9 @@
import pytest
from unittest.mock import Mock

from conftest import NoParametersObserver, OneParameterObserver, OneDefaultParameterObserver
from pybind.event import Event


class Observer(Mock):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class NoParametersObserver(Observer):
def __call__(self):
super().__call__()


class OneParameterObserver(Observer):
def __call__(self, param0):
super().__call__(param0)


class OneDefaultParameterObserver(Observer):
def __call__(self, param0="default"):
super().__call__(param0=param0)


def test_event_initialization():
event = Event()
assert event._observers == []
Expand Down Expand Up @@ -94,7 +74,7 @@ def test_mock_observer_with_default_parameter():
event.observe(observer)
event()

observer.assert_called_once_with(param0="default")
observer.assert_called_once_with("default")


def test_call_with_no_observers():
Expand Down
Loading