Skip to content

Commit b63f391

Browse files
authored
Merge pull request #4 from FancyNeuron/3-setup-github-actions
3 setup GitHub actions
2 parents 1402f39 + efa091b commit b63f391

File tree

7 files changed

+225
-34
lines changed

7 files changed

+225
-34
lines changed

.flake8

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[flake8]
2+
exclude =
3+
.git,
4+
__pycache__,
5+
.venv,
6+
env,
7+
gen_resources,
8+
build,
9+
extend-ignore=F401,E704,E226
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Python package
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
jobs:
10+
build:
11+
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
python-version: ["3.10", "3.12"]
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v3
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
python -m pip install -e .[testing]
28+
- name: Lint with flake8
29+
run: |
30+
flake8 . --count --max-complexity=10 --max-line-length=180 --show-source --statistics
31+
- name: Type check with mypy
32+
run: |
33+
mypy src
34+
- name: Test with pytest
35+
run: |
36+
pytest

src/pybind/emitters.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
from abc import ABC, abstractmethod
22
from typing import TypeVar, Generic
33

4+
45
T = TypeVar("T")
56
U = TypeVar("U")
67
S = TypeVar("S")
78

9+
810
class Emitter(ABC):
911
@abstractmethod
1012
def __call__(self) -> None: ...
1113

14+
1215
class ValueEmitter(Generic[T], ABC):
1316
@abstractmethod
1417
def __call__(self, value0: T) -> None: ...
1518

19+
1620
class BiEmitter(Generic[T, U], ABC):
1721
@abstractmethod
1822
def __call__(self, value0: T, value1: U) -> None: ...
1923

24+
2025
class TriEmitter(Generic[T, U, S], ABC):
2126
@abstractmethod
22-
def __call__(self, value0: T, value1: U, value2: S) -> None: ...
27+
def __call__(self, value0: T, value1: U, value2: S) -> None: ...

src/pybind/event.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import inspect
2+
from inspect import Parameter
23
from typing import Callable, TypeVar, Generic
34

45
from pybind.emitters import Emitter, TriEmitter, BiEmitter, ValueEmitter
@@ -16,32 +17,42 @@ def trim_and_call(listener: Callable, *parameters):
1617
listener(*trimmed_parameters)
1718

1819

20+
def _is_required_parameter(param: Parameter) -> bool:
21+
return param.default == param.empty and param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD)
22+
23+
1924
def count_non_default_parameters(function: Callable) -> int:
2025
parameters = inspect.signature(function).parameters
21-
return sum(1 for param in parameters.values() if param.default == param.empty)
26+
return sum(1 for param in parameters.values() if _is_required_parameter(param))
2227

2328

2429
def assert_parameter_max_count(callable_: Callable, max_count: int) -> None:
2530
if count_non_default_parameters(callable_) > max_count:
26-
raise ValueError(f"Callable {callable_.__name__} has too many non-default parameters: "
31+
if hasattr(callable_, '__name__'):
32+
callable_name = callable_.__name__
33+
elif hasattr(callable_, '__class__'):
34+
callable_name = callable_.__class__.__name__
35+
else:
36+
callable_name = str(callable_)
37+
raise ValueError(f"Callable {callable_name} has too many non-default parameters: "
2738
f"{count_non_default_parameters(callable_)} > {max_count}")
2839

2940

3041
class Event(Observable, Emitter):
42+
_observers: list[Observer]
43+
3144
def __init__(self):
32-
self.listeners = []
45+
self._observers = []
3346

34-
def observe(self, observer: Observer):
35-
self.listeners.append(observer)
47+
def observe(self, observer: Observer) -> None:
48+
self._observers.append(observer)
3649
assert_parameter_max_count(observer, 0)
37-
return self
3850

39-
def unobserve(self, observer: Observer):
40-
self.listeners.remove(observer)
41-
return self
51+
def unobserve(self, observer: Observer) -> None:
52+
self._observers.remove(observer)
4253

4354
def __call__(self) -> None:
44-
for listener in self.listeners:
55+
for listener in self._observers:
4556
listener()
4657

4758

@@ -52,11 +63,11 @@ def __init__(self):
5263
self._observers = []
5364
super().__init__()
5465

55-
def observe(self, observer: Observer | ValueObserver[_T]) -> None:
66+
def observe(self, observer: Observer | ValueObserver[_S]) -> None:
5667
self._observers.append(observer)
5768
assert_parameter_max_count(observer, 1)
5869

59-
def unobserve(self, observer: Observer | ValueObserver[_T]) -> None:
70+
def unobserve(self, observer: Observer | ValueObserver[_S]) -> None:
6071
self._observers.remove(observer)
6172

6273
def __call__(self, value: _S) -> None:
@@ -70,11 +81,11 @@ class BiEvent(Generic[_S, _T], BiObservable[_S, _T], BiEmitter[_S, _T]):
7081
def __init__(self):
7182
self._observers = []
7283

73-
def observe(self, observer: Observer | ValueObserver[_T] | BiObserver[_T, _U]) -> None:
84+
def observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T]) -> None:
7485
self._observers.append(observer)
7586
assert_parameter_max_count(observer, 2)
7687

77-
def unobserve(self, observer: Observer | ValueObserver[_T] | BiObserver[_T, _U]) -> None:
88+
def unobserve(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T]) -> None:
7889
self._observers.remove(observer)
7990

8091
def __call__(self, value_0: _S, value_1: _T) -> None:
@@ -83,16 +94,16 @@ def __call__(self, value_0: _S, value_1: _T) -> None:
8394

8495

8596
class TriEvent(Generic[_S, _T, _U], TriObservable[_S, _T, _U], TriEmitter[_S, _T, _U]):
86-
_observers: list[ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U]]
97+
_observers: list[Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U]]
8798

8899
def __init__(self):
89100
self._observers = []
90101

91-
def observe(self, observer: Observer | ValueObserver[_T] | BiObserver[_T, _U] | TriObserver[_T, _U, _S]) -> None:
102+
def observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U]) -> None:
92103
self._observers.append(observer)
93104
assert_parameter_max_count(observer, 3)
94105

95-
def unobserve(self, observer: Observer | ValueObserver[_T] | BiObserver[_T, _U] | TriObserver[_T, _U, _S]) -> None:
106+
def unobserve(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U]) -> None:
96107
self._observers.remove(observer)
97108

98109
def __call__(self, value_0: _S, value_1: _T, value_2: _U) -> None:

src/pybind/observables.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
from abc import ABC, abstractmethod
22
from typing import TypeVar, Callable, Generic, Protocol
33

4+
5+
_SC = TypeVar("_SC", contravariant=True)
6+
_TC = TypeVar("_TC", contravariant=True)
7+
_UC = TypeVar("_UC", contravariant=True)
8+
49
_S = TypeVar("_S")
510
_T = TypeVar("_T")
611
_U = TypeVar("_U")
@@ -10,16 +15,16 @@ class Observer(Protocol):
1015
def __call__(self) -> None: ...
1116

1217

13-
class ValueObserver(Protocol[_T]):
14-
def __call__(self, arg: _T) -> None: ...
18+
class ValueObserver(Protocol[_TC]):
19+
def __call__(self, arg: _TC) -> None: ...
1520

1621

17-
class BiObserver(Protocol[_T, _U]):
18-
def __call__(self, arg1: _T, arg2: _U) -> None: ...
22+
class BiObserver(Protocol[_TC, _UC]):
23+
def __call__(self, arg1: _TC, arg2: _UC) -> None: ...
1924

2025

21-
class TriObserver(Protocol[_T, _U, _S]):
22-
def __call__(self, arg1: _T, arg2: _U, arg3: _S) -> None: ...
26+
class TriObserver(Protocol[_TC, _UC, _SC]):
27+
def __call__(self, arg1: _TC, arg2: _UC, arg3: _SC) -> None: ...
2328

2429

2530
class Observable(ABC):
@@ -32,31 +37,31 @@ def unobserve(self, observer: Observer) -> None:
3237
raise NotImplementedError
3338

3439

35-
class ValueObservable(Generic[_T], ABC):
40+
class ValueObservable(Generic[_S], ABC):
3641
@abstractmethod
37-
def observe(self, observer: Observer | ValueObserver[_T]) -> None:
42+
def observe(self, observer: Observer | ValueObserver[_S]) -> None:
3843
raise NotImplementedError
3944

4045
@abstractmethod
41-
def unobserve(self, observer: Observer | ValueObserver[_T]) -> None:
46+
def unobserve(self, observer: Observer | ValueObserver[_S]) -> None:
4247
raise NotImplementedError
4348

4449

45-
class BiObservable(Generic[_T, _U], ABC):
50+
class BiObservable(Generic[_S, _T], ABC):
4651
@abstractmethod
47-
def observe(self, observer: Observer | ValueObserver[_T] | BiObserver[_T, _U]) -> None:
52+
def observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T]) -> None:
4853
raise NotImplementedError
4954

5055
@abstractmethod
51-
def unobserve(self, observer: Observer | ValueObserver[_T] | BiObserver[_T, _U]) -> None:
56+
def unobserve(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T]) -> None:
5257
raise NotImplementedError
5358

5459

55-
class TriObservable(Generic[_T, _U, _S], ABC):
60+
class TriObservable(Generic[_S, _T, _U], ABC):
5661
@abstractmethod
57-
def observe(self, observer: Observer | ValueObserver[_T] | BiObserver[_T, _U] | TriObserver[_T, _U, _S]) -> None:
62+
def observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U]) -> None:
5863
raise NotImplementedError
5964

6065
@abstractmethod
61-
def unobserve(self, observer: Observer | ValueObserver[_T] | BiObserver[_T, _U] | TriObserver[_T, _U, _S]) -> None:
62-
raise NotImplementedError
66+
def unobserve(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U]) -> None:
67+
raise NotImplementedError

src/pybind/py.typed

Whitespace-only changes.

tests/test_events.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import pytest
2+
from unittest.mock import Mock
3+
4+
from pybind.event import Event
5+
6+
7+
class Observer(Mock):
8+
def __init__(self, *args, **kwargs):
9+
super().__init__(*args, **kwargs)
10+
11+
12+
class NoParametersObserver(Observer):
13+
def __call__(self):
14+
super().__call__()
15+
16+
17+
class OneParameterObserver(Observer):
18+
def __call__(self, param0):
19+
super().__call__(param0)
20+
21+
22+
class OneDefaultParameterObserver(Observer):
23+
def __call__(self, param0="default"):
24+
super().__call__(param0=param0)
25+
26+
27+
def test_event_initialization():
28+
event = Event()
29+
assert event._observers == []
30+
31+
32+
def test_mock_observe_adds_observer():
33+
event = Event()
34+
observer = NoParametersObserver()
35+
observer.__name__ = "test_observer"
36+
37+
event.observe(observer)
38+
39+
assert observer in event._observers
40+
41+
42+
def test_mock_observe_validates_parameter_count():
43+
event = Event()
44+
45+
with pytest.raises(ValueError):
46+
event.observe(OneParameterObserver())
47+
48+
49+
def test_mock_unobserve_removes_observer():
50+
event = Event()
51+
observer = NoParametersObserver()
52+
event.observe(observer)
53+
54+
event.unobserve(observer)
55+
56+
assert observer not in event._observers
57+
58+
59+
def test_mock_unobserve_nonexistent_observer_raises():
60+
event = Event()
61+
62+
with pytest.raises(ValueError):
63+
event.unobserve(NoParametersObserver())
64+
65+
66+
def test_mock_unobserved_observer_not_called():
67+
event = Event()
68+
observer = NoParametersObserver()
69+
event.observe(observer)
70+
event.unobserve(observer)
71+
72+
event()
73+
74+
observer.assert_not_called()
75+
76+
77+
def test_mock_call_invokes_all_observers():
78+
event = Event()
79+
observer0 = NoParametersObserver()
80+
observer1 = NoParametersObserver()
81+
event.observe(observer0)
82+
event.observe(observer1)
83+
84+
event()
85+
86+
observer0.assert_called_once_with()
87+
observer1.assert_called_once_with()
88+
89+
90+
def test_mock_observer_with_default_parameter():
91+
event = Event()
92+
observer = OneDefaultParameterObserver()
93+
94+
event.observe(observer)
95+
event()
96+
97+
observer.assert_called_once_with(param0="default")
98+
99+
100+
def test_call_with_no_observers():
101+
event = Event()
102+
event()
103+
104+
105+
def test_function_observer_with_default_parameter():
106+
event = Event()
107+
108+
calls = []
109+
110+
def observer_with_default(param="default"):
111+
calls.append(param)
112+
113+
event.observe(observer_with_default)
114+
event()
115+
assert calls == ["default"]
116+
117+
118+
def test_lambda_observer():
119+
event = Event()
120+
calls = []
121+
122+
event.observe(lambda: calls.append(True))
123+
event()
124+
125+
assert calls == [True]

0 commit comments

Comments
 (0)