Skip to content

Commit a74defb

Browse files
authored
feat: Support abstract SM classes. Closes #350 (#353)
1 parent 0a54b24 commit a74defb

4 files changed

Lines changed: 26 additions & 12 deletions

File tree

docs/releases/2.0.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ See {ref}`internal transition` for more details.
4747
- [#342](https://github.com/fgmacedo/python-statemachine/pull/342): Assignment of `Transition`
4848
guards using decorators is now possible.
4949
- [#331](https://github.com/fgmacedo/python-statemachine/pull/331): Added a way to generate diagrams using [QuickChart.io](https://quickchart.io) instead of GraphViz. See {ref}`diagrams` for more details.
50+
- [#353](https://github.com/fgmacedo/python-statemachine/pull/353): Support for abstract state machine classes, so you can subclass `StateMachine` to add behavior on your own base class. Abstract `StateMachine` cannot be instantiated.
5051

5152
## Bugfixes in 2.0
5253

statemachine/factory.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from .state import State
99
from .transition import Transition
1010
from .transition_list import TransitionList
11-
from .utils import qualname
1211
from .utils import ugettext as _
1312

1413

@@ -57,18 +56,19 @@ def _check_disconnected_state(cls):
5756
)
5857

5958
def _check(cls):
59+
has_states = bool(cls.states)
60+
has_events = bool(cls._events)
6061

61-
# do not validate the base class
62-
name = qualname(cls)
63-
if name == "statemachine.statemachine.StateMachine":
64-
return
62+
cls._abstract = not has_states and not has_events
6563

66-
cls._abstract = False
64+
# do not validate the base abstract classes
65+
if cls._abstract:
66+
return
6767

68-
if not cls.states:
68+
if not has_states:
6969
raise InvalidDefinition(_("There are no states."))
7070

71-
if not cls._events:
71+
if not has_events:
7272
raise InvalidDefinition(_("There are no events."))
7373

7474
cls._check_disconnected_state()

statemachine/statemachine.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from .event import Event
66
from .event_data import EventData
77
from .exceptions import InvalidStateValue
8+
from .exceptions import InvalidDefinition
89
from .exceptions import TransitionNotAllowed
910
from .factory import StateMachineMetaclass
1011
from .model import Model
1112
from .transition import Transition
13+
from .utils import ugettext as _
1214

1315

1416
if TYPE_CHECKING:
@@ -28,6 +30,9 @@ def __init__(self, model=None, state_field="state", start_value=None):
2830
self.state_field = state_field
2931
self.start_value = start_value
3032

33+
if self._abstract:
34+
raise InvalidDefinition(_("There are no states or transitions."))
35+
3136
initial_transition = Transition(
3237
None, self._get_initial_state(), event="__initial__"
3338
)

tests/test_statemachine.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,21 @@ def test_state_machine_with_a_invalid_start_value(
296296
machine_cls(model, start_value=start_value)
297297

298298

299-
def test_should_not_create_instance_of_machine_without_states():
299+
def test_should_not_create_instance_of_abstract_machine():
300+
class EmptyMachine(StateMachine):
301+
"An empty machine"
302+
pass
303+
304+
with pytest.raises(exceptions.InvalidDefinition):
305+
EmptyMachine()
300306

307+
308+
def test_should_not_create_instance_of_machine_without_states():
309+
s1 = State("X")
301310
with pytest.raises(exceptions.InvalidDefinition):
302311

303-
class EmptyMachine(StateMachine):
304-
"An empty machine"
305-
pass
312+
class OnlyTransitionMachine(StateMachine):
313+
t1 = s1.to.itself()
306314

307315

308316
def test_should_not_create_instance_of_machine_without_transitions():

0 commit comments

Comments
 (0)