Skip to content

Commit 66b0efb

Browse files
committed
Merge branch 'release/2.3.3'
2 parents e48bde7 + 3afc6b6 commit 66b0efb

11 files changed

Lines changed: 73 additions & 37 deletions

File tree

docs/releases/2.3.3.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# StateMachine 2.3.3
2+
3+
*July 3, 2024*
4+
5+
6+
## Bugfixes in 2.3.3
7+
8+
- Fixes [#457](https://github.com/fgmacedo/python-statemachine/issues/457) regression that caused backwards incomplatible changes when using Enums. Thanks [@hperrey](https://github.com/hperrey)!
9+
10+
11+
12+
## Deprecation notes in 2.3.3
13+
14+
Deprecations that will be removed on the next major release:
15+
16+
- The `States.from_enum(..., use_enum_instance=True)` will be the default.
17+
18+
```{seealso}
19+
See {ref}`States from Enum types` for more details.
20+
```

docs/releases/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Below are release notes through StateMachine and its patch releases.
1515
```{toctree}
1616
:maxdepth: 2
1717
18+
2.3.3
1819
2.3.2
1920
2.3.1
2021
2.3.0

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-statemachine"
3-
version = "2.3.2"
3+
version = "2.3.3"
44
description = "Python Finite State Machines made easy."
55
authors = ["Fernando Macedo <fgmacedo@gmail.com>"]
66
maintainers = [

statemachine/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33

44
__author__ = """Fernando Macedo"""
55
__email__ = "fgmacedo@gmail.com"
6-
__version__ = "2.3.2"
6+
__version__ = "2.3.3"
77

88
__all__ = ["StateMachine", "State"]

statemachine/callbacks.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
from bisect import insort
3-
from collections import Counter
43
from collections import defaultdict
54
from collections import deque
65
from enum import IntEnum
@@ -335,7 +334,7 @@ def all(self, *args, **kwargs):
335334
class CallbacksRegistry:
336335
def __init__(self) -> None:
337336
self._registry: Dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
338-
self._method_types: Counter = Counter()
337+
self.has_async_callbacks: bool = False
339338

340339
def clear(self):
341340
self._registry.clear()
@@ -357,6 +356,6 @@ def check(self, specs: CallbackSpecList):
357356
)
358357

359358
def async_or_sync(self):
360-
self._method_types.update(
359+
self.has_async_callbacks = any(
361360
callback._iscoro for executor in self._registry.values() for callback in executor
362361
)

statemachine/engines/async_.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ async def activate_initial_state(self):
3131
Given how async works on python, there's no built-in way to activate the initial state that
3232
may depend on async code from the StateMachine.__init__ method.
3333
"""
34-
return await self._processing_loop()
34+
return await self.processing_loop()
3535

36-
async def _processing_loop(self):
36+
async def processing_loop(self):
3737
"""Process event triggers.
3838
3939
The simplest implementation is the non-RTC (synchronous),

statemachine/engines/sync.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ def activate_initial_state(self):
2929
Given how async works on python, there's no built-in way to activate the initial state that
3030
may depend on async code from the StateMachine.__init__ method.
3131
"""
32-
return self._processing_loop()
32+
return self.processing_loop()
3333

34-
def _processing_loop(self):
34+
def processing_loop(self):
3535
"""Process event triggers.
3636
3737
The simplest implementation is the non-RTC (synchronous),

statemachine/statemachine.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def __init__(
106106
self._engine = self._get_engine(rtc)
107107

108108
def _get_engine(self, rtc: bool):
109-
if self._callbacks_registry._method_types[True] > 0:
109+
if self._callbacks_registry.has_async_callbacks:
110110
return AsyncEngine(self, rtc=rtc)
111111
else:
112112
return SyncEngine(self, rtc=rtc)
@@ -118,7 +118,7 @@ def activate_initial_state(self):
118118
return run_async_from_sync(result)
119119

120120
def _processing_loop(self):
121-
return self._engine._processing_loop()
121+
return self._engine.processing_loop()
122122

123123
def __init_subclass__(cls, strict_states: bool = False):
124124
cls._strict_states = strict_states
@@ -303,9 +303,6 @@ def _put_nonblocking(self, trigger_data: TriggerData):
303303
def send(self, event: str, *args, **kwargs):
304304
"""Send an :ref:`Event` to the state machine.
305305
306-
This is a thin wrapper around :meth:`async_send` to allow synchronous
307-
code to send events.
308-
309306
.. seealso::
310307
311308
See: :ref:`triggering events`.

statemachine/states.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ def __eq__(self, other):
5454
return list(self) == list(other)
5555

5656
def __getattr__(self, name: str):
57-
name = name.lower()
5857
if name in self._states:
5958
return self._states[name]
6059
raise AttributeError(f"{name} not found in {self.__class__.__name__}")
@@ -81,7 +80,7 @@ def items(self):
8180
return self._states.items()
8281

8382
@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):
8584
"""
8685
Creates a new instance of the ``States`` class from an enumeration.
8786
@@ -125,20 +124,37 @@ def from_enum(cls, enum_type: EnumType, initial, final=None):
125124
True
126125
127126
>>> sm.current_state_value
127+
2
128+
129+
If you need to use the enum instance as the state value, you can set the
130+
``use_enum_instance=True``:
131+
132+
>>> states = States.from_enum(Status, initial=Status.pending, use_enum_instance=True)
133+
>>> states.completed.value
128134
<Status.completed: 2>
129135
136+
.. deprecated:: 2.3.3
137+
138+
On the next major release, the ``use_enum_instance=True`` will be the default.
139+
130140
Args:
131141
enum_type: An enumeration containing the states of the machine.
132142
initial: The initial state of the machine.
133143
final: A set of final states of the machine.
144+
use_enum_instance: If ``True``, the value of the state will be the enum item instance,
145+
otherwise the enum item value.
134146
135147
Returns:
136148
A new instance of the :ref:`States (class)`.
137149
"""
138150
final_set = set(ensure_iterable(final))
139151
return cls(
140152
{
141-
e.name.lower(): State(value=e, initial=e is initial, final=e in final_set)
153+
e.name: State(
154+
value=(e if use_enum_instance else e.value),
155+
initial=e is initial,
156+
final=e in final_set,
157+
)
142158
for e in enum_type
143159
}
144160
)

tests/django_project/workflow/statemachines.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
class WorfklowStateMachine(StateMachine):
88
_ = States.from_enum(WorkflowSteps, initial=WorkflowSteps.DRAFT, final=WorkflowSteps.PUBLISHED)
99

10-
publish = _.draft.to(_.published, cond="is_active")
11-
notify_user = _.draft.to.itself(internal=True, cond="has_user")
10+
publish = _.DRAFT.to(_.PUBLISHED, cond="is_active")
11+
notify_user = _.DRAFT.to.itself(internal=True, cond="has_user")
1212

1313
def has_user(self):
1414
return bool(self.model.user)

0 commit comments

Comments
 (0)