python-statemachine 2.1.0__py3-none-any.whl → 2.3.5__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-statemachine
3
- Version: 2.1.0
3
+ Version: 2.3.5
4
4
  Summary: Python Finite State Machines made easy.
5
5
  Home-page: https://github.com/fgmacedo/python-statemachine
6
6
  License: MIT
@@ -8,7 +8,8 @@ Author: Fernando Macedo
8
8
  Author-email: fgmacedo@gmail.com
9
9
  Maintainer: Fernando Macedo
10
10
  Maintainer-email: fgmacedo@gmail.com
11
- Requires-Python: >=3.7,<3.12
11
+ Requires-Python: >=3.7
12
+ Classifier: Framework :: AsyncIO
12
13
  Classifier: Intended Audience :: Developers
13
14
  Classifier: License :: OSI Approved :: MIT License
14
15
  Classifier: Natural Language :: English
@@ -18,11 +19,8 @@ Classifier: Programming Language :: Python :: 3.8
18
19
  Classifier: Programming Language :: Python :: 3.9
19
20
  Classifier: Programming Language :: Python :: 3.10
20
21
  Classifier: Programming Language :: Python :: 3.11
21
- Classifier: Programming Language :: Python :: 3.10
22
- Classifier: Programming Language :: Python :: 3.11
23
- Classifier: Programming Language :: Python :: 3.7
24
- Classifier: Programming Language :: Python :: 3.8
25
- Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
26
24
  Classifier: Topic :: Software Development :: Libraries
27
25
  Provides-Extra: diagrams
28
26
  Description-Content-Type: text/markdown
@@ -30,8 +28,8 @@ Description-Content-Type: text/markdown
30
28
  # Python StateMachine
31
29
 
32
30
  [![pypi](https://img.shields.io/pypi/v/python-statemachine.svg)](https://pypi.python.org/pypi/python-statemachine)
31
+ [![downloads total](https://static.pepy.tech/badge/python-statemachine)](https://pepy.tech/project/python-statemachine)
33
32
  [![downloads](https://img.shields.io/pypi/dm/python-statemachine.svg)](https://pypi.python.org/pypi/python-statemachine)
34
- [![build status](https://github.com/fgmacedo/python-statemachine/actions/workflows/python-package.yml/badge.svg?branch=develop)](https://github.com/fgmacedo/python-statemachine/actions/workflows/python-package.yml?query=branch%3Adevelop)
35
33
  [![Coverage report](https://codecov.io/gh/fgmacedo/python-statemachine/branch/develop/graph/badge.svg)](https://codecov.io/gh/fgmacedo/python-statemachine)
36
34
  [![Documentation Status](https://readthedocs.org/projects/python-statemachine/badge/?version=latest)](https://python-statemachine.readthedocs.io/en/latest/?badge=latest)
37
35
  [![GitHub commits since last release (main)](https://img.shields.io/github/commits-since/fgmacedo/python-statemachine/main/develop)](https://github.com/fgmacedo/python-statemachine/compare/main...develop)
@@ -39,33 +37,43 @@ Description-Content-Type: text/markdown
39
37
 
40
38
  Python [finite-state machines](https://en.wikipedia.org/wiki/Finite-state_machine) made easy.
41
39
 
40
+ <div align="center">
42
41
 
43
- * Free software: MIT license
44
- * Documentation: https://python-statemachine.readthedocs.io.
45
-
42
+ ![](https://github.com/fgmacedo/python-statemachine/blob/develop/docs/images/python-statemachine.png?raw=true)
46
43
 
47
- Welcome to python-statemachine, an intuitive and powerful state machine framework designed for a
48
- great developer experience.
44
+ </div>
49
45
 
50
- 🚀 With StateMachine, you can easily create complex, dynamic systems with clean, readable code.
46
+ Welcome to python-statemachine, an intuitive and powerful state machine library designed for a
47
+ great developer experience. We provide a _pythonic_ and expressive API for implementing state
48
+ machines in sync or asynchonous Python codebases.
51
49
 
52
- 💡 Our framework makes it easy to understand and reason about the different states, events and
53
- transitions in your system, so you can focus on building great products.
50
+ ## Features
54
51
 
55
- 🔒 python-statemachine also provides robust error handling and ensures that your system stays
56
- in a valid state at all times.
52
+ - **Basic components**: Easily define **States**, **Events**, and **Transitions** to model your logic.
53
+ - ⚙️ **Actions and handlers**: Attach actions and handlers to states, events, and transitions to control behavior dynamically.
54
+ - 🛡️ **Conditional transitions**: Implement **Guards** and **Validators** to conditionally control transitions, ensuring they only occur when specific conditions are met.
55
+ - 🚀 **Full async support**: Enjoy full asynchronous support. Await events, and dispatch callbacks asynchronously for seamless integration with async codebases.
56
+ - 🔄 **Full sync support**: Use the same state machine from synchronous codebases without any modifications.
57
+ - 🎨 **Declarative and simple API**: Utilize a clean, elegant, and readable API to define your state machine, making it easy to maintain and understand.
58
+ - 👀 **Observer pattern support**: Register external and generic objects to watch events and register callbacks.
59
+ - 🔍 **Decoupled design**: Separate concerns with a decoupled "state machine" and "model" design, promoting cleaner architecture and easier maintenance.
60
+ - ✅ **Correctness guarantees**: Ensured correctness with validations at class definition time:
61
+ - Ensures exactly one `initial` state.
62
+ - Disallows transitions from `final` states.
63
+ - Requires ongoing transitions for all non-final states.
64
+ - Guarantees all non-final states have at least one path to a final state if final states are declared.
65
+ - Validates the state machine graph representation has a single component.
66
+ - 📦 **Flexible event dispatching**: Dispatch events with any extra data, making it available to all callbacks, including actions and guards.
67
+ - 🔧 **Dependency injection**: Needed parameters are injected into callbacks.
68
+ - 📊 **Graphical representation**: Generate and output graphical representations of state machines. Create diagrams from the command line, at runtime, or even in Jupyter notebooks.
69
+ - 🌍 **Internationalization support**: Provides error messages in different languages, making the library accessible to a global audience.
70
+ - 🛡️ **Robust testing**: Ensured reliability with a codebase that is 100% covered by automated tests, including all docs examples. Releases follow semantic versioning for predictable releases.
71
+ - 🏛️ **Domain model integration**: Seamlessly integrate with domain models using Mixins.
72
+ - 🔧 **Django integration**: Automatically discover state machines in Django applications.
57
73
 
58
74
 
59
- A few reasons why you may consider using it:
60
-
61
- * 📈 python-statemachine is designed to help you build scalable,
62
- maintainable systems that can handle any complexity.
63
- * 💪 You can easily create and manage multiple state machines within a single application.
64
- * 🚫 Prevents common mistakes and ensures that your system stays in a valid state at all times.
65
-
66
-
67
- ## Getting started
68
75
 
76
+ ## Installing
69
77
 
70
78
  To install Python State Machine, run this command in your terminal:
71
79
 
@@ -77,6 +85,8 @@ our docs for more details.
77
85
 
78
86
  pip install python-statemachine[diagrams]
79
87
 
88
+ ## First example
89
+
80
90
  Define your state machine:
81
91
 
82
92
  ```py
@@ -137,7 +147,7 @@ Then start sending events to your new state machine:
137
147
 
138
148
  ```
139
149
 
140
- That's it. This is all an external object needs to know about your state machine: How to send events.
150
+ **That's it.** This is all an external object needs to know about your state machine: How to send events.
141
151
  Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.
142
152
 
143
153
  But if your use case needs, you can inspect state machine properties, like the current state:
@@ -269,6 +279,34 @@ and in diagrams:
269
279
 
270
280
  ```
271
281
 
282
+ ## Async support
283
+
284
+ We support native coroutine using `asyncio`, enabling seamless integration with asynchronous code.
285
+ There's no change on the public API of the library to work on async codebases.
286
+
287
+
288
+ ```py
289
+ >>> class AsyncStateMachine(StateMachine):
290
+ ... initial = State('Initial', initial=True)
291
+ ... final = State('Final', final=True)
292
+ ...
293
+ ... advance = initial.to(final)
294
+ ...
295
+ ... async def on_advance(self):
296
+ ... return 42
297
+
298
+ >>> async def run_sm():
299
+ ... sm = AsyncStateMachine()
300
+ ... result = await sm.advance()
301
+ ... print(f"Result is {result}")
302
+ ... print(sm.current_state)
303
+
304
+ >>> asyncio.run(run_sm())
305
+ Result is 42
306
+ Final
307
+
308
+ ```
309
+
272
310
  ## A more useful example
273
311
 
274
312
  A simple didactic state machine for controlling an `Order`:
@@ -375,7 +413,7 @@ https://python-statemachine.readthedocs.io.
375
413
  - If you found this project helpful, please consider giving it a star on GitHub.
376
414
 
377
415
  - **Contribute code**: If you would like to contribute code to this project, please submit a pull
378
- request. For more information on how to contribute, please see our [contributing.md]contributing.md) file.
416
+ request. For more information on how to contribute, please see our [contributing.md](contributing.md) file.
379
417
 
380
418
  - **Report bugs**: If you find any bugs in this project, please report them by opening an issue
381
419
  on our GitHub issue tracker.
@@ -0,0 +1,32 @@
1
+ statemachine/__init__.py,sha256=ZSwV-f_lq_MIAG00F_PCnS-514sn_jf-6uykaeGD5Ug,192
2
+ statemachine/callbacks.py,sha256=5_WaILrjUvZOYX_LQe4Gd8nsCvkErjw9DaHYG_ggvxs,11101
3
+ statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ statemachine/contrib/diagram.py,sha256=eK3unncEXNq3e_yfBTZmvgdWWWDZq69r7gsHvCHr5Ys,7208
5
+ statemachine/dispatcher.py,sha256=2DtjCj_kiDF2uk_EEvXxs5NTABOvCr4paxVz77s6mXA,5365
6
+ statemachine/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ statemachine/engines/async_.py,sha256=Z-AS4zQ2xDj2uexJ7fepQTSaw5PjqesemWGXX9g0rpU,5546
8
+ statemachine/engines/sync.py,sha256=bYCyzoYo03LLfxzUl2XyyNlmMmxDLn_w0AMa5IHCF3U,5497
9
+ statemachine/event.py,sha256=LfvO-imBVyVX6krBK0Pz7q-iTRudo_5OjTFaYJpc6IQ,1556
10
+ statemachine/event_data.py,sha256=x9m9Q2q-78kVFnJVJmOAwq6lo9zgSVxko3q4Omwjqxk,2228
11
+ statemachine/events.py,sha256=QgXcxu25WkUaxJvonh0K7hOlYYSQlV0jbhTCAi_gpwc,736
12
+ statemachine/exceptions.py,sha256=RWq1vTjajxt8CzfYCD4DfI2nrgkfCPBL0DjRDOzKkQI,1086
13
+ statemachine/factory.py,sha256=8rRvDXWGjEX6v6mP7YiXOJPTCcm-GhDmuTvyotw1zoc,7948
14
+ statemachine/graph.py,sha256=KtwB1CYckaLjTgQD9tEeuaEzJje9q3fPVpBViW5TgSk,487
15
+ statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
16
+ statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=zJYaV2DxrHdPngDOt1bji8-lPOZMSDjWG83Ypru5DOI,2126
17
+ statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=SPrt-KaCBCKQq7PoOMtyyqU1bihrw_3MPL_NbqZzTVs,3214
18
+ statemachine/mixins.py,sha256=Y1fa52Cj20JaGkyNk3P7Gpqkt4cGTjJ0YyV_VQyCl0M,1231
19
+ statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
20
+ statemachine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ statemachine/registry.py,sha256=HmV9sUGkYVrNUZxJYoZo-trSUis7dIun_WcGktblgc8,922
22
+ statemachine/signature.py,sha256=HwuyWX3p_GZwKKrGbt9wk4-bHpymqs_pgqW7OlDgGRc,7877
23
+ statemachine/state.py,sha256=IKdkMBkXCgcvswMKQZ-hn0-739Hhf6z6q8mSEoevV7o,8262
24
+ statemachine/statemachine.py,sha256=WgI4LbnOyOBl8OZqBXFRYETtt8rQi0XEhQN-D_2oMFM,11536
25
+ statemachine/states.py,sha256=vpRNjiGmTDHvE0fn0gwdq8-WcG32n4ZiPTU-kgZ73rc,4870
26
+ statemachine/transition.py,sha256=R7gfSZjqR59OIdpKePgdr66zTp5azow-tDqqYJsECR4,4711
27
+ statemachine/transition_list.py,sha256=_tdFD1O7hw8xVAU6nRYmGrj6TCAM0SD-Sa0_DYKa88Y,6034
28
+ statemachine/utils.py,sha256=DpcrGqlbrnT-ogh-BogG0L07EG3KirHOsKORHlspDlI,1041
29
+ python_statemachine-2.3.5.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
30
+ python_statemachine-2.3.5.dist-info/METADATA,sha256=CWbBQMeVziWU_TJdMiH67aDjn-Tw7OeOXVSFRzXO83Y,13983
31
+ python_statemachine-2.3.5.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
32
+ python_statemachine-2.3.5.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.4.0
2
+ Generator: poetry-core 1.6.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
statemachine/__init__.py CHANGED
@@ -3,6 +3,6 @@ from .statemachine import StateMachine
3
3
 
4
4
  __author__ = """Fernando Macedo"""
5
5
  __email__ = "fgmacedo@gmail.com"
6
- __version__ = "2.1.0"
6
+ __version__ = "2.3.5"
7
7
 
8
8
  __all__ = ["StateMachine", "State"]
statemachine/callbacks.py CHANGED
@@ -1,42 +1,116 @@
1
+ import asyncio
2
+ from bisect import insort
3
+ from collections import defaultdict
4
+ from collections import deque
5
+ from enum import IntEnum
6
+ from enum import IntFlag
7
+ from enum import auto
8
+ from inspect import isawaitable
9
+ from inspect import iscoroutinefunction
10
+ from typing import Callable
11
+ from typing import Dict
12
+ from typing import Generator
13
+ from typing import Iterable
14
+ from typing import List
15
+ from typing import Type
16
+
1
17
  from .exceptions import AttrNotFound
2
- from .exceptions import InvalidDefinition
3
18
  from .i18n import _
4
19
  from .utils import ensure_iterable
5
20
 
6
21
 
7
- class CallbackWrapper:
8
- """A thin wrapper that ensures the target callback is a proper callable.
22
+ class CallbackPriority(IntEnum):
23
+ GENERIC = 0
24
+ INLINE = 10
25
+ DECORATOR = 20
26
+ NAMING = 30
27
+ AFTER = 40
28
+
29
+
30
+ class SpecReference(IntFlag):
31
+ NAME = auto()
32
+ CALLABLE = auto()
33
+ PROPERTY = auto()
34
+
35
+
36
+ SPECS_ALL = SpecReference.NAME | SpecReference.CALLABLE | SpecReference.PROPERTY
37
+ SPECS_SAFE = SpecReference.NAME
38
+
39
+
40
+ class CallbackGroup(IntEnum):
41
+ ENTER = auto()
42
+ EXIT = auto()
43
+ VALIDATOR = auto()
44
+ BEFORE = auto()
45
+ ON = auto()
46
+ AFTER = auto()
47
+ COND = auto()
48
+
49
+ def build_key(self, specs: "CallbackSpecList") -> str:
50
+ return f"{self.name}@{id(specs)}"
51
+
52
+
53
+ def allways_true(*args, **kwargs):
54
+ return True
9
55
 
10
- At first, `func` can be a string or a callable, and even if it's already
11
- a callable, his signature can mismatch.
12
56
 
13
- After instantiation, `.setup(resolver)` must be called before any real
14
- call is performed, to allow the proper callback resolution.
57
+ class CallbackSpec:
58
+ """Specs about callbacks.
59
+
60
+ At first, `func` can be a name (string), a property or a callable.
61
+
62
+ Names, properties and unbounded callables should be resolved to a callable
63
+ before any real call is performed.
15
64
  """
16
65
 
17
- def __init__(self, func, suppress_errors=False, cond=None):
66
+ def __init__(
67
+ self,
68
+ func,
69
+ group: CallbackGroup,
70
+ is_convention=False,
71
+ cond=None,
72
+ priority: CallbackPriority = CallbackPriority.NAMING,
73
+ expected_value=None,
74
+ ):
18
75
  self.func = func
19
- self.suppress_errors = suppress_errors
20
- self.cond = Callbacks(factory=ConditionWrapper).add(cond)
21
- self._callback = None
22
- self._resolver_id = None
76
+ self.group = group
77
+ self.is_convention = is_convention
78
+ self.cond = cond
79
+ self.expected_value = expected_value
80
+ self.priority = priority
81
+
82
+ if isinstance(func, property):
83
+ self.reference = SpecReference.PROPERTY
84
+ self.attr_name: str = func and func.fget and func.fget.__name__ or ""
85
+ elif callable(func):
86
+ self.reference = SpecReference.CALLABLE
87
+ self.is_bounded = hasattr(func, "__self__")
88
+ self.attr_name = func.__name__
89
+ else:
90
+ self.reference = SpecReference.NAME
91
+ self.attr_name = func
23
92
 
24
93
  def __repr__(self):
25
- return f"{type(self).__name__}({self.func!r})"
94
+ return f"{type(self).__name__}({self.func!r}, is_convention={self.is_convention!r})"
26
95
 
27
96
  def __str__(self):
28
- return getattr(self.func, "__name__", self.func)
97
+ name = getattr(self.func, "__name__", self.func)
98
+ if self.expected_value is False:
99
+ name = f"!{name}"
100
+ return name
29
101
 
30
102
  def __eq__(self, other):
31
- return self.func == other.func and self._resolver_id == other._resolver_id
103
+ return self.func == other.func and self.group == other.group
32
104
 
33
105
  def __hash__(self):
34
106
  return id(self)
35
107
 
36
- def _update_func(self, func):
108
+ def _update_func(self, func: Callable, attr_name: str):
37
109
  self.func = func
110
+ self.reference = SpecReference.CALLABLE
111
+ self.attr_name = attr_name
38
112
 
39
- def setup(self, resolver):
113
+ def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
40
114
  """
41
115
  Resolves the `func` into a usable callable.
42
116
 
@@ -44,56 +118,57 @@ class CallbackWrapper:
44
118
  resolver (callable): A method responsible to build and return a valid callable that
45
119
  can receive arbitrary parameters like `*args, **kwargs`.
46
120
  """
47
- self.cond.setup(resolver)
48
- try:
49
- self._resolver_id = getattr(resolver, "id", id(resolver))
50
- self._callback = resolver(self.func)
51
- return True
52
- except AttrNotFound:
53
- if not self.suppress_errors:
54
- raise
55
- return False
56
-
57
- def __call__(self, *args, **kwargs):
58
- if self._callback is None:
59
- raise InvalidDefinition(
60
- _("Callback {!r} not property configured.").format(self)
121
+ for callback in resolver.search(self):
122
+ condition = self.cond if self.cond is not None else allways_true
123
+ yield CallbackWrapper(
124
+ callback=callback,
125
+ condition=condition,
126
+ meta=self,
127
+ unique_key=callback.unique_key,
61
128
  )
62
- return self._callback(*args, **kwargs)
63
129
 
64
130
 
65
- class ConditionWrapper(CallbackWrapper):
66
- def __init__(self, func, suppress_errors=False, expected_value=True):
67
- super().__init__(func, suppress_errors)
68
- self.expected_value = expected_value
131
+ class SpecListGrouper:
132
+ def __init__(
133
+ self, list: "CallbackSpecList", group: CallbackGroup, factory=CallbackSpec
134
+ ) -> None:
135
+ self.list = list
136
+ self.group = group
137
+ self.factory = factory
138
+ self.key = group.build_key(list)
69
139
 
70
- def __str__(self):
71
- name = super().__str__()
72
- return name if self.expected_value else f"!{name}"
140
+ def add(self, callbacks, **kwargs):
141
+ self.list.add(callbacks, group=self.group, factory=self.factory, **kwargs)
142
+ return self
143
+
144
+ def __call__(self, callback):
145
+ return self.list._add_unbounded_callback(callback, group=self.group, factory=self.factory)
73
146
 
74
- def __call__(self, *args, **kwargs):
75
- return bool(super().__call__(*args, **kwargs)) == self.expected_value
147
+ def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
148
+ self.list._add_unbounded_callback(
149
+ func,
150
+ is_event=is_event,
151
+ transitions=transitions,
152
+ group=self.group,
153
+ factory=self.factory,
154
+ **kwargs,
155
+ )
76
156
 
157
+ def __iter__(self):
158
+ return (item for item in self.list if item.group == self.group)
77
159
 
78
- class Callbacks:
79
- def __init__(self, resolver=None, factory=CallbackWrapper):
80
- self.items = []
81
- self._resolver = resolver
160
+
161
+ class CallbackSpecList:
162
+ """List of {ref}`CallbackSpec` instances"""
163
+
164
+ def __init__(self, factory=CallbackSpec):
165
+ self.items: List[CallbackSpec] = []
166
+ self.conventional_specs = set()
82
167
  self.factory = factory
83
168
 
84
169
  def __repr__(self):
85
170
  return f"{type(self).__name__}({self.items!r}, factory={self.factory!r})"
86
171
 
87
- def __str__(self):
88
- return ", ".join(str(c) for c in self)
89
-
90
- def setup(self, resolver):
91
- """Validate configurations"""
92
- self._resolver = resolver
93
- self.items = [
94
- callback for callback in self.items if callback.setup(self._resolver)
95
- ]
96
-
97
172
  def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
98
173
  """This list was a target for adding a func using decorator
99
174
  `@<state|event>[.on|before|after|enter|exit]` syntax.
@@ -117,56 +192,175 @@ class Callbacks:
117
192
  event.
118
193
 
119
194
  """
120
- callback = self._add(func, **kwargs)
121
- if not getattr(func, "_callbacks_to_update", None):
122
- func._callbacks_to_update = set()
123
- func._callbacks_to_update.add(callback._update_func)
124
- func._is_event = is_event
195
+ spec = self._add(func, **kwargs)
196
+ if not getattr(func, "_specs_to_update", None):
197
+ func._specs_to_update = set()
198
+ if is_event:
199
+ func._specs_to_update.add(spec._update_func)
125
200
  func._transitions = transitions
126
201
 
127
202
  return func
128
203
 
129
- def __call__(self, callback):
130
- return self._add_unbounded_callback(callback)
131
-
132
204
  def __iter__(self):
133
205
  return iter(self.items)
134
206
 
135
207
  def clear(self):
136
208
  self.items = []
137
209
 
138
- def call(self, *args, **kwargs):
139
- return [
140
- callback(*args, **kwargs)
141
- for callback in self.items
142
- if callback.cond.all(*args, **kwargs)
143
- ]
210
+ def grouper(
211
+ self, group: CallbackGroup, factory: Type[CallbackSpec] = CallbackSpec
212
+ ) -> SpecListGrouper:
213
+ return SpecListGrouper(self, group, factory=factory)
144
214
 
145
- def all(self, *args, **kwargs):
146
- return all(condition(*args, **kwargs) for condition in self)
215
+ def _add(self, func, group: CallbackGroup, factory=None, **kwargs):
216
+ if factory is None:
217
+ factory = self.factory
218
+ spec = factory(func, group, **kwargs)
147
219
 
148
- def _add(self, func, resolver=None, prepend=False, **kwargs):
149
- resolver = resolver or self._resolver
150
-
151
- callback = self.factory(func, **kwargs)
152
- if resolver is not None and not callback.setup(resolver):
220
+ if spec in self.items:
153
221
  return
154
222
 
155
- if callback in self.items:
156
- return
223
+ self.items.append(spec)
224
+ if spec.is_convention:
225
+ self.conventional_specs.add(spec.func)
226
+ return spec
157
227
 
158
- if prepend:
159
- self.items.insert(0, callback)
160
- else:
161
- self.items.append(callback)
162
- return callback
163
-
164
- def add(self, callbacks, **kwargs):
228
+ def add(self, callbacks, group: CallbackGroup, **kwargs):
165
229
  if callbacks is None:
166
230
  return self
167
231
 
168
232
  unprepared = ensure_iterable(callbacks)
169
233
  for func in unprepared:
170
- self._add(func, **kwargs)
234
+ self._add(func, group=group, **kwargs)
171
235
 
172
236
  return self
237
+
238
+
239
+ class CallbackWrapper:
240
+ def __init__(
241
+ self,
242
+ callback: Callable,
243
+ condition: Callable,
244
+ meta: "CallbackSpec",
245
+ unique_key: str,
246
+ ) -> None:
247
+ self._callback = callback
248
+ self._iscoro = iscoroutinefunction(callback)
249
+ self.condition = condition
250
+ self.meta = meta
251
+ self.unique_key = unique_key
252
+ self.expected_value = self.meta.expected_value
253
+
254
+ def __repr__(self):
255
+ return f"{type(self).__name__}({self.unique_key})"
256
+
257
+ def __str__(self):
258
+ return str(self.meta)
259
+
260
+ def __lt__(self, other):
261
+ return self.meta.priority < other.meta.priority
262
+
263
+ async def __call__(self, *args, **kwargs):
264
+ value = self._callback(*args, **kwargs)
265
+ if isawaitable(value):
266
+ value = await value
267
+
268
+ if self.expected_value is not None:
269
+ return bool(value) == self.expected_value
270
+ return value
271
+
272
+ def call(self, *args, **kwargs):
273
+ value = self._callback(*args, **kwargs)
274
+ if self.expected_value is not None:
275
+ return bool(value) == self.expected_value
276
+ return value
277
+
278
+
279
+ class CallbacksExecutor:
280
+ """A list of callbacks that can be executed in order."""
281
+
282
+ def __init__(self):
283
+ self.items: List[CallbackWrapper] = deque()
284
+ self.items_already_seen = set()
285
+
286
+ def __iter__(self):
287
+ return iter(self.items)
288
+
289
+ def __repr__(self):
290
+ return f"{type(self).__name__}({self.items!r})"
291
+
292
+ def __str__(self):
293
+ return ", ".join(str(c) for c in self)
294
+
295
+ def _add(self, spec: CallbackSpec, resolver: Callable):
296
+ for callback in spec.build(resolver):
297
+ if callback.unique_key in self.items_already_seen:
298
+ continue
299
+
300
+ self.items_already_seen.add(callback.unique_key)
301
+ insort(self.items, callback)
302
+
303
+ def add(self, items: Iterable[CallbackSpec], resolver: Callable):
304
+ """Validate configurations"""
305
+ for item in items:
306
+ self._add(item, resolver)
307
+ return self
308
+
309
+ async def async_call(self, *args, **kwargs):
310
+ return await asyncio.gather(
311
+ *(
312
+ callback(*args, **kwargs)
313
+ for callback in self
314
+ if callback.condition(*args, **kwargs)
315
+ )
316
+ )
317
+
318
+ async def async_all(self, *args, **kwargs):
319
+ coros = [condition(*args, **kwargs) for condition in self]
320
+ for coro in asyncio.as_completed(coros):
321
+ if not await coro:
322
+ return False
323
+ return True
324
+
325
+ def call(self, *args, **kwargs):
326
+ return [
327
+ callback.call(*args, **kwargs)
328
+ for callback in self
329
+ if callback.condition(*args, **kwargs)
330
+ ]
331
+
332
+ def all(self, *args, **kwargs):
333
+ for condition in self:
334
+ if not condition.call(*args, **kwargs):
335
+ return False
336
+ return True
337
+
338
+
339
+ class CallbacksRegistry:
340
+ def __init__(self) -> None:
341
+ self._registry: Dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
342
+ self.has_async_callbacks: bool = False
343
+
344
+ def clear(self):
345
+ self._registry.clear()
346
+
347
+ def __getitem__(self, key: str) -> CallbacksExecutor:
348
+ return self._registry[key]
349
+
350
+ def check(self, specs: CallbackSpecList):
351
+ for meta in specs:
352
+ if meta.is_convention:
353
+ continue
354
+
355
+ if any(
356
+ callback for callback in self[meta.group.build_key(specs)] if callback.meta == meta
357
+ ):
358
+ continue
359
+ raise AttrNotFound(
360
+ _("Did not found name '{}' from model or statemachine").format(meta.func)
361
+ )
362
+
363
+ def async_or_sync(self):
364
+ self.has_async_callbacks = any(
365
+ callback._iscoro for executor in self._registry.values() for callback in executor
366
+ )