python-statemachine 2.1.2__py3-none-any.whl → 2.2.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.2.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,17 +8,17 @@ 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.9,<3.13
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Natural Language :: English
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
16
  Classifier: Programming Language :: Python :: 3.9
19
17
  Classifier: Programming Language :: Python :: 3.10
20
18
  Classifier: Programming Language :: Python :: 3.11
21
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.7
21
+ Classifier: Programming Language :: Python :: 3.8
22
22
  Classifier: Topic :: Software Development :: Libraries
23
23
  Provides-Extra: diagrams
24
24
  Description-Content-Type: text/markdown
@@ -0,0 +1,28 @@
1
+ statemachine/__init__.py,sha256=nhqSQwRfWBVrcn4h-aLlxLbzeSsjt6lE_SSFnFWK0FY,192
2
+ statemachine/callbacks.py,sha256=QMh564YuNVNV74VxlIyvlF_eW6njsmV9VHauQH9JR1U,8704
3
+ statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ statemachine/contrib/diagram.py,sha256=CTlNRtAHEzCCzRUlxkiyzJOCOp1CohihdD0_7pHrx4c,7004
5
+ statemachine/dispatcher.py,sha256=i9ChhdomQpVbTg9vNS3xoCHUAw3Z32B0UPzK47nb1-A,4924
6
+ statemachine/event.py,sha256=fvWxyFcrbPVxNW94p66rGJ5I1UZUK9mDJz68udkz2JI,2106
7
+ statemachine/event_data.py,sha256=nxOfypngK-78A77gj4pS-oxLx9zl1S9wnSt_rTiCyZA,2257
8
+ statemachine/events.py,sha256=rUbiTX-QGoo7zzmQMEeWeLrIVnp9zhicdRIm34CoAVI,731
9
+ statemachine/exceptions.py,sha256=uzvDbHkzCZkmFI_02L0yC9gd6d0SBn9GmsI8ekc0Hjk,952
10
+ statemachine/factory.py,sha256=m2TeJ19eDIymoGADDp0G0ItrrcCRgIJoJYFJ5lzqmWE,8015
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=MfODCwzML6DzhOpHbno2Pdhsb26AWboR5Ka1JCNvJIA,1685
14
+ statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=KsR9rlnhw17EZG-IbVx3F0wJAJSK0B-OByGmUmDHDUM,2375
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=9KItptchQlP2VpNmBiIFXLNAWNWY3Bs_ymUd7OOgDD4,7507
19
+ statemachine/state.py,sha256=bZki0DyemZa-aVAfollQCmpXxesWONzUmE28eDfqz3o,8315
20
+ statemachine/statemachine.py,sha256=U0rLIUhvsCivth1uNb0pt2xWVUnDbzhXF982yJH-bg0,11659
21
+ statemachine/states.py,sha256=gdGemJYF9k-cifs9Tk0Pe_-1u1Lanf3l08o0t8wAMgg,4203
22
+ statemachine/transition.py,sha256=ebRhJsjOoLya7oYZVjRAKvjq1EsC9ImpExgHANekVMA,5388
23
+ statemachine/transition_list.py,sha256=DatsmMWgK0YK30Nrj-josVvlTgeGapKutzYur9-puF8,5949
24
+ statemachine/utils.py,sha256=fV-Hz1gnyl4tAs9nPr2u9I9zfxq62mokPV8mUpIJc1k,458
25
+ python_statemachine-2.2.0.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
26
+ python_statemachine-2.2.0.dist-info/METADATA,sha256=qH4SxonFNJStkKVocZABu-RLiYD54DcON2gNGjBPGGQ,11454
27
+ python_statemachine-2.2.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
28
+ python_statemachine-2.2.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.2.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
+ 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
 
43
+ def __str__(self):
44
+ return str(self.meta)
45
+
46
+ def __lt__(self, other):
47
+ return self.meta.priority < other.meta.priority
48
+
28
49
  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
50
+ return 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
+ def bool_wrapper(*args, **kwargs):
141
+ return bool(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,34 +231,26 @@ 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
251
  def call(self, *args, **kwargs):
228
252
  return [
229
- callback(*args, **kwargs)
230
- for callback in self
231
- if callback.condition(*args, **kwargs)
253
+ callback(*args, **kwargs) for callback in self if callback.condition(*args, **kwargs)
232
254
  ]
233
255
 
234
256
  def all(self, *args, **kwargs):
@@ -237,24 +259,27 @@ class CallbacksExecutor:
237
259
 
238
260
  class CallbacksRegistry:
239
261
  def __init__(self) -> None:
240
- self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(
241
- CallbacksExecutor
242
- )
262
+ self._registry: Dict[CallbackMetaList, CallbacksExecutor] = defaultdict(CallbacksExecutor)
243
263
 
244
- def register(self, callbacks: CallbackMetaList, resolver):
245
- executor_list = self[callbacks]
246
- executor_list.add(callbacks, resolver)
264
+ def register(self, meta_list: CallbackMetaList, resolver):
265
+ executor_list = self[meta_list]
266
+ executor_list.add(meta_list, resolver)
247
267
  return executor_list
248
268
 
249
- def __getitem__(self, callbacks: CallbackMetaList) -> CallbacksExecutor:
250
- return self._registry[callbacks]
269
+ def clear(self):
270
+ self._registry.clear()
271
+
272
+ def __getitem__(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
273
+ return self._registry[meta_list]
251
274
 
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)
275
+ def check(self, meta_list: CallbackMetaList):
276
+ executor = self[meta_list]
277
+ for meta in meta_list:
278
+ if meta.suppress_errors:
279
+ continue
259
280
 
260
- return register
281
+ if any(callback for callback in executor if callback.meta == meta):
282
+ continue
283
+ raise AttrNotFound(
284
+ _("Did not found name '{}' from model or statemachine").format(meta.func)
285
+ )
@@ -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}"
@@ -1,6 +1,7 @@
1
1
  from collections import namedtuple
2
2
  from operator import attrgetter
3
3
  from typing import Any
4
+ from typing import Generator
4
5
 
5
6
  from .signature import SignatureAdapter
6
7
 
@@ -18,17 +19,10 @@ class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs resolver_id")):
18
19
  if isinstance(obj, ObjectConfig):
19
20
  return obj
20
21
  else:
21
- return cls(obj, set(skip_attrs) if skip_attrs else set(), str(id(obj)))
22
-
23
- def getattr(self, attr):
24
- if attr in self.skip_attrs:
25
- return
26
- return getattr(self.obj, attr, None)
22
+ return cls(obj, skip_attrs or set(), str(id(obj)))
27
23
 
28
24
 
29
25
  class WrapSearchResult:
30
- is_empty = False
31
-
32
26
  def __init__(self, attribute, resolver_id) -> None:
33
27
  self.attribute = attribute
34
28
  self.resolver_id = resolver_id
@@ -48,10 +42,6 @@ class WrapSearchResult:
48
42
  return self._cache(*args, **kwds)
49
43
 
50
44
 
51
- class EmptyWrapSearchResult(WrapSearchResult):
52
- is_empty = True
53
-
54
-
55
45
  class CallableSearchResult(WrapSearchResult):
56
46
  def __init__(self, attribute, a_callable, resolver_id) -> None:
57
47
  self.a_callable = a_callable
@@ -93,32 +83,74 @@ class EventSearchResult(WrapSearchResult):
93
83
  return wrapper
94
84
 
95
85
 
96
- def search_callable(attr, *configs) -> WrapSearchResult:
97
- if callable(attr) or isinstance(attr, property):
98
- return CallableSearchResult(attr, attr, None)
86
+ def _search_callable_attr_is_property(
87
+ attr, configs: tuple[ObjectConfig]
88
+ ) -> "WrapSearchResult | None":
89
+ # if the attr is a property, we'll try to find the object that has the
90
+ # property on the configs
91
+ attr_name = attr.fget.__name__
92
+ for obj, _skip_attrs, resolver_id in configs:
93
+ func = getattr(type(obj), attr_name, None)
94
+ if func is not None and func is attr:
95
+ return AttributeCallableSearchResult(attr_name, obj, resolver_id)
96
+ return None
99
97
 
100
- for config in configs:
101
- func = config.getattr(attr)
102
- if func is not None:
103
- if not callable(func):
104
- return AttributeCallableSearchResult(
105
- attr, config.obj, config.resolver_id
106
- )
107
98
 
108
- if getattr(func, "_is_sm_event", False):
109
- return EventSearchResult(attr, func, config.resolver_id)
99
+ def _search_callable_attr_is_callable(attr, configs: tuple[ObjectConfig]) -> WrapSearchResult:
100
+ # if the attr is an unbounded method, we'll try to find the bounded method
101
+ # on the configs
102
+ if not hasattr(attr, "__self__"):
103
+ for obj, _skip_attrs, resolver_id in configs:
104
+ func = getattr(obj, attr.__name__, None)
105
+ if func is not None and func.__func__ is attr:
106
+ return CallableSearchResult(attr.__name__, func, resolver_id)
110
107
 
111
- return CallableSearchResult(attr, func, config.resolver_id)
108
+ return CallableSearchResult(attr, attr, None)
112
109
 
113
- return EmptyWrapSearchResult(attr, None)
114
110
 
111
+ def _search_callable_in_configs(
112
+ attr, configs: tuple[ObjectConfig]
113
+ ) -> Generator[WrapSearchResult, None, None]:
114
+ for obj, skip_attrs, resolver_id in configs:
115
+ if attr in skip_attrs:
116
+ continue
115
117
 
116
- def resolver_factory(*objects):
117
- """Factory that returns a configured resolver."""
118
+ if not hasattr(obj, attr):
119
+ continue
120
+
121
+ func = getattr(obj, attr)
122
+ if not callable(func):
123
+ yield AttributeCallableSearchResult(attr, obj, resolver_id)
124
+
125
+ if getattr(func, "_is_sm_event", False):
126
+ yield EventSearchResult(attr, func, resolver_id)
127
+
128
+ yield CallableSearchResult(attr, func, resolver_id)
118
129
 
119
- objects = [ObjectConfig.from_obj(obj) for obj in objects]
120
130
 
121
- def wrapper(attr):
122
- return search_callable(attr, *objects)
131
+ def search_callable(attr, configs: tuple) -> Generator[WrapSearchResult, None, None]: # noqa: C901
132
+ if isinstance(attr, property):
133
+ result = _search_callable_attr_is_property(attr, configs)
134
+ if result is not None:
135
+ yield result
136
+ return
137
+
138
+ if callable(attr):
139
+ yield _search_callable_attr_is_callable(attr, configs)
140
+ return
141
+
142
+ yield from _search_callable_in_configs(attr, configs)
143
+
144
+
145
+ def resolver_factory(objects: tuple[ObjectConfig]):
146
+ """Factory that returns a configured resolver."""
147
+
148
+ def wrapper(attr) -> Generator[WrapSearchResult, None, None]:
149
+ yield from search_callable(attr, objects)
123
150
 
124
151
  return wrapper
152
+
153
+
154
+ def resolver_factory_from_objects(*objects: tuple[Any]):
155
+ configs = tuple(ObjectConfig.from_obj(o) for o in objects)
156
+ return resolver_factory(configs)
statemachine/factory.py CHANGED
@@ -1,3 +1,4 @@
1
+ import warnings
1
2
  from typing import TYPE_CHECKING
2
3
  from typing import Any
3
4
  from typing import Dict
@@ -9,6 +10,7 @@ from . import registry
9
10
  from .event import Event
10
11
  from .event import trigger_event_factory
11
12
  from .exceptions import InvalidDefinition
13
+ from .graph import iterate_states_and_transitions
12
14
  from .graph import visit_connected_states
13
15
  from .i18n import _
14
16
  from .state import State
@@ -18,7 +20,15 @@ from .transition_list import TransitionList
18
20
 
19
21
 
20
22
  class StateMachineMetaclass(type):
21
- def __init__(cls, name: str, bases: Tuple[type], attrs: Dict[str, Any]):
23
+ "Metaclass for constructing StateMachine classes"
24
+
25
+ def __init__(
26
+ cls,
27
+ name: str,
28
+ bases: Tuple[type],
29
+ attrs: Dict[str, Any],
30
+ strict_states: bool = False,
31
+ ) -> None:
22
32
  super().__init__(name, bases, attrs)
23
33
  registry.register(cls)
24
34
  cls.name = cls.__name__
@@ -27,7 +37,9 @@ class StateMachineMetaclass(type):
27
37
  """Map of ``state.value`` to the corresponding :ref:`state`."""
28
38
 
29
39
  cls._abstract = True
40
+ cls._strict_states = strict_states
30
41
  cls._events: Dict[str, Event] = {}
42
+ cls._protected_attrs: set = set()
31
43
 
32
44
  cls.add_inherited(bases)
33
45
  cls.add_from_attributes(attrs)
@@ -40,12 +52,12 @@ class StateMachineMetaclass(type):
40
52
  cls.final_states: List[State] = [state for state in cls.states if state.final]
41
53
 
42
54
  cls._check()
55
+ cls._setup()
43
56
 
44
57
  if TYPE_CHECKING:
45
58
  """Makes mypy happy with dynamic created attributes"""
46
59
 
47
- def __getattr__(self, attribute: str) -> Any:
48
- ...
60
+ def __getattr__(self, attribute: str) -> Any: ...
49
61
 
50
62
  def _check(cls):
51
63
  has_states = bool(cls.states)
@@ -66,6 +78,8 @@ class StateMachineMetaclass(type):
66
78
  cls._check_initial_state()
67
79
  cls._check_final_states()
68
80
  cls._check_disconnected_state()
81
+ cls._check_trap_states()
82
+ cls._check_reachable_final_states()
69
83
 
70
84
  def _check_initial_state(cls):
71
85
  initials = [s for s in cls.states if s.initial]
@@ -73,7 +87,7 @@ class StateMachineMetaclass(type):
73
87
  raise InvalidDefinition(
74
88
  _(
75
89
  "There should be one and only one initial state. "
76
- "Your currently have these: {!r}"
90
+ "You currently have these: {!r}"
77
91
  ).format([s.id for s in initials])
78
92
  )
79
93
 
@@ -84,11 +98,44 @@ class StateMachineMetaclass(type):
84
98
 
85
99
  if final_state_with_invalid_transitions:
86
100
  raise InvalidDefinition(
87
- _(
88
- "Cannot declare transitions from final state. Invalid state(s): {}"
89
- ).format([s.id for s in final_state_with_invalid_transitions])
101
+ _("Cannot declare transitions from final state. Invalid state(s): {}").format(
102
+ [s.id for s in final_state_with_invalid_transitions]
103
+ )
90
104
  )
91
105
 
106
+ def _check_trap_states(cls):
107
+ trap_states = [s for s in cls.states if not s.final and not s.transitions]
108
+ if trap_states:
109
+ message = _(
110
+ "All non-final states should have at least one outgoing transition. "
111
+ "These states have no outgoing transition: {!r}"
112
+ ).format([s.id for s in trap_states])
113
+ if cls._strict_states:
114
+ raise InvalidDefinition(message)
115
+ else:
116
+ warnings.warn(message, UserWarning, stacklevel=4)
117
+
118
+ def _check_reachable_final_states(cls):
119
+ if not any(s.final for s in cls.states):
120
+ return # No need to check final reachability
121
+ disconnected_states = cls._states_without_path_to_final_states()
122
+ if disconnected_states:
123
+ message = _(
124
+ "All non-final states should have at least one path to a final state. "
125
+ "These states have no path to a final state: {!r}"
126
+ ).format([s.id for s in disconnected_states])
127
+ if cls._strict_states:
128
+ raise InvalidDefinition(message)
129
+ else:
130
+ warnings.warn(message, UserWarning, stacklevel=1)
131
+
132
+ def _states_without_path_to_final_states(cls):
133
+ return [
134
+ state
135
+ for state in cls.states
136
+ if not state.final and not any(s.final for s in visit_connected_states(state))
137
+ ]
138
+
92
139
  def _disconnected_states(cls, starting_state):
93
140
  visitable_states = set(visit_connected_states(starting_state))
94
141
  return set(cls.states) - visitable_states
@@ -104,6 +151,23 @@ class StateMachineMetaclass(type):
104
151
  ).format([s.id for s in disconnected_states])
105
152
  )
106
153
 
154
+ def _setup(cls):
155
+ for visited in iterate_states_and_transitions(cls.states):
156
+ visited._setup()
157
+
158
+ cls._protected_attrs = {
159
+ "_abstract",
160
+ "model",
161
+ "state_field",
162
+ "start_value",
163
+ "initial_state",
164
+ "final_states",
165
+ "states",
166
+ "_events",
167
+ "states_map",
168
+ "send",
169
+ } | {s.id for s in cls.states}
170
+
107
171
  def add_inherited(cls, bases):
108
172
  for base in bases:
109
173
  for state in getattr(base, "states", []):
statemachine/graph.py CHANGED
@@ -12,3 +12,9 @@ def visit_connected_states(state):
12
12
  already_visited.add(state)
13
13
  yield state
14
14
  visit.extend(t.target for t in state.transitions)
15
+
16
+
17
+ def iterate_states_and_transitions(states):
18
+ for state in states:
19
+ yield state
20
+ yield from state.transitions
statemachine/mixins.py CHANGED
@@ -20,9 +20,7 @@ class MachineMixin:
20
20
  super().__init__(*args, **kwargs)
21
21
  if not self.state_machine_name:
22
22
  raise ValueError(
23
- _("{!r} is not a valid state machine name.").format(
24
- self.state_machine_name
25
- )
23
+ _("{!r} is not a valid state machine name.").format(self.state_machine_name)
26
24
  )
27
25
  machine_cls = registry.get_machine_cls(self.state_machine_name)
28
26
  setattr(
statemachine/signature.py CHANGED
@@ -9,6 +9,7 @@ from typing import Any
9
9
 
10
10
  def _make_key(method):
11
11
  method = method.func if isinstance(method, partial) else method
12
+ method = method.fget if isinstance(method, property) else method
12
13
  if isinstance(method, MethodType):
13
14
  return hash(
14
15
  (
@@ -22,7 +23,6 @@ def _make_key(method):
22
23
 
23
24
 
24
25
  def signature_cache(user_function):
25
-
26
26
  cache = {}
27
27
  cache_get = cache.get
28
28
 
@@ -113,8 +113,7 @@ class SignatureAdapter(Signature):
113
113
  parameters_ex = (param,)
114
114
  break
115
115
  elif (
116
- param.kind == Parameter.VAR_KEYWORD
117
- or param.default is not Parameter.empty
116
+ param.kind == Parameter.VAR_KEYWORD or param.default is not Parameter.empty
118
117
  ):
119
118
  # That's fine too - we have a default value for this
120
119
  # parameter. So, lets start parsing `kwargs`, starting
statemachine/state.py CHANGED
@@ -4,6 +4,7 @@ from typing import Dict
4
4
  from weakref import ref
5
5
 
6
6
  from .callbacks import CallbackMetaList
7
+ from .callbacks import CallbackPriority
7
8
  from .exceptions import StateMachineError
8
9
  from .i18n import _
9
10
  from .transition import Transition
@@ -107,31 +108,30 @@ class State:
107
108
  self._final = final
108
109
  self._id: str = ""
109
110
  self.transitions = TransitionList()
110
- self.enter = CallbackMetaList().add(enter)
111
- self.exit = CallbackMetaList().add(exit)
111
+ self.enter = CallbackMetaList().add(enter, priority=CallbackPriority.INLINE)
112
+ self.exit = CallbackMetaList().add(exit, priority=CallbackPriority.INLINE)
112
113
 
113
114
  def __eq__(self, other):
114
- return (
115
- isinstance(other, State) and self.name == other.name and self.id == other.id
116
- )
115
+ return isinstance(other, State) and self.name == other.name and self.id == other.id
117
116
 
118
117
  def __hash__(self):
119
118
  return hash(repr(self))
120
119
 
121
- def _setup(self, register):
120
+ def _setup(self):
121
+ self.enter.add("on_enter_state", priority=CallbackPriority.GENERIC, suppress_errors=True)
122
+ self.enter.add(
123
+ f"on_enter_{self.id}", priority=CallbackPriority.NAMING, suppress_errors=True
124
+ )
125
+ self.exit.add("on_exit_state", priority=CallbackPriority.GENERIC, suppress_errors=True)
126
+ self.exit.add(f"on_exit_{self.id}", priority=CallbackPriority.NAMING, suppress_errors=True)
127
+
128
+ def _add_observer(self, register):
122
129
  register(self.enter)
123
130
  register(self.exit)
124
- return self
125
131
 
126
- def _add_observer(self, registry):
127
- self.enter.add(
128
- "on_enter_state", registry=registry, prepend=True, suppress_errors=True
129
- )
130
- self.enter.add(f"on_enter_{self.id}", registry=registry, suppress_errors=True)
131
- self.exit.add(
132
- "on_exit_state", registry=registry, prepend=True, suppress_errors=True
133
- )
134
- self.exit.add(f"on_exit_{self.id}", registry=registry, suppress_errors=True)
132
+ def _check_callbacks(self, check):
133
+ check(self.enter)
134
+ check(self.exit)
135
135
 
136
136
  def __repr__(self):
137
137
  return (
@@ -139,6 +139,9 @@ class State:
139
139
  f"initial={self.initial!r}, final={self.final!r})"
140
140
  )
141
141
 
142
+ def __str__(self):
143
+ return self.name
144
+
142
145
  def __get__(self, machine, owner):
143
146
  if machine is None:
144
147
  return self
@@ -146,14 +149,10 @@ class State:
146
149
 
147
150
  def __set__(self, instance, value):
148
151
  raise StateMachineError(
149
- _("State overriding is not allowed. Trying to add '{}' to {}").format(
150
- value, self.id
151
- )
152
+ _("State overriding is not allowed. Trying to add '{}' to {}").format(value, self.id)
152
153
  )
153
154
 
154
- def for_instance(
155
- self, machine: "StateMachine", cache: Dict["State", "State"]
156
- ) -> "State":
155
+ def for_instance(self, machine: "StateMachine", cache: Dict["State", "State"]) -> "State":
157
156
  if self not in cache:
158
157
  cache[self] = InstanceState(self, machine)
159
158
 
@@ -171,9 +170,7 @@ class State:
171
170
  self.name = self._id.replace("_", " ").capitalize()
172
171
 
173
172
  def _to_(self, *states: "State", **kwargs):
174
- transitions = TransitionList(
175
- Transition(self, state, **kwargs) for state in states
176
- )
173
+ transitions = TransitionList(Transition(self, state, **kwargs) for state in states)
177
174
  self.transitions.add_transitions(transitions)
178
175
  return transitions
179
176
 
@@ -1,9 +1,14 @@
1
1
  from collections import deque
2
+ from copy import deepcopy
2
3
  from functools import partial
3
4
  from typing import TYPE_CHECKING
4
5
  from typing import Any
5
6
  from typing import Dict
6
7
 
8
+ from statemachine.graph import iterate_states_and_transitions
9
+
10
+ from .callbacks import CallbackMetaList
11
+ from .callbacks import CallbacksExecutor
7
12
  from .callbacks import CallbacksRegistry
8
13
  from .dispatcher import ObjectConfig
9
14
  from .dispatcher import resolver_factory
@@ -73,22 +78,22 @@ class StateMachine(metaclass=StateMachineMetaclass):
73
78
  self._external_queue: deque = deque()
74
79
  self._callbacks_registry = CallbacksRegistry()
75
80
  self._states_for_instance: Dict["State", "State"] = {}
81
+ self._observers: Dict[Any, Any] = {}
76
82
 
77
- assert hasattr(self, "_abstract")
78
83
  if self._abstract:
79
84
  raise InvalidDefinition(_("There are no states or transitions."))
80
85
 
81
- initial_transition = Transition(
82
- None, self._get_initial_state(), event="__initial__"
83
- )
84
- self._setup(initial_transition)
85
- self._activate_initial_state(initial_transition)
86
+ self._register_callbacks()
87
+ self._activate_initial_state()
88
+
89
+ def __init_subclass__(cls, strict_states: bool = False):
90
+ cls._strict_states = strict_states
91
+ super().__init_subclass__()
86
92
 
87
93
  if TYPE_CHECKING:
88
94
  """Makes mypy happy with dynamic created attributes"""
89
95
 
90
- def __getattr__(self, attribute: str) -> Any:
91
- ...
96
+ def __getattr__(self, attribute: str) -> Any: ...
92
97
 
93
98
  def __repr__(self):
94
99
  current_state_id = self.current_state.id if self.current_state_value else None
@@ -97,19 +102,31 @@ class StateMachine(metaclass=StateMachineMetaclass):
97
102
  f"current_state={current_state_id!r})"
98
103
  )
99
104
 
105
+ def __deepcopy__(self, memo):
106
+ deepcopy_method = self.__deepcopy__
107
+ self.__deepcopy__ = None
108
+ try:
109
+ cp = deepcopy(self, memo)
110
+ finally:
111
+ self.__deepcopy__ = deepcopy_method
112
+ cp.__deepcopy__ = deepcopy_method
113
+ cp._callbacks_registry.clear()
114
+ cp._register_callbacks()
115
+ cp.add_observer(*cp._observers.keys())
116
+ return cp
117
+
100
118
  def _get_initial_state(self):
101
- current_state_value = (
102
- self.start_value if self.start_value else self.initial_state.value
103
- )
119
+ current_state_value = self.start_value if self.start_value else self.initial_state.value
104
120
  try:
105
121
  return self.states_map[current_state_value]
106
122
  except KeyError as err:
107
123
  raise InvalidStateValue(current_state_value) from err
108
124
 
109
- def _activate_initial_state(self, initial_transition):
125
+ def _activate_initial_state(self):
110
126
  if self.current_state_value is None:
111
127
  # send an one-time event `__initial__` to enter the current state.
112
128
  # current_state = self.current_state
129
+ initial_transition = Transition(None, self._get_initial_state(), event="__initial__")
113
130
  initial_transition.before.clear()
114
131
  initial_transition.on.clear()
115
132
  initial_transition.after.clear()
@@ -123,63 +140,29 @@ class StateMachine(metaclass=StateMachineMetaclass):
123
140
  )
124
141
  self._activate(event_data)
125
142
 
126
- def _get_protected_attrs(self):
127
- return {
128
- "_abstract",
129
- "model",
130
- "state_field",
131
- "start_value",
132
- "initial_state",
133
- "final_states",
134
- "states",
135
- "_events",
136
- "states_map",
137
- "send",
138
- } | {s.id for s in self.states}
139
-
140
- def _visit_states_and_transitions(self, visitor):
141
- for state in self.states:
142
- visitor(state)
143
- for transition in state.transitions:
144
- visitor(transition)
145
-
146
- def _setup(self, initial_transition: Transition):
147
- """
148
- Args:
149
- initial_transition: A special :ref:`transition` that triggers the enter on the
150
- `initial` :ref:`State`.
151
- """
152
- machine = ObjectConfig.from_obj(self, skip_attrs=self._get_protected_attrs())
153
- model = ObjectConfig.from_obj(self.model, skip_attrs={self.state_field})
154
- default_resolver = resolver_factory(machine, model)
155
-
156
- register = partial(self._callbacks_registry.register, resolver=default_resolver)
157
-
158
- observer_visitor = self._build_observers_visitor(machine, model)
159
-
160
- def setup_visitor(visited):
161
- visited._setup(register)
162
- observer_visitor(visited)
163
-
164
- self._visit_states_and_transitions(setup_visitor)
165
-
166
- initial_transition._setup(register)
167
-
168
- def _build_observers_visitor(self, *observers):
169
- registry_callbacks = [
143
+ def _register_callbacks(self):
144
+ self._add_observer(
170
145
  (
171
- self._callbacks_registry.build_register_function_for_resolver(
172
- resolver_factory(observer)
173
- )
146
+ ObjectConfig.from_obj(self, skip_attrs=self._protected_attrs),
147
+ ObjectConfig.from_obj(self.model, skip_attrs={self.state_field}),
174
148
  )
175
- for observer in observers
176
- ]
149
+ )
177
150
 
178
- def _add_observer_for_resolver(visited):
179
- for register in registry_callbacks:
180
- visited._add_observer(register)
151
+ check_callbacks = self._callbacks_registry.check
152
+ for visited in iterate_states_and_transitions(self.states):
153
+ try:
154
+ visited._check_callbacks(check_callbacks)
155
+ except Exception as err:
156
+ raise InvalidDefinition(
157
+ f"Error on {visited!s} when resolving callbacks: {err}"
158
+ ) from err
181
159
 
182
- return _add_observer_for_resolver
160
+ def _add_observer(self, observers):
161
+ register = partial(self._callbacks_registry.register, resolver=resolver_factory(observers))
162
+ for visited in iterate_states_and_transitions(self.states):
163
+ visited._add_observer(register)
164
+
165
+ return self
183
166
 
184
167
  def add_observer(self, *observers):
185
168
  """Add an observer.
@@ -191,10 +174,8 @@ class StateMachine(metaclass=StateMachineMetaclass):
191
174
 
192
175
  :ref:`observers`.
193
176
  """
194
-
195
- visitor = self._build_observers_visitor(*observers)
196
- self._visit_states_and_transitions(visitor)
197
- return self
177
+ self._observers.update({o: None for o in observers})
178
+ return self._add_observer(tuple(ObjectConfig.from_obj(o) for o in observers))
198
179
 
199
180
  def _repr_html_(self):
200
181
  return f'<div class="statemachine">{self._repr_svg_()}</div>'
@@ -248,10 +229,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
248
229
  @property
249
230
  def allowed_events(self):
250
231
  """List of the current allowed events."""
251
- return [
252
- getattr(self, event)
253
- for event in self.current_state.transitions.unique_events
254
- ]
232
+ return [getattr(self, event) for event in self.current_state.transitions.unique_events]
255
233
 
256
234
  def _process(self, trigger):
257
235
  """Process event triggers.
@@ -317,15 +295,13 @@ class StateMachine(metaclass=StateMachineMetaclass):
317
295
  source = event_data.state
318
296
  target = transition.target
319
297
 
320
- result = self._callbacks_registry[transition.before].call(
298
+ result = self._callbacks(transition.before).call(
321
299
  *event_data.args, **event_data.extended_kwargs
322
300
  )
323
301
  if source is not None and not transition.internal:
324
- self._callbacks_registry[source.exit].call(
325
- *event_data.args, **event_data.extended_kwargs
326
- )
302
+ self._callbacks(source.exit).call(*event_data.args, **event_data.extended_kwargs)
327
303
 
328
- result += self._callbacks_registry[transition.on].call(
304
+ result += self._callbacks(transition.on).call(
329
305
  *event_data.args, **event_data.extended_kwargs
330
306
  )
331
307
 
@@ -333,12 +309,8 @@ class StateMachine(metaclass=StateMachineMetaclass):
333
309
  event_data.state = target
334
310
 
335
311
  if not transition.internal:
336
- self._callbacks_registry[target.enter].call(
337
- *event_data.args, **event_data.extended_kwargs
338
- )
339
- self._callbacks_registry[transition.after].call(
340
- *event_data.args, **event_data.extended_kwargs
341
- )
312
+ self._callbacks(target.enter).call(*event_data.args, **event_data.extended_kwargs)
313
+ self._callbacks(transition.after).call(*event_data.args, **event_data.extended_kwargs)
342
314
 
343
315
  if len(result) == 0:
344
316
  result = None
@@ -357,3 +329,6 @@ class StateMachine(metaclass=StateMachineMetaclass):
357
329
  """
358
330
  event = Event(event)
359
331
  return event.trigger(self, *args, **kwargs)
332
+
333
+ def _callbacks(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
334
+ return self._callbacks_registry[meta_list]
statemachine/states.py CHANGED
@@ -15,21 +15,23 @@ class States:
15
15
  Helps creating :ref:`StateMachine`'s :ref:`state` definitions from other
16
16
  sources, like an ``Enum`` class, using :meth:`States.from_enum`.
17
17
 
18
+ >>> states_def = [('open', {'initial': True}), ('closed', {'final': True})]
19
+
18
20
  >>> from statemachine import StateMachine
19
21
  >>> class SM(StateMachine):
20
22
  ...
21
23
  ... states = States({
22
- ... name: State(initial=idx == 0) for idx, name in enumerate(["initial", "final"])
24
+ ... name: State(**params) for name, params in states_def
23
25
  ... })
24
26
  ...
25
- ... finish = states.initial.to(states.final)
27
+ ... close = states.open.to(states.closed)
26
28
 
27
29
  And states can be used as usual.
28
30
 
29
31
  >>> sm = SM()
30
- >>> sm.send("finish")
32
+ >>> sm.send("close")
31
33
  >>> sm.current_state.id
32
- 'final'
34
+ 'closed'
33
35
 
34
36
  """
35
37
 
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING
2
2
 
3
3
  from .callbacks import BoolCallbackMeta
4
4
  from .callbacks import CallbackMetaList
5
+ from .callbacks import CallbackPriority
5
6
  from .event import same_event_cond_builder
6
7
  from .events import Events
7
8
  from .exceptions import InvalidDefinition
@@ -48,7 +49,6 @@ class Transition:
48
49
  before=None,
49
50
  after=None,
50
51
  ):
51
-
52
52
  self.source = source
53
53
  self.target = target
54
54
  self.internal = internal
@@ -57,14 +57,14 @@ class Transition:
57
57
  raise InvalidDefinition("Internal transitions should be self-transitions.")
58
58
 
59
59
  self._events = Events().add(event)
60
- self.validators = CallbackMetaList().add(validators)
61
- self.before = CallbackMetaList().add(before)
62
- self.on = CallbackMetaList().add(on)
63
- self.after = CallbackMetaList().add(after)
60
+ self.validators = CallbackMetaList().add(validators, priority=CallbackPriority.INLINE)
61
+ self.before = CallbackMetaList().add(before, priority=CallbackPriority.INLINE)
62
+ self.on = CallbackMetaList().add(on, priority=CallbackPriority.INLINE)
63
+ self.after = CallbackMetaList().add(after, priority=CallbackPriority.INLINE)
64
64
  self.cond = (
65
65
  CallbackMetaList(factory=BoolCallbackMeta)
66
- .add(cond)
67
- .add(unless, expected_value=False)
66
+ .add(cond, priority=CallbackPriority.INLINE)
67
+ .add(unless, priority=CallbackPriority.INLINE, expected_value=False)
68
68
  )
69
69
 
70
70
  def __repr__(self):
@@ -73,49 +73,58 @@ class Transition:
73
73
  f"internal={self.internal!r})"
74
74
  )
75
75
 
76
- def _setup(self, register):
77
- register(self.validators)
78
- register(self.cond)
79
- register(self.before)
80
- register(self.on)
81
- register(self.after)
76
+ def __str__(self):
77
+ return f"transition {self.event!s} from {self.source!s} to {self.target!s}"
82
78
 
83
- def _add_observer(self, registry):
79
+ def _setup(self):
84
80
  before = self.before.add
85
81
  on = self.on.add
86
82
  after = self.after.add
87
- before(
88
- "before_transition", registry=registry, suppress_errors=True, prepend=True
89
- )
90
- on("on_transition", registry=registry, suppress_errors=True, prepend=True)
83
+
84
+ before("before_transition", priority=CallbackPriority.GENERIC, suppress_errors=True)
85
+ on("on_transition", priority=CallbackPriority.GENERIC, suppress_errors=True)
91
86
 
92
87
  for event in self._events:
93
88
  same_event_cond = same_event_cond_builder(event)
94
89
  before(
95
90
  f"before_{event}",
96
- registry=registry,
91
+ priority=CallbackPriority.NAMING,
97
92
  suppress_errors=True,
98
93
  cond=same_event_cond,
99
94
  )
100
95
  on(
101
96
  f"on_{event}",
102
- registry=registry,
97
+ priority=CallbackPriority.NAMING,
103
98
  suppress_errors=True,
104
99
  cond=same_event_cond,
105
100
  )
106
101
  after(
107
102
  f"after_{event}",
108
- registry=registry,
103
+ priority=CallbackPriority.NAMING,
109
104
  suppress_errors=True,
110
105
  cond=same_event_cond,
111
106
  )
112
107
 
113
108
  after(
114
109
  "after_transition",
115
- registry=registry,
110
+ priority=CallbackPriority.AFTER,
116
111
  suppress_errors=True,
117
112
  )
118
113
 
114
+ def _add_observer(self, register):
115
+ register(self.validators)
116
+ register(self.cond)
117
+ register(self.before)
118
+ register(self.on)
119
+ register(self.after)
120
+
121
+ def _check_callbacks(self, check):
122
+ check(self.validators)
123
+ check(self.cond)
124
+ check(self.before)
125
+ check(self.on)
126
+ check(self.after)
127
+
119
128
  def match(self, event):
120
129
  return self._events.match(event)
121
130
 
statemachine/utils.py CHANGED
@@ -6,6 +6,10 @@ def qualname(cls):
6
6
 
7
7
 
8
8
  def ensure_iterable(obj):
9
+ """
10
+ Returns an iterator if obj is not an instance of string or if it
11
+ encounters type error, otherwise it returns a list.
12
+ """
9
13
  if isinstance(obj, str):
10
14
  return [obj]
11
15
  try:
@@ -1,30 +0,0 @@
1
- statemachine/__init__.py,sha256=Bx88FyKa4Gm2RZxCWmYSxlefw0hVvOJMdWcqpsEiCgc,192
2
- statemachine/callbacks.py,sha256=_tmx5AZlhn_f3zhtUNAKBwfkkOKQLPBfSByhDUdFCeo,8265
3
- statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- statemachine/contrib/diagram.py,sha256=_UxkX0ycOhzCAuvV_b3hrvZ6V_VOUFmbBigI3oz4PCA,6748
5
- statemachine/dispatcher.py,sha256=GMAwivNTz_m8X9szPqKD9O0cV2XoWhPkBE8d3btsFHY,3598
6
- statemachine/event.py,sha256=fvWxyFcrbPVxNW94p66rGJ5I1UZUK9mDJz68udkz2JI,2106
7
- statemachine/event_data.py,sha256=nxOfypngK-78A77gj4pS-oxLx9zl1S9wnSt_rTiCyZA,2257
8
- statemachine/events.py,sha256=rUbiTX-QGoo7zzmQMEeWeLrIVnp9zhicdRIm34CoAVI,731
9
- statemachine/exceptions.py,sha256=uzvDbHkzCZkmFI_02L0yC9gd6d0SBn9GmsI8ekc0Hjk,952
10
- statemachine/factory.py,sha256=KGAa3rSnxTFyPSD1UXDgkGtBpg95A5vFISq3YCyvccQ,5779
11
- statemachine/graph.py,sha256=QTNlEmEYCq6NfX6yiKrjfEY8bpbGASgV-M1g5TNR7YM,359
12
- statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
13
- statemachine/locale/en/LC_MESSAGES/statemachine.mo,sha256=aGIvF2G_OISexcLv3ZsudPfAkVIvL33zaEGeXJM47-I,452
14
- statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=MfODCwzML6DzhOpHbno2Pdhsb26AWboR5Ka1JCNvJIA,1685
15
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.mo,sha256=wTh7ge52cALzWScvS1wdjT-QO5ZhrsvodiI-dscYZhI,1914
16
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=KsR9rlnhw17EZG-IbVx3F0wJAJSK0B-OByGmUmDHDUM,2375
17
- statemachine/mixins.py,sha256=54aOvLZh9TyAIrqwfufGNmo1K4gaJR_dzhPdzisGPMk,1055
18
- statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
19
- statemachine/registry.py,sha256=RnVBRS3I_6Tm2OMgXMB_ewX7zQaslqEfhXFOhbqIkG4,959
20
- statemachine/signature.py,sha256=o8HLzhysMSnH_yy8qWG7aW5jVMOWdJM47OMnMyGVfC4,7463
21
- statemachine/state.py,sha256=YJ6NxsYi6KYxMTH8wp_jf4yZL2FSYN_HZmcqJXmch1w,8175
22
- statemachine/statemachine.py,sha256=rol4RKXG7R0hsibLCkcQMs7v3__v069IJc7xtFZuhRc,11998
23
- statemachine/states.py,sha256=gK-_csTDatvX4DYJcHUB45Ealv4FnfVz1Bj7IAS5nn0,4151
24
- statemachine/transition.py,sha256=7UO6Kjb73tsF7nHfaYM94J-FsByZJN81k_KtCBB_1vY,4833
25
- statemachine/transition_list.py,sha256=DatsmMWgK0YK30Nrj-josVvlTgeGapKutzYur9-puF8,5949
26
- statemachine/utils.py,sha256=hY72gKE7VT9dn3xW5ffjKZosVblGbga8G9sKLEh5ZFg,317
27
- python_statemachine-2.1.2.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
28
- python_statemachine-2.1.2.dist-info/METADATA,sha256=3KLowdmDsCjT4zMeeFpan_7g-HVlLGoCgiiLzwzRybA,11454
29
- python_statemachine-2.1.2.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
30
- python_statemachine-2.1.2.dist-info/RECORD,,