python-statemachine 2.3.2__tar.gz → 2.3.4__tar.gz
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.2 → python_statemachine-2.3.4}/PKG-INFO +1 -1
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/pyproject.toml +8 -7
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/__init__.py +1 -1
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/callbacks.py +11 -7
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/dispatcher.py +13 -5
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/engines/async_.py +2 -2
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/engines/sync.py +2 -2
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/statemachine.py +16 -12
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/states.py +26 -9
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/LICENSE +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/README.md +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/contrib/__init__.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/contrib/diagram.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/engines/__init__.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/event.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/event_data.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/events.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/exceptions.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/factory.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/graph.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/i18n.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/locale/en/LC_MESSAGES/statemachine.po +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/mixins.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/model.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/registry.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/signature.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/state.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/transition.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/transition_list.py +0 -0
- {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-statemachine"
|
|
3
|
-
version = "2.3.
|
|
3
|
+
version = "2.3.4"
|
|
4
4
|
description = "Python Finite State Machines made easy."
|
|
5
5
|
authors = ["Fernando Macedo <fgmacedo@gmail.com>"]
|
|
6
6
|
maintainers = [
|
|
@@ -56,12 +56,13 @@ django = { version = "^5.0.3", python = ">3.10" }
|
|
|
56
56
|
pytest-django = { version = "^4.8.0", python = ">3.8" }
|
|
57
57
|
|
|
58
58
|
[tool.poetry.group.docs.dependencies]
|
|
59
|
-
Sphinx = "*"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
Sphinx = { version = "*", python = ">3.8" }
|
|
60
|
+
myst-parser = { version = "*", python = ">3.8" }
|
|
61
|
+
sphinx-gallery = { version = "*", python = ">3.8" }
|
|
62
|
+
pillow = { version ="*", python = ">3.8" }
|
|
63
|
+
sphinx-autobuild = { version = "*", python = ">3.8" }
|
|
64
|
+
furo = { version = "^2024.5.6", python = ">3.8" }
|
|
65
|
+
sphinx-copybutton = { version = "^0.5.2", python = ">3.8" }
|
|
65
66
|
|
|
66
67
|
[build-system]
|
|
67
68
|
requires = ["poetry-core"]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from bisect import insort
|
|
3
|
-
from collections import Counter
|
|
4
3
|
from collections import defaultdict
|
|
5
4
|
from collections import deque
|
|
6
5
|
from enum import IntEnum
|
|
6
|
+
from enum import IntFlag
|
|
7
7
|
from enum import auto
|
|
8
8
|
from inspect import isawaitable
|
|
9
9
|
from inspect import iscoroutinefunction
|
|
@@ -27,10 +27,14 @@ class CallbackPriority(IntEnum):
|
|
|
27
27
|
AFTER = 40
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
class SpecReference(
|
|
31
|
-
NAME =
|
|
32
|
-
CALLABLE =
|
|
33
|
-
PROPERTY =
|
|
30
|
+
class SpecReference(IntFlag):
|
|
31
|
+
NAME = auto()
|
|
32
|
+
CALLABLE = auto()
|
|
33
|
+
PROPERTY = auto()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
SPECS_ALL = SpecReference.NAME | SpecReference.CALLABLE | SpecReference.PROPERTY
|
|
37
|
+
SPECS_SAFE = SpecReference.NAME
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
class CallbackGroup(IntEnum):
|
|
@@ -335,7 +339,7 @@ class CallbacksExecutor:
|
|
|
335
339
|
class CallbacksRegistry:
|
|
336
340
|
def __init__(self) -> None:
|
|
337
341
|
self._registry: Dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
|
|
338
|
-
self.
|
|
342
|
+
self.has_async_callbacks: bool = False
|
|
339
343
|
|
|
340
344
|
def clear(self):
|
|
341
345
|
self._registry.clear()
|
|
@@ -357,6 +361,6 @@ class CallbacksRegistry:
|
|
|
357
361
|
)
|
|
358
362
|
|
|
359
363
|
def async_or_sync(self):
|
|
360
|
-
self.
|
|
364
|
+
self.has_async_callbacks = any(
|
|
361
365
|
callback._iscoro for executor in self._registry.values() for callback in executor
|
|
362
366
|
)
|
|
@@ -8,8 +8,8 @@ from typing import Iterable
|
|
|
8
8
|
from typing import Set
|
|
9
9
|
from typing import Tuple
|
|
10
10
|
|
|
11
|
-
from
|
|
12
|
-
|
|
11
|
+
from .callbacks import SPECS_ALL
|
|
12
|
+
from .callbacks import SpecReference
|
|
13
13
|
from .signature import SignatureAdapter
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
@@ -54,10 +54,18 @@ class Listeners:
|
|
|
54
54
|
all_attrs = set().union(*(listener.all_attrs for listener in listeners))
|
|
55
55
|
return cls(listeners, all_attrs)
|
|
56
56
|
|
|
57
|
-
def resolve(
|
|
57
|
+
def resolve(
|
|
58
|
+
self,
|
|
59
|
+
specs: "CallbackSpecList",
|
|
60
|
+
registry,
|
|
61
|
+
allowed_references: SpecReference = SPECS_ALL,
|
|
62
|
+
):
|
|
58
63
|
found_convention_specs = specs.conventional_specs & self.all_attrs
|
|
59
64
|
filtered_specs = [
|
|
60
|
-
spec
|
|
65
|
+
spec
|
|
66
|
+
for spec in specs
|
|
67
|
+
if spec.reference in allowed_references
|
|
68
|
+
and (not spec.is_convention or spec.func in found_convention_specs)
|
|
61
69
|
]
|
|
62
70
|
if not filtered_specs:
|
|
63
71
|
return
|
|
@@ -101,7 +109,7 @@ class Listeners:
|
|
|
101
109
|
if func is not None and func.__func__ is spec.func:
|
|
102
110
|
return callable_method(spec.attr_name, func, config.resolver_id)
|
|
103
111
|
|
|
104
|
-
return callable_method(spec.
|
|
112
|
+
return callable_method(spec.attr_name, spec.func, None)
|
|
105
113
|
|
|
106
114
|
def _search_name(self, name) -> Generator["Callable", None, None]:
|
|
107
115
|
for config in self.items:
|
|
@@ -31,9 +31,9 @@ class AsyncEngine:
|
|
|
31
31
|
Given how async works on python, there's no built-in way to activate the initial state that
|
|
32
32
|
may depend on async code from the StateMachine.__init__ method.
|
|
33
33
|
"""
|
|
34
|
-
return await self.
|
|
34
|
+
return await self.processing_loop()
|
|
35
35
|
|
|
36
|
-
async def
|
|
36
|
+
async def processing_loop(self):
|
|
37
37
|
"""Process event triggers.
|
|
38
38
|
|
|
39
39
|
The simplest implementation is the non-RTC (synchronous),
|
|
@@ -29,9 +29,9 @@ class SyncEngine:
|
|
|
29
29
|
Given how async works on python, there's no built-in way to activate the initial state that
|
|
30
30
|
may depend on async code from the StateMachine.__init__ method.
|
|
31
31
|
"""
|
|
32
|
-
return self.
|
|
32
|
+
return self.processing_loop()
|
|
33
33
|
|
|
34
|
-
def
|
|
34
|
+
def processing_loop(self):
|
|
35
35
|
"""Process event triggers.
|
|
36
36
|
|
|
37
37
|
The simplest implementation is the non-RTC (synchronous),
|
|
@@ -9,11 +9,11 @@ from typing import Any
|
|
|
9
9
|
from typing import Dict
|
|
10
10
|
from typing import List
|
|
11
11
|
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
|
|
12
|
+
from .callbacks import SPECS_ALL
|
|
13
|
+
from .callbacks import SPECS_SAFE
|
|
15
14
|
from .callbacks import CallbacksExecutor
|
|
16
15
|
from .callbacks import CallbacksRegistry
|
|
16
|
+
from .callbacks import SpecReference
|
|
17
17
|
from .dispatcher import Listener
|
|
18
18
|
from .dispatcher import Listeners
|
|
19
19
|
from .engines.async_ import AsyncEngine
|
|
@@ -24,8 +24,10 @@ from .exceptions import InvalidDefinition
|
|
|
24
24
|
from .exceptions import InvalidStateValue
|
|
25
25
|
from .exceptions import TransitionNotAllowed
|
|
26
26
|
from .factory import StateMachineMetaclass
|
|
27
|
+
from .graph import iterate_states_and_transitions
|
|
27
28
|
from .i18n import _
|
|
28
29
|
from .model import Model
|
|
30
|
+
from .utils import run_async_from_sync
|
|
29
31
|
|
|
30
32
|
if TYPE_CHECKING:
|
|
31
33
|
from .state import State
|
|
@@ -106,7 +108,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
106
108
|
self._engine = self._get_engine(rtc)
|
|
107
109
|
|
|
108
110
|
def _get_engine(self, rtc: bool):
|
|
109
|
-
if self._callbacks_registry.
|
|
111
|
+
if self._callbacks_registry.has_async_callbacks:
|
|
110
112
|
return AsyncEngine(self, rtc=rtc)
|
|
111
113
|
else:
|
|
112
114
|
return SyncEngine(self, rtc=rtc)
|
|
@@ -118,7 +120,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
118
120
|
return run_async_from_sync(result)
|
|
119
121
|
|
|
120
122
|
def _processing_loop(self):
|
|
121
|
-
return self._engine.
|
|
123
|
+
return self._engine.processing_loop()
|
|
122
124
|
|
|
123
125
|
def __init_subclass__(cls, strict_states: bool = False):
|
|
124
126
|
cls._strict_states = strict_states
|
|
@@ -177,8 +179,12 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
177
179
|
continue
|
|
178
180
|
setattr(target, event.name, trigger)
|
|
179
181
|
|
|
180
|
-
def _add_listener(self, listeners: "Listeners"):
|
|
181
|
-
register = partial(
|
|
182
|
+
def _add_listener(self, listeners: "Listeners", allowed_references: SpecReference = SPECS_ALL):
|
|
183
|
+
register = partial(
|
|
184
|
+
listeners.resolve,
|
|
185
|
+
registry=self._callbacks_registry,
|
|
186
|
+
allowed_references=allowed_references,
|
|
187
|
+
)
|
|
182
188
|
for visited in iterate_states_and_transitions(self.states):
|
|
183
189
|
register(visited._specs)
|
|
184
190
|
|
|
@@ -210,7 +216,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
210
216
|
def add_observer(self, *observers):
|
|
211
217
|
"""Add a listener."""
|
|
212
218
|
warnings.warn(
|
|
213
|
-
"""
|
|
219
|
+
"""Method `add_observer` has been renamed to `add_listener`.""",
|
|
214
220
|
DeprecationWarning,
|
|
215
221
|
stacklevel=2,
|
|
216
222
|
)
|
|
@@ -228,7 +234,8 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
228
234
|
"""
|
|
229
235
|
self._listeners.update({o: None for o in listeners})
|
|
230
236
|
return self._add_listener(
|
|
231
|
-
Listeners.from_listeners(Listener.from_obj(o) for o in listeners)
|
|
237
|
+
Listeners.from_listeners(Listener.from_obj(o) for o in listeners),
|
|
238
|
+
allowed_references=SPECS_SAFE,
|
|
232
239
|
)
|
|
233
240
|
|
|
234
241
|
def _repr_html_(self):
|
|
@@ -303,9 +310,6 @@ class StateMachine(metaclass=StateMachineMetaclass):
|
|
|
303
310
|
def send(self, event: str, *args, **kwargs):
|
|
304
311
|
"""Send an :ref:`Event` to the state machine.
|
|
305
312
|
|
|
306
|
-
This is a thin wrapper around :meth:`async_send` to allow synchronous
|
|
307
|
-
code to send events.
|
|
308
|
-
|
|
309
313
|
.. seealso::
|
|
310
314
|
|
|
311
315
|
See: :ref:`triggering events`.
|
|
@@ -54,7 +54,6 @@ class States:
|
|
|
54
54
|
return list(self) == list(other)
|
|
55
55
|
|
|
56
56
|
def __getattr__(self, name: str):
|
|
57
|
-
name = name.lower()
|
|
58
57
|
if name in self._states:
|
|
59
58
|
return self._states[name]
|
|
60
59
|
raise AttributeError(f"{name} not found in {self.__class__.__name__}")
|
|
@@ -81,7 +80,7 @@ class States:
|
|
|
81
80
|
return self._states.items()
|
|
82
81
|
|
|
83
82
|
@classmethod
|
|
84
|
-
def from_enum(cls, enum_type: EnumType, initial, final=None):
|
|
83
|
+
def from_enum(cls, enum_type: EnumType, initial, final=None, use_enum_instance: bool = False):
|
|
85
84
|
"""
|
|
86
85
|
Creates a new instance of the ``States`` class from an enumeration.
|
|
87
86
|
|
|
@@ -103,12 +102,13 @@ class States:
|
|
|
103
102
|
... def on_enter_completed(self):
|
|
104
103
|
... print("Completed!")
|
|
105
104
|
|
|
106
|
-
..
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
.. tip::
|
|
106
|
+
When you assign the result of ``States.from_enum`` to a class-level variable in your
|
|
107
|
+
:ref:`StateMachine`, you're all set. You can use any name for this variable. In this
|
|
108
|
+
example, we used ``_`` to show that the name doesn't matter. The metaclass will inspect
|
|
109
|
+
the variable of type :ref:`States (class)` and automatically assign the inner
|
|
110
|
+
:ref:`State` instances to the state machine.
|
|
111
|
+
|
|
112
112
|
|
|
113
113
|
Everything else is similar, the ``Enum`` is only used to declare the :ref:`State`
|
|
114
114
|
instances.
|
|
@@ -125,12 +125,25 @@ class States:
|
|
|
125
125
|
True
|
|
126
126
|
|
|
127
127
|
>>> sm.current_state_value
|
|
128
|
+
2
|
|
129
|
+
|
|
130
|
+
If you need to use the enum instance as the state value, you can set the
|
|
131
|
+
``use_enum_instance=True``:
|
|
132
|
+
|
|
133
|
+
>>> states = States.from_enum(Status, initial=Status.pending, use_enum_instance=True)
|
|
134
|
+
>>> states.completed.value
|
|
128
135
|
<Status.completed: 2>
|
|
129
136
|
|
|
137
|
+
.. deprecated:: 2.3.3
|
|
138
|
+
|
|
139
|
+
On the next major release, the ``use_enum_instance=True`` will be the default.
|
|
140
|
+
|
|
130
141
|
Args:
|
|
131
142
|
enum_type: An enumeration containing the states of the machine.
|
|
132
143
|
initial: The initial state of the machine.
|
|
133
144
|
final: A set of final states of the machine.
|
|
145
|
+
use_enum_instance: If ``True``, the value of the state will be the enum item instance,
|
|
146
|
+
otherwise the enum item value.
|
|
134
147
|
|
|
135
148
|
Returns:
|
|
136
149
|
A new instance of the :ref:`States (class)`.
|
|
@@ -138,7 +151,11 @@ class States:
|
|
|
138
151
|
final_set = set(ensure_iterable(final))
|
|
139
152
|
return cls(
|
|
140
153
|
{
|
|
141
|
-
e.name
|
|
154
|
+
e.name: State(
|
|
155
|
+
value=(e if use_enum_instance else e.value),
|
|
156
|
+
initial=e is initial,
|
|
157
|
+
final=e in final_set,
|
|
158
|
+
)
|
|
142
159
|
for e in enum_type
|
|
143
160
|
}
|
|
144
161
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|