classic-signals 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- classic-signals-0.1.0/LICENSE +21 -0
- classic-signals-0.1.0/PKG-INFO +78 -0
- classic-signals-0.1.0/README.md +55 -0
- classic-signals-0.1.0/pyproject.toml +6 -0
- classic-signals-0.1.0/setup.cfg +40 -0
- classic-signals-0.1.0/setup.py +3 -0
- classic-signals-0.1.0/sources/classic/signals/__init__.py +4 -0
- classic-signals-0.1.0/sources/classic/signals/decorator.py +17 -0
- classic-signals-0.1.0/sources/classic/signals/hub.py +108 -0
- classic-signals-0.1.0/sources/classic/signals/reaction.py +27 -0
- classic-signals-0.1.0/sources/classic/signals/signal.py +25 -0
- classic-signals-0.1.0/sources/classic/signals/utils.py +42 -0
- classic-signals-0.1.0/sources/classic_signals.egg-info/PKG-INFO +78 -0
- classic-signals-0.1.0/sources/classic_signals.egg-info/SOURCES.txt +19 -0
- classic-signals-0.1.0/sources/classic_signals.egg-info/dependency_links.txt +1 -0
- classic-signals-0.1.0/sources/classic_signals.egg-info/requires.txt +8 -0
- classic-signals-0.1.0/sources/classic_signals.egg-info/top_level.txt +1 -0
- classic-signals-0.1.0/tests/test_hub.py +93 -0
- classic-signals-0.1.0/tests/test_reaction.py +22 -0
- classic-signals-0.1.0/tests/test_signal.py +21 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Sergei Variasov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: classic-signals
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple signals and handlers
|
|
5
|
+
Home-page: https://github.com/variasov/classic-signals
|
|
6
|
+
Author: Sergei Variasov
|
|
7
|
+
Author-email: variasov@gmail.com
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/variasov/classic-signals/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: readerwriterlock~=1.0.9
|
|
17
|
+
Requires-Dist: classic-components==1.1.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest~=7.4.4; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-cov~=4.1; extra == "dev"
|
|
21
|
+
Requires-Dist: twine~=4.0; extra == "dev"
|
|
22
|
+
Requires-Dist: build~=1.0; extra == "dev"
|
|
23
|
+
|
|
24
|
+
# Classic Signals
|
|
25
|
+
|
|
26
|
+
Данный пакет предоставляет возможность использовать сигналы и реакции в проекте.
|
|
27
|
+
|
|
28
|
+
Они нужны для инверсии вызова. По сути, это паттерн Observer и немного сахара
|
|
29
|
+
сверху. В одном месте программы мы объявляем сигналы, пользуясь декоратором
|
|
30
|
+
signal, в других местах приложения пишем реакции на этих сигналов.
|
|
31
|
+
Реакции - это любой вызываемый объект, принимающий один аргумент - инстанс
|
|
32
|
+
сигнала. Это может быть и просто функция или метод, для удобства завернутый
|
|
33
|
+
в декоратор reaction. Последнее, что нужно, это инстанс класса Hub. В Hub
|
|
34
|
+
перед началом работы можно зарегистрировать реакции, потом можно вызвать у него
|
|
35
|
+
метод notify, передав в него инстанс сигнала, и Hub вызовет все реакции,
|
|
36
|
+
относящиеся к сигналу.
|
|
37
|
+
|
|
38
|
+
Пример:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from classic.components import component
|
|
42
|
+
from classic.signals import reaction, Hub, signal
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@signal
|
|
46
|
+
class SomethingHappened:
|
|
47
|
+
"""Под капотом сигналы - на самом деле датаклассы"""
|
|
48
|
+
some_field: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@signal
|
|
52
|
+
class SomethingAnotherHappened:
|
|
53
|
+
some_field: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@component
|
|
57
|
+
class SomeHandlers:
|
|
58
|
+
|
|
59
|
+
@reaction
|
|
60
|
+
def on_something_happened(self, signal: SomethingHappened):
|
|
61
|
+
print(f'Что-то произошло: {signal}')
|
|
62
|
+
|
|
63
|
+
@reaction
|
|
64
|
+
def on_something_another_happened(self, signal: SomethingAnotherHappened):
|
|
65
|
+
print(f'Что-то еще произошло: {signal}')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
hub = Hub()
|
|
69
|
+
SomeHandlers(signals=hub)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
signal_1 = SomethingHappened(some_field=0)
|
|
73
|
+
signal_2 = SomethingAnotherHappened(some_field=0)
|
|
74
|
+
|
|
75
|
+
hub.notify(signal_1, signal_2)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Classic Signals
|
|
2
|
+
|
|
3
|
+
Данный пакет предоставляет возможность использовать сигналы и реакции в проекте.
|
|
4
|
+
|
|
5
|
+
Они нужны для инверсии вызова. По сути, это паттерн Observer и немного сахара
|
|
6
|
+
сверху. В одном месте программы мы объявляем сигналы, пользуясь декоратором
|
|
7
|
+
signal, в других местах приложения пишем реакции на этих сигналов.
|
|
8
|
+
Реакции - это любой вызываемый объект, принимающий один аргумент - инстанс
|
|
9
|
+
сигнала. Это может быть и просто функция или метод, для удобства завернутый
|
|
10
|
+
в декоратор reaction. Последнее, что нужно, это инстанс класса Hub. В Hub
|
|
11
|
+
перед началом работы можно зарегистрировать реакции, потом можно вызвать у него
|
|
12
|
+
метод notify, передав в него инстанс сигнала, и Hub вызовет все реакции,
|
|
13
|
+
относящиеся к сигналу.
|
|
14
|
+
|
|
15
|
+
Пример:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from classic.components import component
|
|
19
|
+
from classic.signals import reaction, Hub, signal
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@signal
|
|
23
|
+
class SomethingHappened:
|
|
24
|
+
"""Под капотом сигналы - на самом деле датаклассы"""
|
|
25
|
+
some_field: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@signal
|
|
29
|
+
class SomethingAnotherHappened:
|
|
30
|
+
some_field: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@component
|
|
34
|
+
class SomeHandlers:
|
|
35
|
+
|
|
36
|
+
@reaction
|
|
37
|
+
def on_something_happened(self, signal: SomethingHappened):
|
|
38
|
+
print(f'Что-то произошло: {signal}')
|
|
39
|
+
|
|
40
|
+
@reaction
|
|
41
|
+
def on_something_another_happened(self, signal: SomethingAnotherHappened):
|
|
42
|
+
print(f'Что-то еще произошло: {signal}')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
hub = Hub()
|
|
46
|
+
SomeHandlers(signals=hub)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
signal_1 = SomethingHappened(some_field=0)
|
|
50
|
+
signal_2 = SomethingAnotherHappened(some_field=0)
|
|
51
|
+
|
|
52
|
+
hub.notify(signal_1, signal_2)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = classic-signals
|
|
3
|
+
version = 0.1.0
|
|
4
|
+
description = Simple signals and handlers
|
|
5
|
+
author = Sergei Variasov
|
|
6
|
+
author_email = variasov@gmail.com
|
|
7
|
+
long_description = file: README.md
|
|
8
|
+
long_description_content_type = text/markdown
|
|
9
|
+
url = https://github.com/variasov/classic-signals
|
|
10
|
+
project_urls =
|
|
11
|
+
Bug Tracker = https://github.com/variasov/classic-signals/issues
|
|
12
|
+
classifiers =
|
|
13
|
+
Programming Language :: Python :: 3.10
|
|
14
|
+
License :: OSI Approved :: MIT License
|
|
15
|
+
Operating System :: OS Independent
|
|
16
|
+
Development Status :: 4 - Beta
|
|
17
|
+
|
|
18
|
+
[options]
|
|
19
|
+
packages = find_namespace:
|
|
20
|
+
python_requires = >=3.10
|
|
21
|
+
package_dir =
|
|
22
|
+
= sources
|
|
23
|
+
install_requires =
|
|
24
|
+
readerwriterlock~=1.0.9
|
|
25
|
+
classic-components==1.1.0
|
|
26
|
+
|
|
27
|
+
[options.packages.find]
|
|
28
|
+
where = sources
|
|
29
|
+
|
|
30
|
+
[options.extras_require]
|
|
31
|
+
dev =
|
|
32
|
+
pytest~=7.4.4
|
|
33
|
+
pytest-cov~=4.1
|
|
34
|
+
twine~=4.0
|
|
35
|
+
build~=1.0
|
|
36
|
+
|
|
37
|
+
[egg_info]
|
|
38
|
+
tag_build =
|
|
39
|
+
tag_date = 0
|
|
40
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from classic.components import add_extra_annotation
|
|
2
|
+
|
|
3
|
+
from .hub import Hub
|
|
4
|
+
from .reaction import Reaction
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def reaction(fn: Reaction) -> Reaction:
|
|
8
|
+
"""
|
|
9
|
+
Декоратор, помечающий указанную функцию,
|
|
10
|
+
как реакцию на сигнал, чтобы затем Hub автоматически их регистрировал.
|
|
11
|
+
|
|
12
|
+
:param fn: функция
|
|
13
|
+
:return: та же самая функция
|
|
14
|
+
"""
|
|
15
|
+
add_extra_annotation(fn, 'hub', Hub)
|
|
16
|
+
fn.__is_reaction = True
|
|
17
|
+
return fn
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
import threading
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Type
|
|
5
|
+
|
|
6
|
+
from classic.components import Registry
|
|
7
|
+
from readerwriterlock import rwlock
|
|
8
|
+
|
|
9
|
+
from .reaction import filter_reactions, Reaction
|
|
10
|
+
from .signal import Signal
|
|
11
|
+
from . import utils
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Hub(Registry):
|
|
15
|
+
"""
|
|
16
|
+
Хранит знания о сигналах и привязанных к ним реакциям, принимает сигналы и
|
|
17
|
+
инициирует реакции на сигналы.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_reactions: dict[Signal, list[Reaction]]
|
|
21
|
+
_lock: rwlock.RWLockRead
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self._reactions = defaultdict(list)
|
|
25
|
+
self._lock = rwlock.RWLockRead(threading.RLock)
|
|
26
|
+
|
|
27
|
+
def notify(self, *signals: Signal) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Вызывает зарегистрированные реакции для указаных сигналов.
|
|
30
|
+
|
|
31
|
+
:param signals: Сигналы, на которые требуется среагировать.
|
|
32
|
+
"""
|
|
33
|
+
with self._lock.gen_rlock():
|
|
34
|
+
for signal in signals:
|
|
35
|
+
for reaction in self._reactions[signal.__class__]:
|
|
36
|
+
reaction(signal)
|
|
37
|
+
|
|
38
|
+
def add_reaction(
|
|
39
|
+
self, reaction: Reaction,
|
|
40
|
+
signal: Optional[Type[Signal]] = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Добавляет реакцию на сигнал.
|
|
44
|
+
|
|
45
|
+
Если сигнал не указан, то реакция будет добавлена в сигнал, указанный
|
|
46
|
+
в аннотации вызываемого объекта. Если аннотация не указана, вызовет
|
|
47
|
+
исключение AssertionError.
|
|
48
|
+
|
|
49
|
+
Проверка динамическая, построена на assert.
|
|
50
|
+
|
|
51
|
+
:param reaction: Вызываемый объект с одним аргументом - сигналом.
|
|
52
|
+
:param signal: Класс сигнала, на который должна реагировать реакция.
|
|
53
|
+
"""
|
|
54
|
+
signal = signal or utils.get_signal_type(reaction)
|
|
55
|
+
with self._lock.gen_wlock():
|
|
56
|
+
if reaction not in self._reactions[signal]:
|
|
57
|
+
self._reactions[signal].append(reaction)
|
|
58
|
+
|
|
59
|
+
def remove_reaction(
|
|
60
|
+
self, reaction: Reaction,
|
|
61
|
+
signal: Optional[Type[Signal]] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Добавляет реакцию на сигнал.
|
|
65
|
+
|
|
66
|
+
Если сигнал не указан, то Hub переберет реакции для всех сигналов,
|
|
67
|
+
и уберет указанный отовсюду.
|
|
68
|
+
|
|
69
|
+
:param reaction: Вызываемый объект с одним аргументом - сигналом.
|
|
70
|
+
:param signal: Класс сигнала, на который должна реагировать реакция.
|
|
71
|
+
"""
|
|
72
|
+
with self._lock.gen_wlock():
|
|
73
|
+
if signal:
|
|
74
|
+
try:
|
|
75
|
+
self._reactions[signal].remove(reaction)
|
|
76
|
+
except ValueError:
|
|
77
|
+
pass
|
|
78
|
+
else:
|
|
79
|
+
for reactions in self._reactions.values():
|
|
80
|
+
try:
|
|
81
|
+
reactions.remove(reaction)
|
|
82
|
+
except ValueError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def register(self, obj: Any) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Разбирает указанный объект на реакции, и добавляет их
|
|
88
|
+
к внутреннему реестру.
|
|
89
|
+
|
|
90
|
+
Реакцией считается метод, завернутый в декоратор reaction.
|
|
91
|
+
|
|
92
|
+
:param obj: Объект, содержащий реакции.
|
|
93
|
+
"""
|
|
94
|
+
with self._lock.gen_wlock():
|
|
95
|
+
for reaction in filter_reactions(obj):
|
|
96
|
+
self.add_reaction(reaction)
|
|
97
|
+
|
|
98
|
+
def unregister(self, obj: Any) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Удаляет все реакции, определенные в объекте.
|
|
101
|
+
|
|
102
|
+
Реакцией считается метод, завернутый в декоратор reaction.
|
|
103
|
+
|
|
104
|
+
:param obj: Объект, содержащий реакции.
|
|
105
|
+
"""
|
|
106
|
+
with self._lock.gen_wlock():
|
|
107
|
+
for reaction in filter_reactions(obj):
|
|
108
|
+
self.remove_reaction(reaction)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any, List, Callable, TypeVar
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
Reaction = TypeVar('Reaction', bound=Callable[[Any], Any])
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_reaction(obj: Any) -> bool:
|
|
9
|
+
"""
|
|
10
|
+
Проверяет, является ли объект реакцией.
|
|
11
|
+
|
|
12
|
+
:param obj: объект
|
|
13
|
+
:returns: bool, True, если указанный объект является реакцией.
|
|
14
|
+
"""
|
|
15
|
+
return callable(obj) and getattr(obj, '__is_reaction', False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def filter_reactions(obj: Any) -> List[Reaction]:
|
|
19
|
+
"""
|
|
20
|
+
Возвращает список реакций, определенных в объекте.
|
|
21
|
+
:param obj: Объект с методами, помеченными декоратором reaction.
|
|
22
|
+
"""
|
|
23
|
+
return [
|
|
24
|
+
member
|
|
25
|
+
for name, member
|
|
26
|
+
in inspect.getmembers(obj, predicate=is_reaction)
|
|
27
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any, TypeVar
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
Signal = TypeVar('Signal')
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def signal(cls: Signal) -> Signal:
|
|
9
|
+
"""
|
|
10
|
+
Декоратор, помечающий класс как сигнал.
|
|
11
|
+
|
|
12
|
+
:param cls: Класс, определяемый как сигнал.
|
|
13
|
+
"""
|
|
14
|
+
new_cls = dataclass(cls, eq=False, frozen=True)
|
|
15
|
+
new_cls.__is_signal = True
|
|
16
|
+
return new_cls
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_signal(obj: Any) -> bool:
|
|
20
|
+
"""
|
|
21
|
+
Проверяет, является ли объект сигналом.
|
|
22
|
+
|
|
23
|
+
:param obj: Объект для проверки.
|
|
24
|
+
"""
|
|
25
|
+
return hasattr(obj, '__is_signal')
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from .signal import is_signal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_signature_length(signature: inspect.Signature) -> bool:
|
|
7
|
+
"""
|
|
8
|
+
Проверяет, что сигнатура функции имеет длину 1.
|
|
9
|
+
:param signature: сигнатура функции
|
|
10
|
+
"""
|
|
11
|
+
# TODO: Needed another, more stable way for detecting methods and functions
|
|
12
|
+
# inspect.ismethod works only on instances, not on class functions
|
|
13
|
+
needed_len = 2 if 'self' in signature.parameters else 1
|
|
14
|
+
|
|
15
|
+
params_len = len(signature.parameters)
|
|
16
|
+
return params_len == needed_len
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_last_param(signature: inspect.Signature):
|
|
20
|
+
"""
|
|
21
|
+
Нам нужно взять последний ключ в словаре,
|
|
22
|
+
но мы не можем сделать:
|
|
23
|
+
>>> dict.keys()[-1]
|
|
24
|
+
"""
|
|
25
|
+
name = next(reversed(signature.parameters.keys()))
|
|
26
|
+
return signature.parameters[name]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_signal_type(handler):
|
|
30
|
+
"""
|
|
31
|
+
Возвращает тип сигнала, на который реагирует функция.
|
|
32
|
+
:param handler: функция реакции
|
|
33
|
+
:return: тип сигнала
|
|
34
|
+
"""
|
|
35
|
+
signature = inspect.signature(handler)
|
|
36
|
+
assert validate_signature_length(signature), \
|
|
37
|
+
f'Reaction for event, must have only 1 parameter!'
|
|
38
|
+
|
|
39
|
+
argument = get_last_param(signature)
|
|
40
|
+
assert is_signal(argument.annotation)
|
|
41
|
+
|
|
42
|
+
return argument.annotation
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: classic-signals
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple signals and handlers
|
|
5
|
+
Home-page: https://github.com/variasov/classic-signals
|
|
6
|
+
Author: Sergei Variasov
|
|
7
|
+
Author-email: variasov@gmail.com
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/variasov/classic-signals/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: readerwriterlock~=1.0.9
|
|
17
|
+
Requires-Dist: classic-components==1.1.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest~=7.4.4; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-cov~=4.1; extra == "dev"
|
|
21
|
+
Requires-Dist: twine~=4.0; extra == "dev"
|
|
22
|
+
Requires-Dist: build~=1.0; extra == "dev"
|
|
23
|
+
|
|
24
|
+
# Classic Signals
|
|
25
|
+
|
|
26
|
+
Данный пакет предоставляет возможность использовать сигналы и реакции в проекте.
|
|
27
|
+
|
|
28
|
+
Они нужны для инверсии вызова. По сути, это паттерн Observer и немного сахара
|
|
29
|
+
сверху. В одном месте программы мы объявляем сигналы, пользуясь декоратором
|
|
30
|
+
signal, в других местах приложения пишем реакции на этих сигналов.
|
|
31
|
+
Реакции - это любой вызываемый объект, принимающий один аргумент - инстанс
|
|
32
|
+
сигнала. Это может быть и просто функция или метод, для удобства завернутый
|
|
33
|
+
в декоратор reaction. Последнее, что нужно, это инстанс класса Hub. В Hub
|
|
34
|
+
перед началом работы можно зарегистрировать реакции, потом можно вызвать у него
|
|
35
|
+
метод notify, передав в него инстанс сигнала, и Hub вызовет все реакции,
|
|
36
|
+
относящиеся к сигналу.
|
|
37
|
+
|
|
38
|
+
Пример:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from classic.components import component
|
|
42
|
+
from classic.signals import reaction, Hub, signal
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@signal
|
|
46
|
+
class SomethingHappened:
|
|
47
|
+
"""Под капотом сигналы - на самом деле датаклассы"""
|
|
48
|
+
some_field: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@signal
|
|
52
|
+
class SomethingAnotherHappened:
|
|
53
|
+
some_field: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@component
|
|
57
|
+
class SomeHandlers:
|
|
58
|
+
|
|
59
|
+
@reaction
|
|
60
|
+
def on_something_happened(self, signal: SomethingHappened):
|
|
61
|
+
print(f'Что-то произошло: {signal}')
|
|
62
|
+
|
|
63
|
+
@reaction
|
|
64
|
+
def on_something_another_happened(self, signal: SomethingAnotherHappened):
|
|
65
|
+
print(f'Что-то еще произошло: {signal}')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
hub = Hub()
|
|
69
|
+
SomeHandlers(signals=hub)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
signal_1 = SomethingHappened(some_field=0)
|
|
73
|
+
signal_2 = SomethingAnotherHappened(some_field=0)
|
|
74
|
+
|
|
75
|
+
hub.notify(signal_1, signal_2)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
setup.py
|
|
6
|
+
sources/classic/signals/__init__.py
|
|
7
|
+
sources/classic/signals/decorator.py
|
|
8
|
+
sources/classic/signals/hub.py
|
|
9
|
+
sources/classic/signals/reaction.py
|
|
10
|
+
sources/classic/signals/signal.py
|
|
11
|
+
sources/classic/signals/utils.py
|
|
12
|
+
sources/classic_signals.egg-info/PKG-INFO
|
|
13
|
+
sources/classic_signals.egg-info/SOURCES.txt
|
|
14
|
+
sources/classic_signals.egg-info/dependency_links.txt
|
|
15
|
+
sources/classic_signals.egg-info/requires.txt
|
|
16
|
+
sources/classic_signals.egg-info/top_level.txt
|
|
17
|
+
tests/test_hub.py
|
|
18
|
+
tests/test_reaction.py
|
|
19
|
+
tests/test_signal.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
classic
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
|
|
2
|
+
def check_all_reaction_called(handlers, some_signal, another_signal):
|
|
3
|
+
assert handlers.some_field is None
|
|
4
|
+
assert handlers.another_field is None
|
|
5
|
+
|
|
6
|
+
hub = handlers.hub
|
|
7
|
+
hub.notify(some_signal)
|
|
8
|
+
hub.notify(another_signal)
|
|
9
|
+
|
|
10
|
+
assert handlers.some_field == some_signal
|
|
11
|
+
assert handlers.another_field == another_signal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_nothing_is_called(handlers, some_signal, another_signal):
|
|
15
|
+
assert handlers.some_field is None
|
|
16
|
+
assert handlers.another_field is None
|
|
17
|
+
|
|
18
|
+
hub = handlers.hub
|
|
19
|
+
hub.notify(some_signal)
|
|
20
|
+
hub.notify(another_signal)
|
|
21
|
+
|
|
22
|
+
assert handlers.some_field is None
|
|
23
|
+
assert handlers.another_field is None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_composite_handlers(component_handlers, some_signal, another_signal):
|
|
27
|
+
check_all_reaction_called(component_handlers, some_signal, another_signal)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_manual_handlers_call_after_registration(
|
|
31
|
+
manual_handlers, some_signal, another_signal
|
|
32
|
+
):
|
|
33
|
+
manual_handlers.register()
|
|
34
|
+
check_all_reaction_called(manual_handlers, some_signal, another_signal)
|
|
35
|
+
manual_handlers.unregister()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_manual_handlers_dont_call_without_registration(
|
|
39
|
+
manual_handlers, some_signal, another_signal
|
|
40
|
+
):
|
|
41
|
+
check_nothing_is_called(manual_handlers, some_signal, another_signal)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_manual_handlers_dont_call_after_deregistration(
|
|
45
|
+
manual_handlers, some_signal, another_signal
|
|
46
|
+
):
|
|
47
|
+
manual_handlers.register()
|
|
48
|
+
manual_handlers.unregister()
|
|
49
|
+
check_nothing_is_called(manual_handlers, some_signal, another_signal)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_manual_handlers_call_after_manual_registration(
|
|
53
|
+
manual_handlers, some_signal, another_signal
|
|
54
|
+
):
|
|
55
|
+
manual_handlers.register_manually()
|
|
56
|
+
check_all_reaction_called(manual_handlers, some_signal, another_signal)
|
|
57
|
+
manual_handlers.unregister_manually()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_manual_handlers_dont_call_without_manual_registration(
|
|
61
|
+
manual_handlers, some_signal, another_signal
|
|
62
|
+
):
|
|
63
|
+
check_nothing_is_called(manual_handlers, some_signal, another_signal)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_manual_handlers_dont_call_after_manual_deregistration(
|
|
67
|
+
manual_handlers, some_signal, another_signal
|
|
68
|
+
):
|
|
69
|
+
manual_handlers.register_manually()
|
|
70
|
+
manual_handlers.unregister_manually()
|
|
71
|
+
check_nothing_is_called(manual_handlers, some_signal, another_signal)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_manual_handlers_call_after_manual_registration_without_signals(
|
|
75
|
+
manual_handlers, some_signal, another_signal
|
|
76
|
+
):
|
|
77
|
+
manual_handlers.register_manually_without_signals()
|
|
78
|
+
check_all_reaction_called(manual_handlers, some_signal, another_signal)
|
|
79
|
+
manual_handlers.unregister_manually_without_signals()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_manual_handlers_dont_call_without_manual_registration_without_signals(
|
|
83
|
+
manual_handlers, some_signal, another_signal
|
|
84
|
+
):
|
|
85
|
+
check_nothing_is_called(manual_handlers, some_signal, another_signal)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_manual_handlers_dont_call_after_manual_deregistration_without_signals(
|
|
89
|
+
manual_handlers, some_signal, another_signal
|
|
90
|
+
):
|
|
91
|
+
manual_handlers.register_manually_without_signals()
|
|
92
|
+
manual_handlers.unregister_manually_without_signals()
|
|
93
|
+
check_nothing_is_called(manual_handlers, some_signal, another_signal)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from classic.signals import Hub, is_reaction, filter_reactions
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_is_reaction_true(handlers):
|
|
5
|
+
assert is_reaction(handlers.reaction_1)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_is_reaction_false(handlers):
|
|
9
|
+
assert not is_reaction(handlers.reaction_3)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_filter_reactions(handlers):
|
|
13
|
+
reactions = filter_reactions(handlers)
|
|
14
|
+
assert len(reactions) == 2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_reaction_decorator(handlers):
|
|
18
|
+
assert getattr(handlers.reaction_1, '__is_reaction', False)
|
|
19
|
+
assert getattr(
|
|
20
|
+
handlers.reaction_1,
|
|
21
|
+
'__extra_annotations__',
|
|
22
|
+
) == {'hub': Hub}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import is_dataclass
|
|
2
|
+
|
|
3
|
+
from classic.signals import is_signal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SomeClass:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_signal_decorator(some_signal):
|
|
11
|
+
assert hasattr(some_signal, '__is_signal')
|
|
12
|
+
assert is_dataclass(some_signal)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_is_signal_true(some_signal):
|
|
16
|
+
assert is_signal(some_signal)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_is_signal_false():
|
|
20
|
+
some_class = SomeClass()
|
|
21
|
+
assert not is_signal(some_class)
|