Skip to content

Commit 0a15aa7

Browse files
authored
feat: Support to declare States using Enum. Closes #367 (#375)
1 parent 2420b18 commit 0a15aa7

17 files changed

Lines changed: 720 additions & 326 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@ repos:
99
exclude: docs/auto_examples
1010
- repo: https://github.com/charliermarsh/ruff-pre-commit
1111
# Ruff version.
12-
rev: 'v0.0.220'
12+
rev: 'v0.0.257'
1313
hooks:
1414
- id: ruff
15-
# Respect `exclude` and `extend-exclude` settings.
16-
args: ["--force-exclude"]
1715
- repo: https://github.com/psf/black
1816
rev: 22.10.0
1917
hooks:

docs/api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
:members:
2121
```
2222

23+
## States (class)
24+
25+
```{eval-rst}
26+
.. autoclass:: statemachine.states.States
27+
:noindex:
28+
:members:
29+
```
30+
2331
## Transition
2432

2533
```{seealso}

docs/releases/2.0.1.md

Lines changed: 0 additions & 12 deletions
This file was deleted.

docs/releases/2.1.0.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# StateMachine 2.1.0
2+
3+
*Not released yet*
4+
5+
## What's new in 2.1
6+
7+
### Added support for declaring states using Enum
8+
9+
Given an ``Enum`` type that declares our expected states:
10+
11+
```py
12+
>>> from enum import Enum
13+
14+
>>> class Status(Enum):
15+
... pending = 1
16+
... completed = 2
17+
18+
```
19+
20+
A {ref}`StateMachine` can be declared as follows:
21+
22+
```py
23+
>>> from statemachine import StateMachine
24+
>>> from statemachine.states import States
25+
26+
>>> class ApprovalMachine(StateMachine):
27+
...
28+
... _ = States.from_enum(Status, initial=Status.pending, final=Status.completed)
29+
...
30+
... finish = _.pending.to(_.completed)
31+
...
32+
... def on_enter_completed(self):
33+
... print("Completed!")
34+
35+
```
36+
37+
See {ref}`States from Enum types`.
38+
39+
## Bugfixes
40+
41+
- Fixes [#369](https://github.com/fgmacedo/python-statemachine/issues/369) adding support to wrap
42+
methods used as {ref}`Actions` decorated with `functools.partial`.

docs/releases/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Below are release notes through StateMachine and its patch releases.
1515
```{toctree}
1616
:maxdepth: 1
1717
18-
2.0.1
18+
2.1.0
1919
2.0.0
2020
2121
```

docs/states.md

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

44
{ref}`State`, as the name says, holds the representation of a state in a {ref}`StateMachine`.
55

6+
```{eval-rst}
7+
.. autoclass:: statemachine.state.State
8+
:noindex:
9+
```
10+
11+
```{seealso}
12+
How to define and attach [](actions.md) to {ref}`States`.
13+
```
14+
615

716
## Initial state
817

@@ -58,6 +67,18 @@ You can retrieve all final states.
5867

5968
```
6069

70+
## States from Enum types
71+
72+
{ref}`States` can also be declared from standard `Enum` classes.
73+
74+
For this, use {ref}`States (class)` to convert your `Enum` type to a list of {ref}`State` objects.
75+
76+
77+
```{eval-rst}
78+
.. automethod:: statemachine.states.States.from_enum
79+
:noindex:
80+
```
81+
6182
```{seealso}
62-
How to define and attach [](actions.md) to {ref}`States`.
83+
See the example {ref}`sphx_glr_auto_examples_enum_campaign_machine.py`.
6384
```

poetry.lock

Lines changed: 263 additions & 275 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pytest = "^7.2.0"
4040
pytest-cov = "^4.0.0"
4141
pytest-sugar = "^0.9.6"
4242
pydot = "^1.4.2"
43-
ruff = "^0.0.220"
43+
ruff = "^0.0.257"
4444
pre-commit = "^2.21.0"
4545
mypy = "^0.991"
4646
black = "^22.12.0"
@@ -100,6 +100,8 @@ select = [
100100
]
101101
ignore = [
102102
"UP006", # `use-pep585-annotation` Requires Python3.9+
103+
"UP035", # `use-pep585-annotation` Requires Python3.9+
104+
"UP038", # `use-pep585-annotation` Requires Python3.9+
103105
]
104106

105107
line-length = 99

statemachine/factory.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from typing import TYPE_CHECKING
2+
from typing import Any
3+
from typing import Dict
4+
from typing import Tuple
15
from uuid import uuid4
26

37
from . import registry
@@ -7,25 +11,35 @@
711
from .graph import visit_connected_states
812
from .i18n import _
913
from .state import State
14+
from .states import States
1015
from .transition import Transition
1116
from .transition_list import TransitionList
1217

1318

1419
class StateMachineMetaclass(type):
15-
def __init__(cls, name, bases, attrs):
20+
def __init__(cls, name: str, bases: Tuple[type], attrs: Dict[str, Any]):
1621
super().__init__(name, bases, attrs)
1722
registry.register(cls)
18-
cls._abstract = True
1923
cls.name = cls.__name__
20-
cls.states = []
21-
cls._events = {}
22-
cls.states_map = {}
24+
cls.states: States = States()
25+
cls.states_map: Dict[Any, State] = {}
26+
"""Map of ``state.value`` to the corresponding :ref:`state`."""
27+
28+
cls._abstract = True
29+
cls._events: Dict[str, Event] = {}
30+
2331
cls.add_inherited(bases)
2432
cls.add_from_attributes(attrs)
2533

2634
cls._set_special_states()
2735
cls._check()
2836

37+
if TYPE_CHECKING:
38+
"""Makes mypy happy with dynamic created attributes"""
39+
40+
def __getattr__(self, attribute: str) -> Any:
41+
...
42+
2943
def _set_special_states(cls):
3044
if not cls.states:
3145
return
@@ -95,13 +109,19 @@ def add_inherited(cls, bases):
95109

96110
def add_from_attributes(cls, attrs):
97111
for key, value in sorted(attrs.items(), key=lambda pair: pair[0]):
112+
if isinstance(value, States):
113+
cls._add_states_from_dict(value)
98114
if isinstance(value, State):
99115
cls.add_state(key, value)
100116
elif isinstance(value, (Transition, TransitionList)):
101117
cls.add_event(key, value)
102118
elif getattr(value, "_callbacks_to_update", None):
103119
cls._add_unbounded_callback(key, value)
104120

121+
def _add_states_from_dict(cls, states):
122+
for state_id, state in states.items():
123+
cls.add_state(state_id, state)
124+
105125
def _add_unbounded_callback(cls, attr_name, func):
106126
if func._is_event:
107127
# if func is an event, the `attr_name` will be replaced by an event trigger,
@@ -118,6 +138,8 @@ def add_state(cls, id, state: State):
118138
state._set_id(id)
119139
cls.states.append(state)
120140
cls.states_map[state.value] = state
141+
if not hasattr(cls, id):
142+
setattr(cls, id, state)
121143

122144
# also register all events associated directly with transitions
123145
for event in state.transitions.unique_events:

statemachine/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
class Model:
22
def __init__(self):
33
self.state = None
4+
"""Holds the current :ref:`state` value of the :ref:`StateMachine`."""
45

56
def __repr__(self):
67
return f"Model(state={self.state})"

0 commit comments

Comments
 (0)