python-statemachine 2.1.2__py3-none-any.whl → 2.3.0__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.2
3
+ Version: 2.3.0
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.13
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
@@ -26,8 +27,8 @@ Description-Content-Type: text/markdown
26
27
  # Python StateMachine
27
28
 
28
29
  [![pypi](https://img.shields.io/pypi/v/python-statemachine.svg)](https://pypi.python.org/pypi/python-statemachine)
30
+ [![downloads total](https://static.pepy.tech/badge/python-statemachine)](https://pepy.tech/project/python-statemachine)
29
31
  [![downloads](https://img.shields.io/pypi/dm/python-statemachine.svg)](https://pypi.python.org/pypi/python-statemachine)
30
- [![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)
31
32
  [![Coverage report](https://codecov.io/gh/fgmacedo/python-statemachine/branch/develop/graph/badge.svg)](https://codecov.io/gh/fgmacedo/python-statemachine)
32
33
  [![Documentation Status](https://readthedocs.org/projects/python-statemachine/badge/?version=latest)](https://python-statemachine.readthedocs.io/en/latest/?badge=latest)
33
34
  [![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)
@@ -35,33 +36,43 @@ Description-Content-Type: text/markdown
35
36
 
36
37
  Python [finite-state machines](https://en.wikipedia.org/wiki/Finite-state_machine) made easy.
37
38
 
39
+ <div align="center">
38
40
 
39
- * Free software: MIT license
40
- * Documentation: https://python-statemachine.readthedocs.io.
41
+ ![](https://github.com/fgmacedo/python-statemachine/blob/develop/docs/images/python-statemachine.png?raw=true)
41
42
 
43
+ </div>
42
44
 
43
- Welcome to python-statemachine, an intuitive and powerful state machine framework designed for a
44
- great developer experience.
45
+ Welcome to python-statemachine, an intuitive and powerful state machine library designed for a
46
+ great developer experience. We provide an _pythonic_ and expressive API for implementing state
47
+ machines in sync or asynchonous Python codebases.
45
48
 
46
- 🚀 With StateMachine, you can easily create complex, dynamic systems with clean, readable code.
49
+ ## Features
47
50
 
48
- 💡 Our framework makes it easy to understand and reason about the different states, events and
49
- transitions in your system, so you can focus on building great products.
51
+ - **Basic components**: Easily define **States**, **Events**, and **Transitions** to model your logic.
52
+ - ⚙️ **Actions and handlers**: Attach actions and handlers to states, events, and transitions to control behavior dynamically.
53
+ - 🛡️ **Conditional transitions**: Implement **Guards** and **Validators** to conditionally control transitions, ensuring they only occur when specific conditions are met.
54
+ - 🚀 **Full async support**: Enjoy full asynchronous support. Await events, and dispatch callbacks asynchronously for seamless integration with async codebases.
55
+ - 🔄 **Full sync support**: Use the same state machine from synchronous codebases without any modifications.
56
+ - 🎨 **Declarative and simple API**: Utilize a clean, elegant, and readable API to define your state machine, making it easy to maintain and understand.
57
+ - 👀 **Observer pattern support**: Register external and generic objects to watch events and register callbacks.
58
+ - 🔍 **Decoupled design**: Separate concerns with a decoupled "state machine" and "model" design, promoting cleaner architecture and easier maintenance.
59
+ - ✅ **Correctness guarantees**: Ensured correctness with validations at class definition time:
60
+ - Ensures exactly one `initial` state.
61
+ - Disallows transitions from `final` states.
62
+ - Requires ongoing transitions for all non-final states.
63
+ - Guarantees all non-final states have at least one path to a final state if final states are declared.
64
+ - Validates the state machine graph representation has a single component.
65
+ - 📦 **Flexible event dispatching**: Dispatch events with any extra data, making it available to all callbacks, including actions and guards.
66
+ - 🔧 **Dependency injection**: Needed parameters are injected into callbacks.
67
+ - 📊 **Graphical representation**: Generate and output graphical representations of state machines. Create diagrams from the command line, at runtime, or even in Jupyter notebooks.
68
+ - 🌍 **Internationalization support**: Provides error messages in different languages, making the library accessible to a global audience.
69
+ - 🛡️ **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.
70
+ - 🏛️ **Domain model integration**: Seamlessly integrate with domain models using Mixins.
71
+ - 🔧 **Django integration**: Automatically discover state machines in Django applications.
50
72
 
51
- 🔒 python-statemachine also provides robust error handling and ensures that your system stays
52
- in a valid state at all times.
53
73
 
54
74
 
55
- A few reasons why you may consider using it:
56
-
57
- * 📈 python-statemachine is designed to help you build scalable,
58
- maintainable systems that can handle any complexity.
59
- * 💪 You can easily create and manage multiple state machines within a single application.
60
- * 🚫 Prevents common mistakes and ensures that your system stays in a valid state at all times.
61
-
62
-
63
- ## Getting started
64
-
75
+ ## Installing
65
76
 
66
77
  To install Python State Machine, run this command in your terminal:
67
78
 
@@ -73,6 +84,8 @@ our docs for more details.
73
84
 
74
85
  pip install python-statemachine[diagrams]
75
86
 
87
+ ## First example
88
+
76
89
  Define your state machine:
77
90
 
78
91
  ```py
@@ -90,7 +103,7 @@ Define your state machine:
90
103
  ... | red.to(green)
91
104
  ... )
92
105
  ...
93
- ... def before_cycle(self, event: str, source: State, target: State, message: str = ""):
106
+ ... async def before_cycle(self, event: str, source: State, target: State, message: str = ""):
94
107
  ... message = ". " + message if message else ""
95
108
  ... return f"Running {event} from {source.id} to {target.id}{message}"
96
109
  ...
@@ -133,7 +146,27 @@ Then start sending events to your new state machine:
133
146
 
134
147
  ```
135
148
 
136
- That's it. This is all an external object needs to know about your state machine: How to send events.
149
+ You can use the exactly same state machine from an async codebase:
150
+
151
+
152
+ ```py
153
+ >>> async def run_sm():
154
+ ... asm = TrafficLightMachine()
155
+ ... results = []
156
+ ... for _i in range(4):
157
+ ... result = await asm.send("cycle")
158
+ ... results.append(result)
159
+ ... return results
160
+
161
+ >>> asyncio.run(run_sm())
162
+ Don't move.
163
+ Go ahead!
164
+ ['Running cycle from green to yellow', 'Running cycle from yellow to red', ...
165
+
166
+ ```
167
+
168
+
169
+ **That's it.** This is all an external object needs to know about your state machine: How to send events.
137
170
  Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.
138
171
 
139
172
  But if your use case needs, you can inspect state machine properties, like the current state:
@@ -220,7 +253,7 @@ callback method.
220
253
  Note how `before_cycle` was declared:
221
254
 
222
255
  ```py
223
- def before_cycle(self, event: str, source: State, target: State, message: str = ""):
256
+ async def before_cycle(self, event: str, source: State, target: State, message: str = ""):
224
257
  message = ". " + message if message else ""
225
258
  return f"Running {event} from {source.id} to {target.id}{message}"
226
259
  ```
@@ -265,6 +298,34 @@ and in diagrams:
265
298
 
266
299
  ```
267
300
 
301
+ ## Async support
302
+
303
+ We support native coroutine using `asyncio`, enabling seamless integration with asynchronous code.
304
+ There's no change on the public API of the library to work on async codebases.
305
+
306
+
307
+ ```py
308
+ >>> class AsyncStateMachine(StateMachine):
309
+ ... initial = State('Initial', initial=True)
310
+ ... final = State('Final', final=True)
311
+ ...
312
+ ... advance = initial.to(final)
313
+ ...
314
+ ... async def on_advance(self):
315
+ ... return 42
316
+
317
+ >>> async def run_sm():
318
+ ... sm = AsyncStateMachine()
319
+ ... result = await sm.advance()
320
+ ... print(f"Result is {result}")
321
+ ... print(sm.current_state)
322
+
323
+ >>> asyncio.run(run_sm())
324
+ Result is 42
325
+ Final
326
+
327
+ ```
328
+
268
329
  ## A more useful example
269
330
 
270
331
  A simple didactic state machine for controlling an `Order`:
@@ -0,0 +1,28 @@
1
+ statemachine/__init__.py,sha256=Omi4-IXrTE0oLfO3oF75OqMYBI9TrbZeBzZIkt-F0rU,192
2
+ statemachine/callbacks.py,sha256=D1mb3Ky18S1jk0-ursuJljqDv63WZShnMvMK_S1Hi9k,8846
3
+ statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ statemachine/contrib/diagram.py,sha256=CTlNRtAHEzCCzRUlxkiyzJOCOp1CohihdD0_7pHrx4c,7004
5
+ statemachine/dispatcher.py,sha256=Zizr_Qaj2BfuHu5VbbnNh1giT_pUVLe4mekHj6Ca2dk,5075
6
+ statemachine/event.py,sha256=lfJUSvkDQTnk1COueuV--xQD4o6GeaI94Y8tTOmiS6s,2304
7
+ statemachine/event_data.py,sha256=nxOfypngK-78A77gj4pS-oxLx9zl1S9wnSt_rTiCyZA,2257
8
+ statemachine/events.py,sha256=rUbiTX-QGoo7zzmQMEeWeLrIVnp9zhicdRIm34CoAVI,731
9
+ statemachine/exceptions.py,sha256=RWq1vTjajxt8CzfYCD4DfI2nrgkfCPBL0DjRDOzKkQI,1086
10
+ statemachine/factory.py,sha256=pAfRUuO474FekxrUZkKr8NkQqQ59vOu4dRHSD8Dr9rI,7982
11
+ statemachine/graph.py,sha256=KtwB1CYckaLjTgQD9tEeuaEzJje9q3fPVpBViW5TgSk,487
12
+ statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
13
+ statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=zJYaV2DxrHdPngDOt1bji8-lPOZMSDjWG83Ypru5DOI,2126
14
+ statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=SPrt-KaCBCKQq7PoOMtyyqU1bihrw_3MPL_NbqZzTVs,3214
15
+ statemachine/mixins.py,sha256=B8WB3EGyZpMWAQg4Nw3kUMCfswgmLCChDpOQdxR28eE,1017
16
+ statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
17
+ statemachine/registry.py,sha256=RnVBRS3I_6Tm2OMgXMB_ewX7zQaslqEfhXFOhbqIkG4,959
18
+ statemachine/signature.py,sha256=aGoKxC36mTO60GGICVlyduhhB2nKVOXOOHepcBG2Uws,7809
19
+ statemachine/state.py,sha256=bZki0DyemZa-aVAfollQCmpXxesWONzUmE28eDfqz3o,8315
20
+ statemachine/statemachine.py,sha256=5r8vtbMFQtOq9W8UmaWBN_SIzOHkO-4JfPysHfdoB8w,13827
21
+ statemachine/states.py,sha256=gdGemJYF9k-cifs9Tk0Pe_-1u1Lanf3l08o0t8wAMgg,4203
22
+ statemachine/transition.py,sha256=fo6B-XlHDqU8TNrDXBSn4GStdGUrZbcfbEgtsdESmrk,5412
23
+ statemachine/transition_list.py,sha256=DatsmMWgK0YK30Nrj-josVvlTgeGapKutzYur9-puF8,5949
24
+ statemachine/utils.py,sha256=FVtvT1lBSP3mRrYM-wxsX2pV_NZwSDsoBW4syIpACeE,798
25
+ python_statemachine-2.3.0.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
26
+ python_statemachine-2.3.0.dist-info/METADATA,sha256=ubb6vnMCYaYyz486RJW5r1dKKu_emWD7Rwc4x2S7lyY,14361
27
+ python_statemachine-2.3.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
28
+ python_statemachine-2.3.0.dist-info/RECORD,,
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.2"
6
+ __version__ = "2.3.0"
7
7
 
8
8
  __all__ = ["StateMachine", "State"]
statemachine/callbacks.py CHANGED
@@ -1,7 +1,10 @@
1
+ from bisect import insort
1
2
  from collections import defaultdict
2
3
  from collections import deque
4
+ from enum import IntEnum
3
5
  from typing import Callable
4
6
  from typing import Dict
7
+ from typing import Generator
5
8
  from typing import List
6
9
 
7
10
  from .exceptions import AttrNotFound
@@ -9,27 +12,42 @@ from .i18n import _
9
12
  from .utils import ensure_iterable
10
13
 
11
14
 
15
+ class CallbackPriority(IntEnum):
16
+ GENERIC = 0
17
+ INLINE = 10
18
+ DECORATOR = 20
19
+ NAMING = 30
20
+ AFTER = 40
21
+
22
+
23
+ async def allways_true(*args, **kwargs):
24
+ return True
25
+
26
+
12
27
  class CallbackWrapper:
13
28
  def __init__(
14
29
  self,
15
30
  callback: Callable,
16
31
  condition: Callable,
32
+ meta: "CallbackMeta",
17
33
  unique_key: str,
18
- expected_value: "bool | None" = None,
19
34
  ) -> None:
20
35
  self._callback = callback
21
36
  self.condition = condition
37
+ self.meta = meta
22
38
  self.unique_key = unique_key
23
- self.expected_value = expected_value
24
39
 
25
40
  def __repr__(self):
26
41
  return f"{type(self).__name__}({self.unique_key})"
27
42
 
28
- def __call__(self, *args, **kwargs):
29
- result = self._callback(*args, **kwargs)
30
- if self.expected_value is not None:
31
- return bool(result) == self.expected_value
32
- return result
43
+ def __str__(self):
44
+ return str(self.meta)
45
+
46
+ def __lt__(self, other):
47
+ return self.meta.priority < other.meta.priority
48
+
49
+ async def __call__(self, *args, **kwargs):
50
+ return await self._callback(*args, **kwargs)
33
51
 
34
52
 
35
53
  class CallbackMeta:
@@ -42,14 +60,22 @@ class CallbackMeta:
42
60
  call is performed, to allow the proper callback resolution.
43
61
  """
44
62
 
45
- def __init__(self, func, suppress_errors=False, cond=None, expected_value=None):
63
+ def __init__(
64
+ self,
65
+ func,
66
+ suppress_errors=False,
67
+ cond=None,
68
+ priority: CallbackPriority = CallbackPriority.NAMING,
69
+ expected_value=None,
70
+ ):
46
71
  self.func = func
47
72
  self.suppress_errors = suppress_errors
48
- self.cond = CallbackMetaList().add(cond)
73
+ self.cond = cond
49
74
  self.expected_value = expected_value
75
+ self.priority = priority
50
76
 
51
77
  def __repr__(self):
52
- return f"{type(self).__name__}({self.func!r})"
78
+ return f"{type(self).__name__}({self.func!r}, suppress_errors={self.suppress_errors!r})"
53
79
 
54
80
  def __str__(self):
55
81
  return getattr(self.func, "__name__", self.func)
@@ -63,7 +89,10 @@ class CallbackMeta:
63
89
  def _update_func(self, func):
64
90
  self.func = func
65
91
 
66
- def build(self, resolver) -> "CallbackWrapper | None":
92
+ def _wrap_callable(self, func, _expected_value):
93
+ return func
94
+
95
+ def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
67
96
  """
68
97
  Resolves the `func` into a usable callable.
69
98
 
@@ -71,25 +100,14 @@ class CallbackMeta:
71
100
  resolver (callable): A method responsible to build and return a valid callable that
72
101
  can receive arbitrary parameters like `*args, **kwargs`.
73
102
  """
74
- callback = resolver(self.func)
75
- if not callback.is_empty:
76
- conditions = CallbacksExecutor()
77
- conditions.add(self.cond, resolver)
78
-
79
- return CallbackWrapper(
80
- callback=callback,
81
- condition=conditions.all,
103
+ for callback in resolver(self.func):
104
+ condition = next(resolver(self.cond)) if self.cond is not None else allways_true
105
+ yield CallbackWrapper(
106
+ callback=self._wrap_callable(callback, self.expected_value),
107
+ condition=condition,
108
+ meta=self,
82
109
  unique_key=callback.unique_key,
83
- expected_value=self.expected_value,
84
- )
85
-
86
- if not self.suppress_errors:
87
- raise AttrNotFound(
88
- _("Did not found name '{}' from model or statemachine").format(
89
- self.func
90
- )
91
110
  )
92
- return None
93
111
 
94
112
 
95
113
  class BoolCallbackMeta(CallbackMeta):
@@ -102,18 +120,32 @@ class BoolCallbackMeta(CallbackMeta):
102
120
  call is performed, to allow the proper callback resolution.
103
121
  """
104
122
 
105
- def __init__(self, func, suppress_errors=False, cond=None, expected_value=True):
106
- self.func = func
107
- self.suppress_errors = suppress_errors
108
- self.cond = CallbackMetaList().add(cond)
109
- self.expected_value = expected_value
123
+ def __init__(
124
+ self,
125
+ func,
126
+ suppress_errors=False,
127
+ cond=None,
128
+ priority: CallbackPriority = CallbackPriority.NAMING,
129
+ expected_value=True,
130
+ ):
131
+ super().__init__(
132
+ func, suppress_errors, cond, priority=priority, expected_value=expected_value
133
+ )
110
134
 
111
135
  def __str__(self):
112
136
  name = super().__str__()
113
137
  return name if self.expected_value else f"!{name}"
114
138
 
139
+ def _wrap_callable(self, func, expected_value):
140
+ async def bool_wrapper(*args, **kwargs):
141
+ return bool(await func(*args, **kwargs)) == expected_value
142
+
143
+ return bool_wrapper
144
+
115
145
 
116
146
  class CallbackMetaList:
147
+ """List of `CallbackMeta` instances"""
148
+
117
149
  def __init__(self, factory=CallbackMeta):
118
150
  self.items: List[CallbackMeta] = []
119
151
  self.factory = factory
@@ -157,6 +189,7 @@ class CallbackMetaList:
157
189
  return func
158
190
 
159
191
  def __call__(self, callback):
192
+ """Allows usage of the callback list as a decorator."""
160
193
  return self._add_unbounded_callback(callback)
161
194
 
162
195
  def __iter__(self):
@@ -165,18 +198,13 @@ class CallbackMetaList:
165
198
  def clear(self):
166
199
  self.items = []
167
200
 
168
- def _add(self, func, registry=None, prepend=False, **kwargs):
201
+ def _add(self, func, **kwargs):
169
202
  meta = self.factory(func, **kwargs)
170
- if registry is not None and not registry(self, meta, prepend=prepend):
171
- return
172
203
 
173
204
  if meta in self.items:
174
205
  return
175
206
 
176
- if prepend:
177
- self.items.insert(0, meta)
178
- else:
179
- self.items.append(meta)
207
+ self.items.append(meta)
180
208
  return meta
181
209
 
182
210
  def add(self, callbacks, **kwargs):
@@ -191,6 +219,8 @@ class CallbackMetaList:
191
219
 
192
220
 
193
221
  class CallbacksExecutor:
222
+ """A list of callbacks that can be executed in order."""
223
+
194
224
  def __init__(self):
195
225
  self.items: List[CallbackWrapper] = deque()
196
226
  self.items_already_seen = set()
@@ -201,60 +231,60 @@ class CallbacksExecutor:
201
231
  def __repr__(self):
202
232
  return f"{type(self).__name__}({self.items!r})"
203
233
 
204
- def add_one(
205
- self, callback_info: CallbackMeta, resolver: Callable, prepend: bool = False
206
- ) -> "CallbackWrapper | None":
207
- callback = callback_info.build(resolver)
208
- if callback is None:
209
- return None
234
+ def __str__(self):
235
+ return ", ".join(str(c) for c in self)
210
236
 
211
- if callback.unique_key in self.items_already_seen:
212
- return None
237
+ def _add(self, callback_meta: CallbackMeta, resolver: Callable):
238
+ for callback in callback_meta.build(resolver):
239
+ if callback.unique_key in self.items_already_seen:
240
+ continue
213
241
 
214
- self.items_already_seen.add(callback.unique_key)
215
- if prepend:
216
- self.items.insert(0, callback)
217
- else:
218
- self.items.append(callback)
219
- return callback
242
+ self.items_already_seen.add(callback.unique_key)
243
+ insort(self.items, callback)
220
244
 
221
245
  def add(self, items: CallbackMetaList, resolver: Callable):
222
246
  """Validate configurations"""
223
247
  for item in items:
224
- self.add_one(item, resolver)
248
+ self._add(item, resolver)
225
249
  return self
226
250
 
227
- def call(self, *args, **kwargs):
251
+ async def call(self, *args, **kwargs):
228
252
  return [
229
- callback(*args, **kwargs)
253
+ await callback(*args, **kwargs)
230
254
  for callback in self
231
- if callback.condition(*args, **kwargs)
255
+ if await callback.condition(*args, **kwargs)
232
256
  ]
233
257
 
234
- def all(self, *args, **kwargs):
235
- return all(condition(*args, **kwargs) for condition in self)
258
+ async def all(self, *args, **kwargs):
259
+ for condition in self:
260
+ if not await condition(*args, **kwargs):
261
+ return False
262
+ return True
236
263
 
237
264
 
238
265
  class CallbacksRegistry:
239
266
  def __init__(self) -> None:
240
- self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(
241
- CallbacksExecutor
242
- )
267
+ self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(CallbacksExecutor)
243
268
 
244
- def register(self, callbacks: CallbackMetaList, resolver):
245
- executor_list = self[callbacks]
246
- executor_list.add(callbacks, resolver)
269
+ def register(self, meta_list: CallbackMetaList, resolver):
270
+ executor_list = self[meta_list]
271
+ executor_list.add(meta_list, resolver)
247
272
  return executor_list
248
273
 
249
- def __getitem__(self, callbacks: CallbackMetaList) -> CallbacksExecutor:
250
- return self._registry[callbacks]
274
+ def clear(self):
275
+ self._registry.clear()
276
+
277
+ def __getitem__(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
278
+ return self._registry[meta_list]
251
279
 
252
- def build_register_function_for_resolver(self, resolver):
253
- def register(
254
- meta_list: CallbackMetaList,
255
- meta: CallbackMeta,
256
- prepend: bool = False,
257
- ):
258
- return self[meta_list].add_one(meta, resolver, prepend=prepend)
280
+ def check(self, meta_list: CallbackMetaList):
281
+ executor = self[meta_list]
282
+ for meta in meta_list:
283
+ if meta.suppress_errors:
284
+ continue
259
285
 
260
- return register
286
+ if any(callback for callback in executor if callback.meta == meta):
287
+ continue
288
+ raise AttrNotFound(
289
+ _("Did not found name '{}' from model or statemachine").format(meta.func)
290
+ )
@@ -66,21 +66,35 @@ class DotGraphMachine:
66
66
  fontsize=self.transition_font_size,
67
67
  )
68
68
 
69
+ def _actions_getter(self):
70
+ if isinstance(self.machine, StateMachine):
71
+
72
+ def getter(x):
73
+ return self.machine._callbacks(x)
74
+ else:
75
+
76
+ def getter(x):
77
+ return x
78
+
79
+ return getter
80
+
69
81
  def _state_actions(self, state):
70
- entry = ", ".join([str(action) for action in state.enter])
71
- exit = ", ".join([str(action) for action in state.exit])
82
+ getter = self._actions_getter()
83
+
84
+ entry = str(getter(state.enter))
85
+ exit_ = str(getter(state.exit))
72
86
  internal = ", ".join(
73
- f"{transition.event} / {transition.on!s}"
87
+ f"{transition.event} / {str(getter(transition.on))}"
74
88
  for transition in state.transitions
75
89
  if transition.internal
76
90
  )
77
91
 
78
92
  if entry:
79
93
  entry = f"entry / {entry}"
80
- if exit:
81
- exit = f"exit / {exit}"
94
+ if exit_:
95
+ exit_ = f"exit / {exit_}"
82
96
 
83
- actions = "\n".join(x for x in [entry, exit, internal] if x)
97
+ actions = "\n".join(x for x in [entry, exit_, internal] if x)
84
98
 
85
99
  if actions:
86
100
  actions = f"\n{actions}"