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.
@@ -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,6 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools >= 40.6.0",
4
+ "wheel"
5
+ ]
6
+ build-backend = "setuptools.build_meta"
@@ -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,3 @@
1
+ from setuptools import setup
2
+
3
+ setup()
@@ -0,0 +1,4 @@
1
+ from .signal import Signal, signal, is_signal
2
+ from .reaction import is_reaction, filter_reactions
3
+ from .hub import Hub
4
+ from .decorator import reaction
@@ -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,8 @@
1
+ readerwriterlock~=1.0.9
2
+ classic-components==1.1.0
3
+
4
+ [dev]
5
+ pytest~=7.4.4
6
+ pytest-cov~=4.1
7
+ twine~=4.0
8
+ build~=1.0
@@ -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)