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.
- {python_statemachine-2.3.1.dist-info → python_statemachine-2.3.2.dist-info}/METADATA +3 -23
- python_statemachine-2.3.2.dist-info/RECORD +31 -0
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +183 -111
- statemachine/contrib/diagram.py +7 -4
- statemachine/dispatcher.py +127 -135
- statemachine/engines/__init__.py +0 -0
- statemachine/engines/async_.py +136 -0
- statemachine/engines/sync.py +139 -0
- statemachine/event.py +9 -28
- statemachine/event_data.py +2 -4
- statemachine/events.py +2 -2
- statemachine/factory.py +10 -11
- statemachine/mixins.py +10 -4
- statemachine/registry.py +5 -8
- statemachine/signature.py +6 -5
- statemachine/state.py +13 -17
- statemachine/statemachine.py +111 -167
- statemachine/states.py +4 -3
- statemachine/transition.py +22 -45
- statemachine/transition_list.py +6 -8
- statemachine/utils.py +1 -1
- python_statemachine-2.3.1.dist-info/RECORD +0 -28
- {python_statemachine-2.3.1.dist-info → python_statemachine-2.3.2.dist-info}/LICENSE +0 -0
- {python_statemachine-2.3.1.dist-info → python_statemachine-2.3.2.dist-info}/WHEEL +0 -0
statemachine/factory.py
CHANGED
|
@@ -185,7 +185,7 @@ class StateMachineMetaclass(type):
|
|
|
185
185
|
cls.add_state(key, value)
|
|
186
186
|
elif isinstance(value, (Transition, TransitionList)):
|
|
187
187
|
cls.add_event(key, value)
|
|
188
|
-
elif getattr(value, "
|
|
188
|
+
elif getattr(value, "_specs_to_update", None):
|
|
189
189
|
cls._add_unbounded_callback(key, value)
|
|
190
190
|
|
|
191
191
|
def _add_states_from_dict(cls, states):
|
|
@@ -193,16 +193,15 @@ class StateMachineMetaclass(type):
|
|
|
193
193
|
cls.add_state(state_id, state)
|
|
194
194
|
|
|
195
195
|
def _add_unbounded_callback(cls, attr_name, func):
|
|
196
|
-
if func
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
ref(attr_name)
|
|
196
|
+
# if func is an event, the `attr_name` will be replaced by an event trigger,
|
|
197
|
+
# so we'll also give the ``func`` a new unique name to be used by the callback
|
|
198
|
+
# machinery.
|
|
199
|
+
cls.add_event(attr_name, func._transitions)
|
|
200
|
+
attr_name = f"_{attr_name}_{uuid4().hex}"
|
|
201
|
+
setattr(cls, attr_name, func)
|
|
202
|
+
|
|
203
|
+
for ref in func._specs_to_update:
|
|
204
|
+
ref(getattr(cls, attr_name), attr_name)
|
|
206
205
|
|
|
207
206
|
def add_state(cls, id, state: State):
|
|
208
207
|
state._set_id(id)
|
statemachine/mixins.py
CHANGED
|
@@ -7,15 +7,18 @@ class MachineMixin:
|
|
|
7
7
|
``StateMachine``.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
state_field_name = "state"
|
|
10
|
+
state_field_name: str = "state"
|
|
11
11
|
"""The model's state field name that will hold the state value."""
|
|
12
12
|
|
|
13
|
-
state_machine_name
|
|
13
|
+
state_machine_name: "str | None" = None
|
|
14
14
|
"""A fully qualified name of the class, where it can be imported."""
|
|
15
15
|
|
|
16
|
-
state_machine_attr = "statemachine"
|
|
16
|
+
state_machine_attr: str = "statemachine"
|
|
17
17
|
"""Name of the model's attribute that will hold the machine instance."""
|
|
18
18
|
|
|
19
|
+
bind_events_as_methods: bool = False
|
|
20
|
+
"""If ``True`` the state machine events triggers will be bound to the model as methods."""
|
|
21
|
+
|
|
19
22
|
def __init__(self, *args, **kwargs):
|
|
20
23
|
super().__init__(*args, **kwargs)
|
|
21
24
|
if not self.state_machine_name:
|
|
@@ -23,8 +26,11 @@ class MachineMixin:
|
|
|
23
26
|
_("{!r} is not a valid state machine name.").format(self.state_machine_name)
|
|
24
27
|
)
|
|
25
28
|
machine_cls = registry.get_machine_cls(self.state_machine_name)
|
|
29
|
+
sm = machine_cls(self, state_field=self.state_field_name)
|
|
26
30
|
setattr(
|
|
27
31
|
self,
|
|
28
32
|
self.state_machine_attr,
|
|
29
|
-
|
|
33
|
+
sm,
|
|
30
34
|
)
|
|
35
|
+
if self.bind_events_as_methods:
|
|
36
|
+
sm.bind_events_to(self)
|
statemachine/registry.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import warnings
|
|
2
2
|
|
|
3
|
+
from .utils import qualname
|
|
4
|
+
|
|
3
5
|
try:
|
|
4
|
-
_has_django = True
|
|
5
6
|
from django.utils.module_loading import autodiscover_modules
|
|
6
|
-
except ImportError:
|
|
7
|
+
except ImportError: # pragma: no cover
|
|
7
8
|
# Not a django project
|
|
8
|
-
autodiscover_modules
|
|
9
|
-
|
|
9
|
+
def autodiscover_modules(module_name: str):
|
|
10
|
+
pass
|
|
10
11
|
|
|
11
|
-
from .utils import qualname
|
|
12
12
|
|
|
13
13
|
_REGISTRY = {}
|
|
14
14
|
_initialized = False
|
|
@@ -39,8 +39,5 @@ def init_registry():
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def load_modules(modules=None):
|
|
42
|
-
if not _has_django:
|
|
43
|
-
return
|
|
44
|
-
|
|
45
42
|
for module in modules:
|
|
46
43
|
autodiscover_modules(module)
|
statemachine/signature.py
CHANGED
|
@@ -6,6 +6,7 @@ from inspect import iscoroutinefunction
|
|
|
6
6
|
from itertools import chain
|
|
7
7
|
from types import MethodType
|
|
8
8
|
from typing import Any
|
|
9
|
+
from typing import Callable
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def _make_key(method):
|
|
@@ -44,7 +45,7 @@ def signature_cache(user_function):
|
|
|
44
45
|
|
|
45
46
|
class SignatureAdapter(Signature):
|
|
46
47
|
@classmethod
|
|
47
|
-
def wrap(cls, method):
|
|
48
|
+
def wrap(cls, method) -> Callable:
|
|
48
49
|
"""Build a wrapper that adapts the received arguments to the inner ``method`` signature"""
|
|
49
50
|
|
|
50
51
|
sig = cls.from_callable(method)
|
|
@@ -54,18 +55,18 @@ class SignatureAdapter(Signature):
|
|
|
54
55
|
|
|
55
56
|
if iscoroutinefunction(method):
|
|
56
57
|
|
|
57
|
-
async def
|
|
58
|
+
async def signature_adapter(*args: Any, **kwargs: Any) -> Any:
|
|
58
59
|
ba = sig_bind_expected(*args, **kwargs)
|
|
59
60
|
return await method(*ba.args, **ba.kwargs)
|
|
60
61
|
else:
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
def signature_adapter(*args: Any, **kwargs: Any) -> Any: # type: ignore[misc]
|
|
63
64
|
ba = sig_bind_expected(*args, **kwargs)
|
|
64
65
|
return method(*ba.args, **ba.kwargs)
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
signature_adapter.__name__ = metadata_to_copy.__name__
|
|
67
68
|
|
|
68
|
-
return
|
|
69
|
+
return signature_adapter
|
|
69
70
|
|
|
70
71
|
@classmethod
|
|
71
72
|
@signature_cache
|
statemachine/state.py
CHANGED
|
@@ -3,8 +3,9 @@ from typing import Any
|
|
|
3
3
|
from typing import Dict
|
|
4
4
|
from weakref import ref
|
|
5
5
|
|
|
6
|
-
from .callbacks import
|
|
6
|
+
from .callbacks import CallbackGroup
|
|
7
7
|
from .callbacks import CallbackPriority
|
|
8
|
+
from .callbacks import CallbackSpecList
|
|
8
9
|
from .exceptions import StateMachineError
|
|
9
10
|
from .i18n import _
|
|
10
11
|
from .transition import Transition
|
|
@@ -108,8 +109,13 @@ class State:
|
|
|
108
109
|
self._final = final
|
|
109
110
|
self._id: str = ""
|
|
110
111
|
self.transitions = TransitionList()
|
|
111
|
-
self.
|
|
112
|
-
self.
|
|
112
|
+
self._specs = CallbackSpecList()
|
|
113
|
+
self.enter = self._specs.grouper(CallbackGroup.ENTER).add(
|
|
114
|
+
enter, priority=CallbackPriority.INLINE
|
|
115
|
+
)
|
|
116
|
+
self.exit = self._specs.grouper(CallbackGroup.EXIT).add(
|
|
117
|
+
exit, priority=CallbackPriority.INLINE
|
|
118
|
+
)
|
|
113
119
|
|
|
114
120
|
def __eq__(self, other):
|
|
115
121
|
return isinstance(other, State) and self.name == other.name and self.id == other.id
|
|
@@ -118,20 +124,10 @@ class State:
|
|
|
118
124
|
return hash(repr(self))
|
|
119
125
|
|
|
120
126
|
def _setup(self):
|
|
121
|
-
self.enter.add("on_enter_state", priority=CallbackPriority.GENERIC,
|
|
122
|
-
self.enter.add(
|
|
123
|
-
|
|
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):
|
|
129
|
-
register(self.enter)
|
|
130
|
-
register(self.exit)
|
|
131
|
-
|
|
132
|
-
def _check_callbacks(self, check):
|
|
133
|
-
check(self.enter)
|
|
134
|
-
check(self.exit)
|
|
127
|
+
self.enter.add("on_enter_state", priority=CallbackPriority.GENERIC, is_convention=True)
|
|
128
|
+
self.enter.add(f"on_enter_{self.id}", priority=CallbackPriority.NAMING, is_convention=True)
|
|
129
|
+
self.exit.add("on_exit_state", priority=CallbackPriority.GENERIC, is_convention=True)
|
|
130
|
+
self.exit.add(f"on_exit_{self.id}", priority=CallbackPriority.NAMING, is_convention=True)
|
|
135
131
|
|
|
136
132
|
def __repr__(self):
|
|
137
133
|
return (
|
statemachine/statemachine.py
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
|
+
import warnings
|
|
1
2
|
from collections import deque
|
|
2
3
|
from copy import deepcopy
|
|
3
4
|
from functools import partial
|
|
5
|
+
from inspect import isawaitable
|
|
6
|
+
from threading import Lock
|
|
4
7
|
from typing import TYPE_CHECKING
|
|
5
8
|
from typing import Any
|
|
6
9
|
from typing import Dict
|
|
10
|
+
from typing import List
|
|
7
11
|
|
|
8
12
|
from statemachine.graph import iterate_states_and_transitions
|
|
9
13
|
from statemachine.utils import run_async_from_sync
|
|
10
14
|
|
|
11
|
-
from .callbacks import CallbackMetaList
|
|
12
15
|
from .callbacks import CallbacksExecutor
|
|
13
16
|
from .callbacks import CallbacksRegistry
|
|
14
|
-
from .dispatcher import
|
|
15
|
-
from .dispatcher import
|
|
17
|
+
from .dispatcher import Listener
|
|
18
|
+
from .dispatcher import Listeners
|
|
19
|
+
from .engines.async_ import AsyncEngine
|
|
20
|
+
from .engines.sync import SyncEngine
|
|
16
21
|
from .event import Event
|
|
17
|
-
from .event_data import EventData
|
|
18
22
|
from .event_data import TriggerData
|
|
19
23
|
from .exceptions import InvalidDefinition
|
|
20
24
|
from .exceptions import InvalidStateValue
|
|
@@ -22,7 +26,6 @@ from .exceptions import TransitionNotAllowed
|
|
|
22
26
|
from .factory import StateMachineMetaclass
|
|
23
27
|
from .i18n import _
|
|
24
28
|
from .model import Model
|
|
25
|
-
from .transition import Transition
|
|
26
29
|
|
|
27
30
|
if TYPE_CHECKING:
|
|
28
31
|
from .state import State
|
|
@@ -49,6 +52,9 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
49
52
|
:ref:`transition`, including tolerance to unknown :ref:`event` triggers.
|
|
50
53
|
Default: ``False``.
|
|
51
54
|
|
|
55
|
+
listeners: An optional list of objects that provies attributes to be used as callbacks.
|
|
56
|
+
See :ref:`listeners` for more details.
|
|
57
|
+
|
|
52
58
|
"""
|
|
53
59
|
|
|
54
60
|
TransitionNotAllowed = TransitionNotAllowed
|
|
@@ -69,28 +75,50 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
69
75
|
start_value: Any = None,
|
|
70
76
|
rtc: bool = True,
|
|
71
77
|
allow_event_without_transition: bool = False,
|
|
78
|
+
listeners: "List[object] | None" = None,
|
|
72
79
|
):
|
|
73
80
|
self.model = model if model else Model()
|
|
74
81
|
self.state_field = state_field
|
|
75
82
|
self.start_value = start_value
|
|
76
83
|
self.allow_event_without_transition = allow_event_without_transition
|
|
77
|
-
self.__initialized = False
|
|
78
|
-
self.__rtc = rtc
|
|
79
|
-
self.__processing: bool = False
|
|
80
84
|
self._external_queue: deque = deque()
|
|
81
85
|
self._callbacks_registry = CallbacksRegistry()
|
|
82
86
|
self._states_for_instance: Dict[State, State] = {}
|
|
83
|
-
|
|
87
|
+
|
|
88
|
+
self._listeners: Dict[Any, Any] = {}
|
|
89
|
+
"""Listeners that provides attributes to be used as callbacks."""
|
|
84
90
|
|
|
85
91
|
if self._abstract:
|
|
86
92
|
raise InvalidDefinition(_("There are no states or transitions."))
|
|
87
93
|
|
|
88
|
-
self._register_callbacks()
|
|
94
|
+
self._register_callbacks(listeners or [])
|
|
89
95
|
|
|
90
96
|
# Activate the initial state, this only works if the outer scope is sync code.
|
|
91
97
|
# for async code, the user should manually call `await sm.activate_initial_state()`
|
|
92
98
|
# after state machine creation.
|
|
93
|
-
|
|
99
|
+
if self.current_state_value is None:
|
|
100
|
+
trigger_data = TriggerData(
|
|
101
|
+
machine=self,
|
|
102
|
+
event="__initial__",
|
|
103
|
+
)
|
|
104
|
+
self._put_nonblocking(trigger_data)
|
|
105
|
+
|
|
106
|
+
self._engine = self._get_engine(rtc)
|
|
107
|
+
|
|
108
|
+
def _get_engine(self, rtc: bool):
|
|
109
|
+
if self._callbacks_registry._method_types[True] > 0:
|
|
110
|
+
return AsyncEngine(self, rtc=rtc)
|
|
111
|
+
else:
|
|
112
|
+
return SyncEngine(self, rtc=rtc)
|
|
113
|
+
|
|
114
|
+
def activate_initial_state(self):
|
|
115
|
+
result = self._engine.activate_initial_state()
|
|
116
|
+
if not isawaitable(result):
|
|
117
|
+
return result
|
|
118
|
+
return run_async_from_sync(result)
|
|
119
|
+
|
|
120
|
+
def _processing_loop(self):
|
|
121
|
+
return self._engine._processing_loop()
|
|
94
122
|
|
|
95
123
|
def __init_subclass__(cls, strict_states: bool = False):
|
|
96
124
|
cls._strict_states = strict_states
|
|
@@ -110,15 +138,20 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
110
138
|
|
|
111
139
|
def __deepcopy__(self, memo):
|
|
112
140
|
deepcopy_method = self.__deepcopy__
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
141
|
+
lock = self._engine._processing
|
|
142
|
+
with lock:
|
|
143
|
+
self.__deepcopy__ = None
|
|
144
|
+
self._engine._processing = None
|
|
145
|
+
try:
|
|
146
|
+
cp = deepcopy(self, memo)
|
|
147
|
+
cp._engine._processing = Lock()
|
|
148
|
+
finally:
|
|
149
|
+
self.__deepcopy__ = deepcopy_method
|
|
150
|
+
cp.__deepcopy__ = deepcopy_method
|
|
151
|
+
self._engine._processing = lock
|
|
119
152
|
cp._callbacks_registry.clear()
|
|
120
|
-
cp._register_callbacks()
|
|
121
|
-
cp.
|
|
153
|
+
cp._register_callbacks([])
|
|
154
|
+
cp.add_listener(*cp._listeners.keys())
|
|
122
155
|
return cp
|
|
123
156
|
|
|
124
157
|
def _get_initial_state(self):
|
|
@@ -128,79 +161,75 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
128
161
|
except KeyError as err:
|
|
129
162
|
raise InvalidStateValue(current_state_value) from err
|
|
130
163
|
|
|
131
|
-
|
|
132
|
-
"""
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
initial_transition = Transition(None, self._get_initial_state(), event="__initial__")
|
|
152
|
-
initial_transition.before.clear()
|
|
153
|
-
initial_transition.on.clear()
|
|
154
|
-
initial_transition.after.clear()
|
|
155
|
-
|
|
156
|
-
event_data = EventData(
|
|
157
|
-
trigger_data=TriggerData(
|
|
158
|
-
machine=self,
|
|
159
|
-
event=initial_transition.event,
|
|
160
|
-
),
|
|
161
|
-
transition=initial_transition,
|
|
162
|
-
)
|
|
163
|
-
await self._activate(event_data)
|
|
164
|
+
def bind_events_to(self, *targets):
|
|
165
|
+
"""Bind the state machine events to the target objects."""
|
|
166
|
+
|
|
167
|
+
for event in self.events:
|
|
168
|
+
trigger = getattr(self, event.name)
|
|
169
|
+
for target in targets:
|
|
170
|
+
if hasattr(target, event.name):
|
|
171
|
+
warnings.warn(
|
|
172
|
+
f"Attribute {event.name!r} already exists on {target!r}. "
|
|
173
|
+
f"Skipping binding.",
|
|
174
|
+
UserWarning,
|
|
175
|
+
stacklevel=2,
|
|
176
|
+
)
|
|
177
|
+
continue
|
|
178
|
+
setattr(target, event.name, trigger)
|
|
179
|
+
|
|
180
|
+
def _add_listener(self, listeners: "Listeners"):
|
|
181
|
+
register = partial(listeners.resolve, registry=self._callbacks_registry)
|
|
182
|
+
for visited in iterate_states_and_transitions(self.states):
|
|
183
|
+
register(visited._specs)
|
|
164
184
|
|
|
165
|
-
|
|
166
|
-
await self.activate_initial_state()
|
|
185
|
+
return self
|
|
167
186
|
|
|
168
|
-
def _register_callbacks(self):
|
|
169
|
-
self.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
def _register_callbacks(self, listeners: List[object]):
|
|
188
|
+
self._listeners.update({listener: None for listener in listeners})
|
|
189
|
+
self._add_listener(
|
|
190
|
+
Listeners.from_listeners(
|
|
191
|
+
(
|
|
192
|
+
Listener.from_obj(self, skip_attrs=self._protected_attrs),
|
|
193
|
+
Listener.from_obj(self.model, skip_attrs={self.state_field}),
|
|
194
|
+
*(Listener.from_obj(listener) for listener in listeners),
|
|
195
|
+
)
|
|
173
196
|
)
|
|
174
197
|
)
|
|
175
198
|
|
|
176
199
|
check_callbacks = self._callbacks_registry.check
|
|
177
200
|
for visited in iterate_states_and_transitions(self.states):
|
|
178
201
|
try:
|
|
179
|
-
visited.
|
|
202
|
+
check_callbacks(visited._specs)
|
|
180
203
|
except Exception as err:
|
|
181
204
|
raise InvalidDefinition(
|
|
182
205
|
f"Error on {visited!s} when resolving callbacks: {err}"
|
|
183
206
|
) from err
|
|
184
207
|
|
|
185
|
-
|
|
186
|
-
register = partial(self._callbacks_registry.register, resolver=resolver_factory(observers))
|
|
187
|
-
for visited in iterate_states_and_transitions(self.states):
|
|
188
|
-
visited._add_observer(register)
|
|
189
|
-
|
|
190
|
-
return self
|
|
208
|
+
self._callbacks_registry.async_or_sync()
|
|
191
209
|
|
|
192
210
|
def add_observer(self, *observers):
|
|
193
|
-
"""Add
|
|
211
|
+
"""Add a listener."""
|
|
212
|
+
warnings.warn(
|
|
213
|
+
"""The `add_observer` was rebranded to `add_listener`.""",
|
|
214
|
+
DeprecationWarning,
|
|
215
|
+
stacklevel=2,
|
|
216
|
+
)
|
|
217
|
+
return self.add_listener(*observers)
|
|
218
|
+
|
|
219
|
+
def add_listener(self, *listeners):
|
|
220
|
+
"""Add a listener.
|
|
194
221
|
|
|
195
|
-
|
|
222
|
+
Listener are a way to generically add behavior to a :ref:`StateMachine` without changing
|
|
196
223
|
its internal implementation.
|
|
197
224
|
|
|
198
225
|
.. seealso::
|
|
199
226
|
|
|
200
|
-
:ref:`
|
|
227
|
+
:ref:`listeners`.
|
|
201
228
|
"""
|
|
202
|
-
self.
|
|
203
|
-
return self.
|
|
229
|
+
self._listeners.update({o: None for o in listeners})
|
|
230
|
+
return self._add_listener(
|
|
231
|
+
Listeners.from_listeners(Listener.from_obj(o) for o in listeners)
|
|
232
|
+
)
|
|
204
233
|
|
|
205
234
|
def _repr_html_(self):
|
|
206
235
|
return f'<div class="statemachine">{self._repr_svg_()}</div>'
|
|
@@ -267,97 +296,9 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
267
296
|
"""List of the current allowed events."""
|
|
268
297
|
return [getattr(self, event) for event in self.current_state.transitions.unique_events]
|
|
269
298
|
|
|
270
|
-
|
|
271
|
-
"""
|
|
272
|
-
|
|
273
|
-
The simplest implementation is the non-RTC (synchronous),
|
|
274
|
-
where the trigger will be run immediately and the result collected as the return.
|
|
275
|
-
|
|
276
|
-
.. note::
|
|
277
|
-
|
|
278
|
-
While processing the trigger, if others events are generated, they
|
|
279
|
-
will also be processed immediately, so a "nested" behavior happens.
|
|
280
|
-
|
|
281
|
-
If the machine is on ``rtc`` model (queued), the event is put on a queue, and only the
|
|
282
|
-
first event will have the result collected.
|
|
283
|
-
|
|
284
|
-
.. note::
|
|
285
|
-
While processing the queue items, if others events are generated, they
|
|
286
|
-
will be processed sequentially (and not nested).
|
|
287
|
-
|
|
288
|
-
"""
|
|
289
|
-
if not self.__rtc:
|
|
290
|
-
# The machine is in "synchronous" mode
|
|
291
|
-
return await trigger()
|
|
292
|
-
|
|
293
|
-
# The machine is in "queued" mode
|
|
294
|
-
# Add the trigger to queue and start processing in a loop.
|
|
295
|
-
self._external_queue.append(trigger)
|
|
296
|
-
|
|
297
|
-
# We make sure that only the first event enters the processing critical section,
|
|
298
|
-
# next events will only be put on the queue and processed by the same loop.
|
|
299
|
-
if self.__processing:
|
|
300
|
-
return
|
|
301
|
-
|
|
302
|
-
return await self._processing_loop()
|
|
303
|
-
|
|
304
|
-
async def _processing_loop(self):
|
|
305
|
-
"""Execute the triggers in the queue in order until the queue is empty"""
|
|
306
|
-
self.__processing = True
|
|
307
|
-
|
|
308
|
-
# We will collect the first result as the processing result to keep backwards compatibility
|
|
309
|
-
# so we need to use a sentinel object instead of `None` because the first result may
|
|
310
|
-
# be also `None`, and on this case the `first_result` may be overridden by another result.
|
|
311
|
-
sentinel = object()
|
|
312
|
-
first_result = sentinel
|
|
313
|
-
try:
|
|
314
|
-
while self._external_queue:
|
|
315
|
-
trigger = self._external_queue.popleft()
|
|
316
|
-
try:
|
|
317
|
-
result = await trigger()
|
|
318
|
-
if first_result is sentinel:
|
|
319
|
-
first_result = result
|
|
320
|
-
except Exception:
|
|
321
|
-
# Whe clear the queue as we don't have an expected behavior
|
|
322
|
-
# and cannot keep processing
|
|
323
|
-
self._external_queue.clear()
|
|
324
|
-
raise
|
|
325
|
-
finally:
|
|
326
|
-
self.__processing = False
|
|
327
|
-
return first_result if first_result is not sentinel else None
|
|
328
|
-
|
|
329
|
-
async def _activate(self, event_data: EventData):
|
|
330
|
-
transition = event_data.transition
|
|
331
|
-
source = event_data.state
|
|
332
|
-
target = transition.target
|
|
333
|
-
|
|
334
|
-
result = await self._callbacks(transition.before).call(
|
|
335
|
-
*event_data.args, **event_data.extended_kwargs
|
|
336
|
-
)
|
|
337
|
-
if source is not None and not transition.internal:
|
|
338
|
-
await self._callbacks(source.exit).call(*event_data.args, **event_data.extended_kwargs)
|
|
339
|
-
|
|
340
|
-
result += await self._callbacks(transition.on).call(
|
|
341
|
-
*event_data.args, **event_data.extended_kwargs
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
self.current_state = target
|
|
345
|
-
event_data.state = target
|
|
346
|
-
|
|
347
|
-
if not transition.internal:
|
|
348
|
-
await self._callbacks(target.enter).call(
|
|
349
|
-
*event_data.args, **event_data.extended_kwargs
|
|
350
|
-
)
|
|
351
|
-
await self._callbacks(transition.after).call(
|
|
352
|
-
*event_data.args, **event_data.extended_kwargs
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
if len(result) == 0:
|
|
356
|
-
result = None
|
|
357
|
-
elif len(result) == 1:
|
|
358
|
-
result = result[0]
|
|
359
|
-
|
|
360
|
-
return result
|
|
299
|
+
def _put_nonblocking(self, trigger_data: TriggerData):
|
|
300
|
+
"""Put the trigger on the queue without blocking the caller."""
|
|
301
|
+
self._external_queue.append(trigger_data)
|
|
361
302
|
|
|
362
303
|
def send(self, event: str, *args, **kwargs):
|
|
363
304
|
"""Send an :ref:`Event` to the state machine.
|
|
@@ -370,9 +311,12 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
370
311
|
See: :ref:`triggering events`.
|
|
371
312
|
|
|
372
313
|
"""
|
|
373
|
-
|
|
314
|
+
result = self._async_send(event, *args, **kwargs)
|
|
315
|
+
if not isawaitable(result):
|
|
316
|
+
return result
|
|
317
|
+
return run_async_from_sync(result)
|
|
374
318
|
|
|
375
|
-
|
|
319
|
+
def _async_send(self, event: str, *args, **kwargs):
|
|
376
320
|
"""Send an :ref:`Event` to the state machine.
|
|
377
321
|
|
|
378
322
|
.. seealso::
|
|
@@ -381,7 +325,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
381
325
|
|
|
382
326
|
"""
|
|
383
327
|
event_instance: Event = Event(event)
|
|
384
|
-
return
|
|
328
|
+
return event_instance.trigger(self, *args, **kwargs)
|
|
385
329
|
|
|
386
|
-
def
|
|
387
|
-
return self._callbacks_registry[
|
|
330
|
+
def _get_callbacks(self, key) -> CallbacksExecutor:
|
|
331
|
+
return self._callbacks_registry[key]
|
statemachine/states.py
CHANGED
|
@@ -54,6 +54,7 @@ class States:
|
|
|
54
54
|
return list(self) == list(other)
|
|
55
55
|
|
|
56
56
|
def __getattr__(self, name: str):
|
|
57
|
+
name = name.lower()
|
|
57
58
|
if name in self._states:
|
|
58
59
|
return self._states[name]
|
|
59
60
|
raise AttributeError(f"{name} not found in {self.__class__.__name__}")
|
|
@@ -80,7 +81,7 @@ class States:
|
|
|
80
81
|
return self._states.items()
|
|
81
82
|
|
|
82
83
|
@classmethod
|
|
83
|
-
def from_enum(cls, enum_type: EnumType, initial
|
|
84
|
+
def from_enum(cls, enum_type: EnumType, initial, final=None):
|
|
84
85
|
"""
|
|
85
86
|
Creates a new instance of the ``States`` class from an enumeration.
|
|
86
87
|
|
|
@@ -124,7 +125,7 @@ class States:
|
|
|
124
125
|
True
|
|
125
126
|
|
|
126
127
|
>>> sm.current_state_value
|
|
127
|
-
2
|
|
128
|
+
<Status.completed: 2>
|
|
128
129
|
|
|
129
130
|
Args:
|
|
130
131
|
enum_type: An enumeration containing the states of the machine.
|
|
@@ -137,7 +138,7 @@ class States:
|
|
|
137
138
|
final_set = set(ensure_iterable(final))
|
|
138
139
|
return cls(
|
|
139
140
|
{
|
|
140
|
-
e.name: State(value=e
|
|
141
|
+
e.name.lower(): State(value=e, initial=e is initial, final=e in final_set)
|
|
141
142
|
for e in enum_type
|
|
142
143
|
}
|
|
143
144
|
)
|