python-statemachine 2.3.1__py3-none-any.whl → 2.3.3__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.3.1
3
+ Version: 2.3.3
4
4
  Summary: Python Finite State Machines made easy.
5
5
  Home-page: https://github.com/fgmacedo/python-statemachine
6
6
  License: MIT
@@ -103,7 +103,7 @@ Define your state machine:
103
103
  ... | red.to(green)
104
104
  ... )
105
105
  ...
106
- ... async def before_cycle(self, event: str, source: State, target: State, message: str = ""):
106
+ ... def before_cycle(self, event: str, source: State, target: State, message: str = ""):
107
107
  ... message = ". " + message if message else ""
108
108
  ... return f"Running {event} from {source.id} to {target.id}{message}"
109
109
  ...
@@ -146,26 +146,6 @@ Then start sending events to your new state machine:
146
146
 
147
147
  ```
148
148
 
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
149
  **That's it.** This is all an external object needs to know about your state machine: How to send events.
170
150
  Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.
171
151
 
@@ -253,7 +233,7 @@ callback method.
253
233
  Note how `before_cycle` was declared:
254
234
 
255
235
  ```py
256
- async def before_cycle(self, event: str, source: State, target: State, message: str = ""):
236
+ def before_cycle(self, event: str, source: State, target: State, message: str = ""):
257
237
  message = ". " + message if message else ""
258
238
  return f"Running {event} from {source.id} to {target.id}{message}"
259
239
  ```
@@ -0,0 +1,31 @@
1
+ statemachine/__init__.py,sha256=ZOsh7Du9N_DZCAZKm6FHONs0DODQb0w5P-9Vi5ofgTQ,192
2
+ statemachine/callbacks.py,sha256=m9AS3G4qumJBjFwQNJZi3JbWbEOxjcW50H59VC7bLLY,10946
3
+ statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ statemachine/contrib/diagram.py,sha256=JJx52yZqgDNIebvcUJ82IGT4EyerXQmOcF5mR21_kao,7198
5
+ statemachine/dispatcher.py,sha256=oYlld3gvQDn2iXRPjk9ujtd5JnqWHxcft5oV80i-jMY,5175
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/registry.py,sha256=HmV9sUGkYVrNUZxJYoZo-trSUis7dIun_WcGktblgc8,922
21
+ statemachine/signature.py,sha256=HwuyWX3p_GZwKKrGbt9wk4-bHpymqs_pgqW7OlDgGRc,7877
22
+ statemachine/state.py,sha256=IKdkMBkXCgcvswMKQZ-hn0-739Hhf6z6q8mSEoevV7o,8262
23
+ statemachine/statemachine.py,sha256=zJeGHQxKItuVsmaLdMwsvRoJ29oSI2Ff3B9pYwVi8H4,11274
24
+ statemachine/states.py,sha256=gkwF9y1x0-t6ULQL0JrJcR1uflHGQNRZoyaMTXrPuKw,4886
25
+ statemachine/transition.py,sha256=R7gfSZjqR59OIdpKePgdr66zTp5azow-tDqqYJsECR4,4711
26
+ statemachine/transition_list.py,sha256=_tdFD1O7hw8xVAU6nRYmGrj6TCAM0SD-Sa0_DYKa88Y,6034
27
+ statemachine/utils.py,sha256=DpcrGqlbrnT-ogh-BogG0L07EG3KirHOsKORHlspDlI,1041
28
+ python_statemachine-2.3.3.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
29
+ python_statemachine-2.3.3.dist-info/METADATA,sha256=f9c8HTfQptQe4ocTS85DdYpG4ZrXZWi6cDVIIvxoIvo,13932
30
+ python_statemachine-2.3.3.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
31
+ python_statemachine-2.3.3.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.3.1"
6
+ __version__ = "2.3.3"
7
7
 
8
8
  __all__ = ["StateMachine", "State"]
statemachine/callbacks.py CHANGED
@@ -1,11 +1,17 @@
1
+ import asyncio
1
2
  from bisect import insort
2
3
  from collections import defaultdict
3
4
  from collections import deque
4
5
  from enum import IntEnum
6
+ from enum import auto
7
+ from inspect import isawaitable
8
+ from inspect import iscoroutinefunction
5
9
  from typing import Callable
6
10
  from typing import Dict
7
11
  from typing import Generator
12
+ from typing import Iterable
8
13
  from typing import List
14
+ from typing import Type
9
15
 
10
16
  from .exceptions import AttrNotFound
11
17
  from .i18n import _
@@ -20,77 +26,84 @@ class CallbackPriority(IntEnum):
20
26
  AFTER = 40
21
27
 
22
28
 
23
- async def allways_true(*args, **kwargs):
24
- return True
29
+ class SpecReference(IntEnum):
30
+ NAME = 1
31
+ CALLABLE = 2
32
+ PROPERTY = 3
25
33
 
26
34
 
27
- class CallbackWrapper:
28
- def __init__(
29
- self,
30
- callback: Callable,
31
- condition: Callable,
32
- meta: "CallbackMeta",
33
- unique_key: str,
34
- ) -> None:
35
- self._callback = callback
36
- self.condition = condition
37
- self.meta = meta
38
- self.unique_key = unique_key
35
+ class CallbackGroup(IntEnum):
36
+ ENTER = auto()
37
+ EXIT = auto()
38
+ VALIDATOR = auto()
39
+ BEFORE = auto()
40
+ ON = auto()
41
+ AFTER = auto()
42
+ COND = auto()
39
43
 
40
- def __repr__(self):
41
- return f"{type(self).__name__}({self.unique_key})"
42
-
43
- def __str__(self):
44
- return str(self.meta)
44
+ def build_key(self, specs: "CallbackSpecList") -> str:
45
+ return f"{self.name}@{id(specs)}"
45
46
 
46
- def __lt__(self, other):
47
- return self.meta.priority < other.meta.priority
48
47
 
49
- async def __call__(self, *args, **kwargs):
50
- return await self._callback(*args, **kwargs)
48
+ def allways_true(*args, **kwargs):
49
+ return True
51
50
 
52
51
 
53
- class CallbackMeta:
54
- """A thin wrapper that register info about actions and guards.
52
+ class CallbackSpec:
53
+ """Specs about callbacks.
55
54
 
56
- At first, `func` can be a string or a callable, and even if it's already
57
- a callable, his signature can mismatch.
55
+ At first, `func` can be a name (string), a property or a callable.
58
56
 
59
- After instantiation, `.setup(resolver)` must be called before any real
60
- call is performed, to allow the proper callback resolution.
57
+ Names, properties and unbounded callables should be resolved to a callable
58
+ before any real call is performed.
61
59
  """
62
60
 
63
61
  def __init__(
64
62
  self,
65
63
  func,
66
- suppress_errors=False,
64
+ group: CallbackGroup,
65
+ is_convention=False,
67
66
  cond=None,
68
67
  priority: CallbackPriority = CallbackPriority.NAMING,
69
68
  expected_value=None,
70
69
  ):
71
70
  self.func = func
72
- self.suppress_errors = suppress_errors
71
+ self.group = group
72
+ self.is_convention = is_convention
73
73
  self.cond = cond
74
74
  self.expected_value = expected_value
75
75
  self.priority = priority
76
76
 
77
+ if isinstance(func, property):
78
+ self.reference = SpecReference.PROPERTY
79
+ self.attr_name: str = func and func.fget and func.fget.__name__ or ""
80
+ elif callable(func):
81
+ self.reference = SpecReference.CALLABLE
82
+ self.is_bounded = hasattr(func, "__self__")
83
+ self.attr_name = func.__name__
84
+ else:
85
+ self.reference = SpecReference.NAME
86
+ self.attr_name = func
87
+
77
88
  def __repr__(self):
78
- return f"{type(self).__name__}({self.func!r}, suppress_errors={self.suppress_errors!r})"
89
+ return f"{type(self).__name__}({self.func!r}, is_convention={self.is_convention!r})"
79
90
 
80
91
  def __str__(self):
81
- return getattr(self.func, "__name__", self.func)
92
+ name = getattr(self.func, "__name__", self.func)
93
+ if self.expected_value is False:
94
+ name = f"!{name}"
95
+ return name
82
96
 
83
97
  def __eq__(self, other):
84
- return self.func == other.func
98
+ return self.func == other.func and self.group == other.group
85
99
 
86
100
  def __hash__(self):
87
101
  return id(self)
88
102
 
89
- def _update_func(self, func):
103
+ def _update_func(self, func: Callable, attr_name: str):
90
104
  self.func = func
91
-
92
- def _wrap_callable(self, func, _expected_value):
93
- return func
105
+ self.reference = SpecReference.CALLABLE
106
+ self.attr_name = attr_name
94
107
 
95
108
  def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
96
109
  """
@@ -100,62 +113,57 @@ class CallbackMeta:
100
113
  resolver (callable): A method responsible to build and return a valid callable that
101
114
  can receive arbitrary parameters like `*args, **kwargs`.
102
115
  """
103
- for callback in resolver(self.func):
104
- condition = next(resolver(self.cond)) if self.cond is not None else allways_true
116
+ for callback in resolver.search(self):
117
+ condition = self.cond if self.cond is not None else allways_true
105
118
  yield CallbackWrapper(
106
- callback=self._wrap_callable(callback, self.expected_value),
119
+ callback=callback,
107
120
  condition=condition,
108
121
  meta=self,
109
122
  unique_key=callback.unique_key,
110
123
  )
111
124
 
112
125
 
113
- class BoolCallbackMeta(CallbackMeta):
114
- """A thin wrapper that register info about actions and guards.
126
+ class SpecListGrouper:
127
+ def __init__(
128
+ self, list: "CallbackSpecList", group: CallbackGroup, factory=CallbackSpec
129
+ ) -> None:
130
+ self.list = list
131
+ self.group = group
132
+ self.factory = factory
133
+ self.key = group.build_key(list)
115
134
 
116
- At first, `func` can be a string or a callable, and even if it's already
117
- a callable, his signature can mismatch.
135
+ def add(self, callbacks, **kwargs):
136
+ self.list.add(callbacks, group=self.group, factory=self.factory, **kwargs)
137
+ return self
118
138
 
119
- After instantiation, `.setup(resolver)` must be called before any real
120
- call is performed, to allow the proper callback resolution.
121
- """
139
+ def __call__(self, callback):
140
+ return self.list._add_unbounded_callback(callback, group=self.group, factory=self.factory)
122
141
 
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
142
+ def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
143
+ self.list._add_unbounded_callback(
144
+ func,
145
+ is_event=is_event,
146
+ transitions=transitions,
147
+ group=self.group,
148
+ factory=self.factory,
149
+ **kwargs,
133
150
  )
134
151
 
135
- def __str__(self):
136
- name = super().__str__()
137
- return name if self.expected_value else f"!{name}"
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
152
+ def __iter__(self):
153
+ return (item for item in self.list if item.group == self.group)
144
154
 
145
155
 
146
- class CallbackMetaList:
147
- """List of `CallbackMeta` instances"""
156
+ class CallbackSpecList:
157
+ """List of {ref}`CallbackSpec` instances"""
148
158
 
149
- def __init__(self, factory=CallbackMeta):
150
- self.items: List[CallbackMeta] = []
159
+ def __init__(self, factory=CallbackSpec):
160
+ self.items: List[CallbackSpec] = []
161
+ self.conventional_specs = set()
151
162
  self.factory = factory
152
163
 
153
164
  def __repr__(self):
154
165
  return f"{type(self).__name__}({self.items!r}, factory={self.factory!r})"
155
166
 
156
- def __str__(self):
157
- return ", ".join(str(c) for c in self)
158
-
159
167
  def _add_unbounded_callback(self, func, is_event=False, transitions=None, **kwargs):
160
168
  """This list was a target for adding a func using decorator
161
169
  `@<state|event>[.on|before|after|enter|exit]` syntax.
@@ -179,45 +187,90 @@ class CallbackMetaList:
179
187
  event.
180
188
 
181
189
  """
182
- callback = self._add(func, **kwargs)
183
- if not getattr(func, "_callbacks_to_update", None):
184
- func._callbacks_to_update = set()
185
- func._callbacks_to_update.add(callback._update_func)
186
- func._is_event = is_event
190
+ spec = self._add(func, **kwargs)
191
+ if not getattr(func, "_specs_to_update", None):
192
+ func._specs_to_update = set()
193
+ if is_event:
194
+ func._specs_to_update.add(spec._update_func)
187
195
  func._transitions = transitions
188
196
 
189
197
  return func
190
198
 
191
- def __call__(self, callback):
192
- """Allows usage of the callback list as a decorator."""
193
- return self._add_unbounded_callback(callback)
194
-
195
199
  def __iter__(self):
196
200
  return iter(self.items)
197
201
 
198
202
  def clear(self):
199
203
  self.items = []
200
204
 
201
- def _add(self, func, **kwargs):
202
- meta = self.factory(func, **kwargs)
205
+ def grouper(
206
+ self, group: CallbackGroup, factory: Type[CallbackSpec] = CallbackSpec
207
+ ) -> SpecListGrouper:
208
+ return SpecListGrouper(self, group, factory=factory)
209
+
210
+ def _add(self, func, group: CallbackGroup, factory=None, **kwargs):
211
+ if factory is None:
212
+ factory = self.factory
213
+ spec = factory(func, group, **kwargs)
203
214
 
204
- if meta in self.items:
215
+ if spec in self.items:
205
216
  return
206
217
 
207
- self.items.append(meta)
208
- return meta
218
+ self.items.append(spec)
219
+ if spec.is_convention:
220
+ self.conventional_specs.add(spec.func)
221
+ return spec
209
222
 
210
- def add(self, callbacks, **kwargs):
223
+ def add(self, callbacks, group: CallbackGroup, **kwargs):
211
224
  if callbacks is None:
212
225
  return self
213
226
 
214
227
  unprepared = ensure_iterable(callbacks)
215
228
  for func in unprepared:
216
- self._add(func, **kwargs)
229
+ self._add(func, group=group, **kwargs)
217
230
 
218
231
  return self
219
232
 
220
233
 
234
+ class CallbackWrapper:
235
+ def __init__(
236
+ self,
237
+ callback: Callable,
238
+ condition: Callable,
239
+ meta: "CallbackSpec",
240
+ unique_key: str,
241
+ ) -> None:
242
+ self._callback = callback
243
+ self._iscoro = iscoroutinefunction(callback)
244
+ self.condition = condition
245
+ self.meta = meta
246
+ self.unique_key = unique_key
247
+ self.expected_value = self.meta.expected_value
248
+
249
+ def __repr__(self):
250
+ return f"{type(self).__name__}({self.unique_key})"
251
+
252
+ def __str__(self):
253
+ return str(self.meta)
254
+
255
+ def __lt__(self, other):
256
+ return self.meta.priority < other.meta.priority
257
+
258
+ async def __call__(self, *args, **kwargs):
259
+ value = self._callback(*args, **kwargs)
260
+ if isawaitable(value):
261
+ value = await value
262
+
263
+ if self.expected_value is not None:
264
+ return bool(value) == self.expected_value
265
+ return value
266
+
267
+ def call(self, *args, **kwargs):
268
+ value = self._callback(*args, **kwargs)
269
+ if self.expected_value is not None:
270
+ return bool(value) == self.expected_value
271
+ return value
272
+
273
+
221
274
  class CallbacksExecutor:
222
275
  """A list of callbacks that can be executed in order."""
223
276
 
@@ -234,57 +287,75 @@ class CallbacksExecutor:
234
287
  def __str__(self):
235
288
  return ", ".join(str(c) for c in self)
236
289
 
237
- def _add(self, callback_meta: CallbackMeta, resolver: Callable):
238
- for callback in callback_meta.build(resolver):
290
+ def _add(self, spec: CallbackSpec, resolver: Callable):
291
+ for callback in spec.build(resolver):
239
292
  if callback.unique_key in self.items_already_seen:
240
293
  continue
241
294
 
242
295
  self.items_already_seen.add(callback.unique_key)
243
296
  insort(self.items, callback)
244
297
 
245
- def add(self, items: CallbackMetaList, resolver: Callable):
298
+ def add(self, items: Iterable[CallbackSpec], resolver: Callable):
246
299
  """Validate configurations"""
247
300
  for item in items:
248
301
  self._add(item, resolver)
249
302
  return self
250
303
 
251
- async def call(self, *args, **kwargs):
304
+ async def async_call(self, *args, **kwargs):
305
+ return await asyncio.gather(
306
+ *(
307
+ callback(*args, **kwargs)
308
+ for callback in self
309
+ if callback.condition(*args, **kwargs)
310
+ )
311
+ )
312
+
313
+ async def async_all(self, *args, **kwargs):
314
+ coros = [condition(*args, **kwargs) for condition in self]
315
+ for coro in asyncio.as_completed(coros):
316
+ if not await coro:
317
+ return False
318
+ return True
319
+
320
+ def call(self, *args, **kwargs):
252
321
  return [
253
- await callback(*args, **kwargs)
322
+ callback.call(*args, **kwargs)
254
323
  for callback in self
255
- if await callback.condition(*args, **kwargs)
324
+ if callback.condition(*args, **kwargs)
256
325
  ]
257
326
 
258
- async def all(self, *args, **kwargs):
327
+ def all(self, *args, **kwargs):
259
328
  for condition in self:
260
- if not await condition(*args, **kwargs):
329
+ if not condition.call(*args, **kwargs):
261
330
  return False
262
331
  return True
263
332
 
264
333
 
265
334
  class CallbacksRegistry:
266
335
  def __init__(self) -> None:
267
- self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(CallbacksExecutor)
268
-
269
- def register(self, meta_list: CallbackMetaList, resolver):
270
- executor_list = self[meta_list]
271
- executor_list.add(meta_list, resolver)
272
- return executor_list
336
+ self._registry: Dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
337
+ self.has_async_callbacks: bool = False
273
338
 
274
339
  def clear(self):
275
340
  self._registry.clear()
276
341
 
277
- def __getitem__(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
278
- return self._registry[meta_list]
342
+ def __getitem__(self, key: str) -> CallbacksExecutor:
343
+ return self._registry[key]
279
344
 
280
- def check(self, meta_list: CallbackMetaList):
281
- executor = self[meta_list]
282
- for meta in meta_list:
283
- if meta.suppress_errors:
345
+ def check(self, specs: CallbackSpecList):
346
+ for meta in specs:
347
+ if meta.is_convention:
284
348
  continue
285
349
 
286
- if any(callback for callback in executor if callback.meta == meta):
350
+ if any(
351
+ callback for callback in self[meta.group.build_key(specs)] if callback.meta == meta
352
+ ):
287
353
  continue
288
354
  raise AttrNotFound(
289
355
  _("Did not found name '{}' from model or statemachine").format(meta.func)
290
356
  )
357
+
358
+ def async_or_sync(self):
359
+ self.has_async_callbacks = any(
360
+ callback._iscoro for executor in self._registry.values() for callback in executor
361
+ )
@@ -69,12 +69,15 @@ class DotGraphMachine:
69
69
  def _actions_getter(self):
70
70
  if isinstance(self.machine, StateMachine):
71
71
 
72
- def getter(x):
73
- return self.machine._callbacks(x)
72
+ def getter(grouper):
73
+ return self.machine._get_callbacks(grouper.key)
74
74
  else:
75
75
 
76
- def getter(x):
77
- return x
76
+ def getter(grouper):
77
+ all_names = set(dir(self.machine))
78
+ return ", ".join(
79
+ str(c) for c in grouper if not c.is_convention or c.func in all_names
80
+ )
78
81
 
79
82
  return getter
80
83