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