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.
Files changed (31) hide show
  1. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/PKG-INFO +1 -1
  2. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/pyproject.toml +8 -7
  3. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/__init__.py +1 -1
  4. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/callbacks.py +11 -7
  5. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/dispatcher.py +13 -5
  6. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/engines/async_.py +2 -2
  7. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/engines/sync.py +2 -2
  8. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/statemachine.py +16 -12
  9. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/states.py +26 -9
  10. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/LICENSE +0 -0
  11. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/README.md +0 -0
  12. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/contrib/__init__.py +0 -0
  13. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/contrib/diagram.py +0 -0
  14. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/engines/__init__.py +0 -0
  15. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/event.py +0 -0
  16. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/event_data.py +0 -0
  17. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/events.py +0 -0
  18. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/exceptions.py +0 -0
  19. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/factory.py +0 -0
  20. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/graph.py +0 -0
  21. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/i18n.py +0 -0
  22. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/locale/en/LC_MESSAGES/statemachine.po +0 -0
  23. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +0 -0
  24. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/mixins.py +0 -0
  25. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/model.py +0 -0
  26. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/registry.py +0 -0
  27. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/signature.py +0 -0
  28. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/state.py +0 -0
  29. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/transition.py +0 -0
  30. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/transition_list.py +0 -0
  31. {python_statemachine-2.3.2 → python_statemachine-2.3.4}/statemachine/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-statemachine
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Summary: Python Finite State Machines made easy.
5
5
  Home-page: https://github.com/fgmacedo/python-statemachine
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-statemachine"
3
- version = "2.3.2"
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
- sphinx-rtd-theme = "2.0.0"
61
- myst-parser = "*"
62
- sphinx-gallery = "*"
63
- pillow = "*"
64
- sphinx-autobuild = "*"
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"]
@@ -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.2"
6
+ __version__ = "2.3.4"
7
7
 
8
8
  __all__ = ["StateMachine", "State"]
@@ -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(IntEnum):
31
- NAME = 1
32
- CALLABLE = 2
33
- PROPERTY = 3
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._method_types: Counter = Counter()
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._method_types.update(
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 statemachine.callbacks import SpecReference
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(self, specs: "CallbackSpecList", registry):
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 for spec in specs if not spec.is_convention or spec.func in found_convention_specs
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.func, spec.func, None)
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._processing_loop()
34
+ return await self.processing_loop()
35
35
 
36
- async def _processing_loop(self):
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._processing_loop()
32
+ return self.processing_loop()
33
33
 
34
- def _processing_loop(self):
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 statemachine.graph import iterate_states_and_transitions
13
- from statemachine.utils import run_async_from_sync
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._method_types[True] > 0:
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._processing_loop()
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(listeners.resolve, registry=self._callbacks_registry)
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
- """The `add_observer` was rebranded to `add_listener`.""",
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
- .. note::
107
- Given that you assign the response of ``States.from_enum`` to a class level
108
- variable on your :ref:`StateMachine` you're good to go, you can use any name to assign
109
- this response, on this example we used ``_`` to indicate that the name does not matter.
110
- The variable of type :ref:`States (class)` will be inspected by the metaclass and the
111
- inner :ref:`State` instances assigned to the state machine.
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.lower(): State(value=e, initial=e is initial, final=e in final_set)
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
  )