From d6c33e4c96c8dae8beef8fb666fdf402ce45551b Mon Sep 17 00:00:00 2001 From: Fernando Macedo Date: Fri, 6 Dec 2024 15:11:48 -0300 Subject: [PATCH] chore: Remove support for non-RTC model; Preparing processing_loop for new version --- docs/processing_model.md | 68 ++-------------- statemachine/engines/async_.py | 18 +---- statemachine/engines/base.py | 59 ++++++++++---- statemachine/engines/sync.py | 20 +---- statemachine/io/scxml/actions.py | 2 - statemachine/orderedset.py | 105 +++++++++++++++++++++++++ statemachine/statemachine.py | 44 +++++++---- tests/conftest.py | 4 +- tests/test_rtc.py | 130 ++++++++++--------------------- tests/test_transitions.py | 4 +- 10 files changed, 233 insertions(+), 221 deletions(-) create mode 100644 statemachine/orderedset.py diff --git a/docs/processing_model.md b/docs/processing_model.md index c7d9b6b9..d699c1e4 100644 --- a/docs/processing_model.md +++ b/docs/processing_model.md @@ -10,19 +10,8 @@ In the literature, It's expected that all state-machine events should execute on The main point is: What should happen if the state machine triggers nested events while processing a parent event? -```{hint} -The importance of this decision depends on your state machine definition. Also the difference between RTC -and non-RTC processing models is more pronounced in a multi-threaded system than in a single-threaded system. -In other words, even if you run in {ref}`Non-RTC model`, only one external {ref}`event` will be -handled at a time and all internal events will run before the next external event is called, -so you only notice the difference if your state machine definition has nested event triggers while -processing these external events. -``` - -There are two distinct models for processing events in the library. The default is to run in -{ref}`RTC model` to be compliant with the specs, where the {ref}`event` is put on a -queue before processing. You can also configure your state machine to run in -{ref}`Non-RTC model`, where the {ref}`event` will be run immediately. +This library atheres to the {ref}`RTC model` to be compliant with the specs, where the {ref}`event` is put on a +queue before processing. Consider this state machine: @@ -60,13 +49,13 @@ Consider this state machine: In a run-to-completion (RTC) processing model (**default**), the state machine executes each event to completion before processing the next event. This means that the state machine completes all the actions associated with an event before moving on to the next event. This guarantees that the system is always in a consistent state. -If the machine is in `rtc` mode, the event is put on a queue. +Internally, the events are put on a queue before processing. ```{note} -While processing the queue items, if others events are generated, they will be processed sequentially. +While processing the queue items, if others events are generated, they will be processed sequentially in FIFO order. ``` -Running the above state machine will give these results on the RTC model: +Running the above state machine will give these results: ```py >>> sm = ServerConnection() @@ -89,50 +78,3 @@ after 'connection_succeed' from 'connecting' to 'connected' Note that the events `connect` and `connection_succeed` are executed sequentially, and the `connect.after` runs on the expected order. ``` -## Non-RTC model - -```{deprecated} 2.3.2 -`StateMachine.rtc` option is deprecated. We'll keep only the **run-to-completion** (RTC) model. -``` - -In contrast, in a non-RTC (synchronous) processing model, the state machine starts executing nested events -while processing a parent event. This means that when an event is triggered, the state machine -chains the processing when another event was triggered as a result of the first event. - -```{warning} -This can lead to complex and unpredictable behavior in the system if your state-machine definition triggers **nested -events**. -``` - -If your state machine does not trigger nested events while processing a parent event, -and you plan to use the API in an _imperative programming style_, you can consider using the synchronous mode (non-RTC). - -In this model, you can think of events as analogous to simple method calls. - -```{note} -While processing the {ref}`event`, if others events are generated, they will also be processed immediately, so a **nested** behavior happens. -``` - -Running the above state machine will give these results on the non-RTC (synchronous) model: - -```py ->>> sm = ServerConnection(rtc=False) -enter 'disconnected' from '' given '__initial__' - ->>> sm.send("connect") -exit 'disconnected' to 'connecting' given 'connect' -on 'connect' from 'disconnected' to 'connecting' -enter 'connecting' from 'disconnected' given 'connect' -exit 'connecting' to 'connected' given 'connection_succeed' -on 'connection_succeed' from 'connecting' to 'connected' -enter 'connected' from 'connecting' given 'connection_succeed' -after 'connection_succeed' from 'connecting' to 'connected' -after 'connect' from 'disconnected' to 'connecting' -['on_transition', 'on_connect'] - -``` - -```{note} -Note that the events `connect` and `connection_succeed` are nested, and the `connect.after` -unexpectedly only runs after `connection_succeed.after`. -``` diff --git a/statemachine/engines/async_.py b/statemachine/engines/async_.py index b147e260..75636378 100644 --- a/statemachine/engines/async_.py +++ b/statemachine/engines/async_.py @@ -4,22 +4,15 @@ from ..event_data import EventData from ..event_data import TriggerData -from ..exceptions import InvalidDefinition from ..exceptions import TransitionNotAllowed from ..i18n import _ from .base import BaseEngine if TYPE_CHECKING: - from ..statemachine import StateMachine from ..transition import Transition class AsyncEngine(BaseEngine): - def __init__(self, sm: "StateMachine", rtc: bool = True): - if not rtc: - raise InvalidDefinition(_("Only RTC is supported on async engine")) - super().__init__(sm=sm, rtc=rtc) - async def activate_initial_state(self): """ Activate the initial state. @@ -35,16 +28,7 @@ async def activate_initial_state(self): async def processing_loop(self): """Process event triggers. - The simplest implementation is the non-RTC (synchronous), - where the trigger will be run immediately and the result collected as the return. - - .. note:: - - While processing the trigger, if others events are generated, they - will also be processed immediately, so a "nested" behavior happens. - - If the machine is on ``rtc`` model (queued), the event is put on a queue, and only the - first event will have the result collected. + The event is put on a queue, and only the first event will have the result collected. .. note:: While processing the queue items, if others events are generated, they diff --git a/statemachine/engines/base.py b/statemachine/engines/base.py index b92719fd..39c1184c 100644 --- a/statemachine/engines/base.py +++ b/statemachine/engines/base.py @@ -1,10 +1,14 @@ +from itertools import chain from queue import PriorityQueue from queue import Queue from threading import Lock from typing import TYPE_CHECKING from weakref import proxy +from statemachine.orderedset import OrderedSet + from ..event import BoundEvent +from ..event_data import EventData from ..event_data import TriggerData from ..exceptions import TransitionNotAllowed from ..state import State @@ -14,43 +18,64 @@ from ..statemachine import StateMachine +class EventQueue: + def __init__(self): + self.queue: Queue = PriorityQueue() + + def empty(self): + return self.queue.qsize() == 0 + + def put(self, trigger_data: TriggerData): + """Put the trigger on the queue without blocking the caller.""" + self.queue.put(trigger_data) + + def pop(self): + """Pop a trigger from the queue without blocking the caller.""" + return self.queue.get(block=False) + + def clear(self): + with self.queue.mutex: + self.queue.queue.clear() + + def remove(self, send_id: str): + # We use the internal `queue` to make thins faster as the mutex + # is protecting the block below + with self.queue.mutex: + self.queue.queue = [ + trigger_data + for trigger_data in self.queue.queue + if trigger_data.send_id != send_id + ] + + class BaseEngine: - def __init__(self, sm: "StateMachine", rtc: bool = True): + def __init__(self, sm: "StateMachine"): self.sm: StateMachine = proxy(sm) - self._external_queue: Queue = PriorityQueue() + self.external_queue = EventQueue() + self.internal_queue = EventQueue() self._sentinel = object() - self._rtc = rtc self._running = True self._processing = Lock() def empty(self): - return self._external_queue.qsize() == 0 + return self.external_queue.empty() def put(self, trigger_data: TriggerData): """Put the trigger on the queue without blocking the caller.""" if not self._running and not self.sm.allow_event_without_transition: raise TransitionNotAllowed(trigger_data.event, self.sm.current_state) - self._external_queue.put(trigger_data) + self.external_queue.put(trigger_data) def pop(self): - return self._external_queue.get(block=False) + return self.external_queue.pop() def clear(self): - with self._external_queue.mutex: - self._external_queue.queue.clear() + self.external_queue.clear() def cancel_event(self, send_id: str): """Cancel the event with the given send_id.""" - - # We use the internal `queue` to make thins faster as the mutex - # is protecting the block below - with self._external_queue.mutex: - self._external_queue.queue = [ - trigger_data - for trigger_data in self._external_queue.queue - if trigger_data.send_id != send_id - ] + self.external_queue.remove(send_id) def start(self): if self.sm.current_state_value is not None: diff --git a/statemachine/engines/sync.py b/statemachine/engines/sync.py index 2e24426a..e1d9dfcf 100644 --- a/statemachine/engines/sync.py +++ b/statemachine/engines/sync.py @@ -2,6 +2,8 @@ from time import time from typing import TYPE_CHECKING +from statemachine.orderedset import OrderedSet + from ..event_data import EventData from ..event_data import TriggerData from ..exceptions import TransitionNotAllowed @@ -31,27 +33,13 @@ def activate_initial_state(self): def processing_loop(self): """Process event triggers. - The simplest implementation is the non-RTC (synchronous), - where the trigger will be run immediately and the result collected as the return. - - .. note:: - - While processing the trigger, if others events are generated, they - will also be processed immediately, so a "nested" behavior happens. - - If the machine is on ``rtc`` model (queued), the event is put on a queue, and only the - first event will have the result collected. + The event is put on a queue, and only the first event will have the result collected. .. note:: While processing the queue items, if others events are generated, they will be processed sequentially (and not nested). """ - if not self._rtc: - # The machine is in "synchronous" mode - trigger_data = self.pop() - return self._trigger(trigger_data) - # We make sure that only the first event enters the processing critical section, # next events will only be put on the queue and processed by the same loop. if not self._processing.acquire(blocking=False): @@ -127,7 +115,7 @@ def _activate(self, trigger_data: TriggerData, transition: "Transition"): # noq result += self.sm._callbacks.call(transition.on.key, *args, **kwargs) - self.sm.current_state = target + self.sm.configuration = OrderedSet([target]) event_data.state = target kwargs["state"] = target diff --git a/statemachine/io/scxml/actions.py b/statemachine/io/scxml/actions.py index c815f2db..bf17d0fe 100644 --- a/statemachine/io/scxml/actions.py +++ b/statemachine/io/scxml/actions.py @@ -417,7 +417,6 @@ def __init__( model: Any = None, state_field: str = "state", start_value: Any = None, - rtc: bool = True, allow_event_without_transition: bool = True, listeners: "List[object] | None" = None, ): @@ -431,7 +430,6 @@ def __init__( model, state_field=state_field, start_value=start_value, - rtc=rtc, allow_event_without_transition=allow_event_without_transition, listeners=listeners, ) diff --git a/statemachine/orderedset.py b/statemachine/orderedset.py new file mode 100644 index 00000000..4cb16524 --- /dev/null +++ b/statemachine/orderedset.py @@ -0,0 +1,105 @@ +import itertools +from typing import Iterable +from typing import Iterator +from typing import MutableSet +from typing import TypeVar + +T = TypeVar("T") + + +class OrderedSet(MutableSet[T]): + """A set that preserves insertion order by internally using a dict. + + >>> OrderedSet([1, 2, "foo"]) + OrderedSet([1, 2, 'foo']) + + + >>> OrderedSet([1, 2, 3, 3, 2, 1, 'a', 'b', 'a', 'c']) + OrderedSet([1, 2, 3, 'a', 'b', 'c']) + + >>> s = OrderedSet([1, 2, 3]) + >>> s.add(4) + >>> s + OrderedSet([1, 2, 3, 4]) + + >>> s = OrderedSet([1, 2, 3]) + >>> "foo" in s + False + + >>> 1 in s + True + + >>> list(s) + [1, 2, 3] + + >>> s == OrderedSet([1, 2, 3]) + True + + >>> s > OrderedSet([1, 2]) # set is a superset of other + True + + >>> s & {2} + OrderedSet([2]) + + >>> s | {4} + OrderedSet([1, 2, 3, 4]) + + >>> s - {2} + OrderedSet([1, 3]) + + >>> s - {1} + OrderedSet([2, 3]) + + >>> {1} - s + OrderedSet([]) + + >>> s ^ {2} + OrderedSet([1, 3]) + + >>> s[1] + 2 + + >>> s[2] + 3 + + >>> eval(repr(OrderedSet(['a', 'b', 'c']))) + OrderedSet(['a', 'b', 'c']) + + + + """ + + __slots__ = ("_d",) + + def __init__(self, iterable: Iterable[T] | None = None): + self._d = dict.fromkeys(iterable) if iterable else {} + + def add(self, x: T) -> None: + self._d[x] = None + + def clear(self) -> None: + self._d.clear() + + def discard(self, x: T) -> None: + self._d.pop(x, None) + + def __getitem__(self, index) -> T: + try: + return next(itertools.islice(self._d, index, index + 1)) + except StopIteration as e: + raise IndexError(f"index {index} out of range") from e + + def __contains__(self, x: object) -> bool: + return self._d.__contains__(x) + + def __len__(self) -> int: + return self._d.__len__() + + def __iter__(self) -> Iterator[T]: + return self._d.__iter__() + + def __str__(self): + return f"{{{', '.join(str(i) for i in self)}}}" + + def __repr__(self): + return f"OrderedSet({list(self._d.keys())})" diff --git a/statemachine/statemachine.py b/statemachine/statemachine.py index 2dc3b330..d1767495 100644 --- a/statemachine/statemachine.py +++ b/statemachine/statemachine.py @@ -4,6 +4,10 @@ from typing import Any from typing import Dict from typing import List +from typing import MutableSet +from typing import Set + +from statemachine.orderedset import OrderedSet from .callbacks import SPECS_ALL from .callbacks import SPECS_SAFE @@ -41,9 +45,6 @@ class StateMachine(metaclass=StateMachineMetaclass): start_value: An optional start state value if there's no current state assigned on the :ref:`domain models`. Default: ``None``. - rtc (bool): Controls the :ref:`processing model`. Defaults to ``True`` - that corresponds to a **run-to-completion** (RTC) model. - allow_event_without_transition: If ``False`` when an event does not result in a transition, an exception ``TransitionNotAllowed`` will be raised. If ``True`` the state machine allows triggering events that may not lead to a state @@ -71,7 +72,6 @@ def __init__( model: Any = None, state_field: str = "state", start_value: Any = None, - rtc: bool = True, allow_event_without_transition: bool = False, listeners: "List[object] | None" = None, ): @@ -81,7 +81,6 @@ def __init__( self.allow_event_without_transition = allow_event_without_transition self._callbacks = CallbacksRegistry() self._states_for_instance: Dict[State, State] = {} - self._listeners: Dict[int, Any] = {} """Listeners that provides attributes to be used as callbacks.""" @@ -93,14 +92,14 @@ def __init__( # Activate the initial state, this only works if the outer scope is sync code. # for async code, the user should manually call `await sm.activate_initial_state()` # after state machine creation. - self._engine = self._get_engine(rtc) + self._engine = self._get_engine() self._engine.start() - def _get_engine(self, rtc: bool): + def _get_engine(self): if self._callbacks.has_async_callbacks: - return AsyncEngine(self, rtc=rtc) + return AsyncEngine(self) - return SyncEngine(self, rtc=rtc) + return SyncEngine(self) def activate_initial_state(self): result = self._engine.activate_initial_state() @@ -132,7 +131,6 @@ def __repr__(self): def __getstate__(self): state = self.__dict__.copy() - state["_rtc"] = self._engine._rtc del state["_callbacks"] del state["_states_for_instance"] del state["_engine"] @@ -140,7 +138,6 @@ def __getstate__(self): def __setstate__(self, state): listeners = state.pop("_listeners") - rtc = state.pop("_rtc") self.__dict__.update(state) self._callbacks = CallbacksRegistry() self._states_for_instance: Dict[State, State] = {} @@ -149,7 +146,7 @@ def __setstate__(self, state): self._register_callbacks([]) self.add_listener(*listeners.values()) - self._engine = self._get_engine(rtc) + self._engine = self._get_engine() def _get_initial_state(self): initial_state_value = self.start_value if self.start_value else self.initial_state.value @@ -243,6 +240,25 @@ def _graph(self): return DotGraphMachine(self).get_graph() + @property + def configuration(self) -> OrderedSet["State"]: + if self.current_state_value is None: + return OrderedSet() + + if not isinstance(self.current_state_value, MutableSet): + return OrderedSet([self.states_map[self.current_state_value]]) + + return self.current_state_value + + @configuration.setter + def configuration(self, value: OrderedSet["State"]): + if len(value) == 0: + self.current_state_value = None + elif len(value) == 1: + self.current_state_value = value.pop().value + else: + self.current_state_value = value + @property def current_state_value(self): """Get/Set the current :ref:`state` value. @@ -254,12 +270,12 @@ def current_state_value(self): @current_state_value.setter def current_state_value(self, value): - if value not in self.states_map: + if not isinstance(value, MutableSet) and value not in self.states_map: raise InvalidStateValue(value) setattr(self.model, self.state_field, value) @property - def current_state(self) -> "State": + def current_state(self) -> "State | Set[State]": """Get/Set the current :ref:`state`. This is a low level API, that can be to assign any valid state diff --git a/tests/conftest.py b/tests/conftest.py index a720a808..8a304e12 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -135,8 +135,8 @@ class TrafficLightMachine(StateMachine): stop = yellow.to(red) go = red.to(green) - def _get_engine(self, rtc: bool): - return engine(self, rtc) + def _get_engine(self): + return engine(self) return TrafficLightMachine diff --git a/tests/test_rtc.py b/tests/test_rtc.py index 05515f9a..29a8a5ea 100644 --- a/tests/test_rtc.py +++ b/tests/test_rtc.py @@ -5,8 +5,6 @@ from statemachine import State from statemachine import StateMachine -from statemachine.exceptions import InvalidDefinition -from statemachine.exceptions import TransitionNotAllowed @pytest.fixture() @@ -58,9 +56,9 @@ class ChainedSM(StateMachine): t2b = s2.to(s3) t3 = s3.to(s4) - def __init__(self, rtc=True): + def __init__(self): self.spy = mock.Mock() - super().__init__(rtc=rtc) + super().__init__() def on_t1(self): return [self.t2a(), self.t2b(), self.send("t3")] @@ -88,46 +86,27 @@ def after_transition(self, event: str, source: State, target: State): class TestChainedTransition: @pytest.mark.parametrize( - ("rtc", "expected_calls"), + ("expected_calls"), [ - ( - False, - [ - mock.call("on_enter_state", state="a", source="", value=0), - mock.call("before_t1", source="a", value=42), - mock.call("on_exit_state", state="a", source="a", value=42), - mock.call("on_t1", source="a", value=42), - mock.call("on_enter_state", state="b", source="a", value=42), - mock.call("before_t1", source="b", value=42), - mock.call("on_exit_state", state="b", source="b", value=42), - mock.call("on_t1", source="b", value=42), - mock.call("on_enter_state", state="c", source="b", value=42), - mock.call("after_t1", source="b", value=42), - mock.call("after_t1", source="a", value=42), - ], - ), - ( - True, - [ - mock.call("on_enter_state", state="a", source="", value=0), - mock.call("before_t1", source="a", value=42), - mock.call("on_exit_state", state="a", source="a", value=42), - mock.call("on_t1", source="a", value=42), - mock.call("on_enter_state", state="b", source="a", value=42), - mock.call("after_t1", source="a", value=42), - mock.call("before_t1", source="b", value=42), - mock.call("on_exit_state", state="b", source="b", value=42), - mock.call("on_t1", source="b", value=42), - mock.call("on_enter_state", state="c", source="b", value=42), - mock.call("after_t1", source="b", value=42), - ], - ), + [ + mock.call("on_enter_state", state="a", source="", value=0), + mock.call("before_t1", source="a", value=42), + mock.call("on_exit_state", state="a", source="a", value=42), + mock.call("on_t1", source="a", value=42), + mock.call("on_enter_state", state="b", source="a", value=42), + mock.call("after_t1", source="a", value=42), + mock.call("before_t1", source="b", value=42), + mock.call("on_exit_state", state="b", source="b", value=42), + mock.call("on_t1", source="b", value=42), + mock.call("on_enter_state", state="c", source="b", value=42), + mock.call("after_t1", source="b", value=42), + ], ], ) def test_should_allow_chaining_transitions_using_actions( - self, chained_after_sm_class, rtc, expected_calls + self, chained_after_sm_class, expected_calls ): - sm = chained_after_sm_class(rtc=rtc) + sm = chained_after_sm_class() sm.t1(value=42) assert sm.c.is_active @@ -135,38 +114,31 @@ def test_should_allow_chaining_transitions_using_actions( assert sm.spy.call_args_list == expected_calls @pytest.mark.parametrize( - ("rtc", "expected"), + ("expected"), [ - ( - True, - [ - mock.call("on_enter_state", event="__initial__", state="s1", source=""), - mock.call("on_exit_state", event="t1", state="s1", target="s2"), - mock.call("on_transition", event="t1", source="s1", target="s2"), - mock.call("on_enter_state", event="t1", state="s2", source="s1"), - mock.call("after_transition", event="t1", source="s1", target="s2"), - mock.call("on_exit_state", event="t2a", state="s2", target="s2"), - mock.call("on_transition", event="t2a", source="s2", target="s2"), - mock.call("on_enter_state", event="t2a", state="s2", source="s2"), - mock.call("after_transition", event="t2a", source="s2", target="s2"), - mock.call("on_exit_state", event="t2b", state="s2", target="s3"), - mock.call("on_transition", event="t2b", source="s2", target="s3"), - mock.call("on_enter_state", event="t2b", state="s3", source="s2"), - mock.call("after_transition", event="t2b", source="s2", target="s3"), - mock.call("on_exit_state", event="t3", state="s3", target="s4"), - mock.call("on_transition", event="t3", source="s3", target="s4"), - mock.call("on_enter_state", event="t3", state="s4", source="s3"), - mock.call("after_transition", event="t3", source="s3", target="s4"), - ], - ), - ( - False, - TransitionNotAllowed, - ), + [ + mock.call("on_enter_state", event="__initial__", state="s1", source=""), + mock.call("on_exit_state", event="t1", state="s1", target="s2"), + mock.call("on_transition", event="t1", source="s1", target="s2"), + mock.call("on_enter_state", event="t1", state="s2", source="s1"), + mock.call("after_transition", event="t1", source="s1", target="s2"), + mock.call("on_exit_state", event="t2a", state="s2", target="s2"), + mock.call("on_transition", event="t2a", source="s2", target="s2"), + mock.call("on_enter_state", event="t2a", state="s2", source="s2"), + mock.call("after_transition", event="t2a", source="s2", target="s2"), + mock.call("on_exit_state", event="t2b", state="s2", target="s3"), + mock.call("on_transition", event="t2b", source="s2", target="s3"), + mock.call("on_enter_state", event="t2b", state="s3", source="s2"), + mock.call("after_transition", event="t2b", source="s2", target="s3"), + mock.call("on_exit_state", event="t3", state="s3", target="s4"), + mock.call("on_transition", event="t3", source="s3", target="s4"), + mock.call("on_enter_state", event="t3", state="s4", source="s3"), + mock.call("after_transition", event="t3", source="s3", target="s4"), + ], ], ) - def test_should_preserve_event_order(self, chained_on_sm_class, rtc, expected): - sm = chained_on_sm_class(rtc=rtc) + def test_should_preserve_event_order(self, chained_on_sm_class, expected): + sm = chained_on_sm_class() if inspect.isclass(expected) and issubclass(expected, Exception): with pytest.raises(expected): @@ -177,24 +149,6 @@ def test_should_preserve_event_order(self, chained_on_sm_class, rtc, expected): class TestAsyncEngineRTC: - async def test_no_rtc_in_async_is_not_supported(self, chained_on_sm_class): - class AsyncStateMachine(StateMachine): - initial = State("Initial", initial=True) - processing = State() - final = State("Final", final=True) - - start = initial.to(processing) - finish = processing.to(final) - - async def on_start(self): - return "starting" - - async def on_finish(self): - return "finishing" - - with pytest.raises(InvalidDefinition, match="Only RTC is supported on async engine"): - AsyncStateMachine(rtc=False) - @pytest.mark.parametrize( ("expected"), [ @@ -231,9 +185,9 @@ class ChainedSM(StateMachine): t2b = s2.to(s3) t3 = s3.to(s4) - def __init__(self, rtc=True): + def __init__(self): self.spy = mock.Mock() - super().__init__(rtc=rtc) + super().__init__() async def on_t1(self): return [await self.t2a(), await self.t2b(), await self.send("t3")] diff --git a/tests/test_transitions.py b/tests/test_transitions.py index 5f975db4..6c0c2c41 100644 --- a/tests/test_transitions.py +++ b/tests/test_transitions.py @@ -252,8 +252,8 @@ class TestStateMachine(StateMachine): loop = initial.to.itself(internal=internal) - def _get_engine(self, rtc: bool): - return engine(self, rtc) + def _get_engine(self): + return engine(self) def on_exit_initial(self): calls.append("on_exit_initial")