tinyfsm 0.0.1__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.
- tinyfsm-0.0.1/LICENSE.txt +19 -0
- tinyfsm-0.0.1/PKG-INFO +101 -0
- tinyfsm-0.0.1/README.md +83 -0
- tinyfsm-0.0.1/pyproject.toml +26 -0
- tinyfsm-0.0.1/tinyfsm/__init__.py +1 -0
- tinyfsm-0.0.1/tinyfsm/_export_list.py +10 -0
- tinyfsm-0.0.1/tinyfsm/api.py +13 -0
- tinyfsm-0.0.1/tinyfsm/exc.py +60 -0
- tinyfsm-0.0.1/tinyfsm/interface.py +79 -0
- tinyfsm-0.0.1/tinyfsm/runner.py +131 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright 2026 Maciej Wiatrzyk <maciej.wiatrzyk@gmail.com>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the “Software”), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
8
|
+
so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
tinyfsm-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: tinyfsm
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A tiny and minimal finite state machine for Python.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Maciej Wiatrzyk
|
|
7
|
+
Author-email: maciej.wiatrzyk@gmail.com
|
|
8
|
+
Requires-Python: >=3.9,<4
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# TinyFSM
|
|
19
|
+
|
|
20
|
+
A tiny and minimal finite state machine for Python.
|
|
21
|
+
|
|
22
|
+
## About
|
|
23
|
+
|
|
24
|
+
**TinyFSM** is a fast and minimal finite state machine engine for Python. It
|
|
25
|
+
runs the finite state defined as list of traversals, where each traversal is
|
|
26
|
+
given by current and next state name and a traversal function that is evaluated
|
|
27
|
+
on currently processed event. Once traversal function returns true, the current
|
|
28
|
+
state is changed to the next state according to matched traversal.
|
|
29
|
+
|
|
30
|
+
This library can be used as a backbone for creating larger scale FSM-based
|
|
31
|
+
tokenizers and parsers.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
$ pip install tinyfsm
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
Below you'll find a simple text parser that split text into WORD and SPACE
|
|
42
|
+
tokens, where WORD is a consecutive collection of alpha characters, and SPACE
|
|
43
|
+
is consecutive collection of space characters:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from tinyfsm.api import Traversal, StateMachineRunner, EventRejectedError
|
|
47
|
+
|
|
48
|
+
# Declaration of the state machine as list of Traversal objects.
|
|
49
|
+
# Each object contains source state, destination state, and a function that
|
|
50
|
+
# checks if current input (or event) should trigger state traversal.
|
|
51
|
+
definition = [
|
|
52
|
+
Traversal[str]("initial", "word", lambda event: event.isalpha()),
|
|
53
|
+
Traversal[str]("initial", "space", lambda event: event == " "),
|
|
54
|
+
Traversal[str]("word", "word", lambda event: event.isalpha()),
|
|
55
|
+
Traversal[str]("word", "space", lambda event: event == " "),
|
|
56
|
+
Traversal[str]("word", "final", lambda event: event == ""),
|
|
57
|
+
Traversal[str]("space", "space", lambda event: event == " "),
|
|
58
|
+
Traversal[str]("space", "word", lambda event: event.isalpha()),
|
|
59
|
+
Traversal[str]("space", "final", lambda event: event == ""),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
class Listener:
|
|
63
|
+
"""Event listener for the state machine.
|
|
64
|
+
|
|
65
|
+
It listens for events like state change, or input value dispatching end.
|
|
66
|
+
Implementations (like this one) can use these methods to buffer inputs and
|
|
67
|
+
emit tokens.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, output: list[tuple[str, str]]):
|
|
71
|
+
self._output = output
|
|
72
|
+
self._buffer = ""
|
|
73
|
+
|
|
74
|
+
def on_state_change(self, event: str, prev_state: str, current_state: str):
|
|
75
|
+
if prev_state != current_state:
|
|
76
|
+
if prev_state == "word":
|
|
77
|
+
self._output.append(("WORD", self._buffer))
|
|
78
|
+
if prev_state == "space":
|
|
79
|
+
self._output.append(("SPACE", self._buffer))
|
|
80
|
+
self._buffer = ""
|
|
81
|
+
|
|
82
|
+
def on_dispatch_done(self, event: str, current_state: str):
|
|
83
|
+
self._buffer += event
|
|
84
|
+
|
|
85
|
+
def tokenize(text: str) -> list[tuple[str, str]]:
|
|
86
|
+
"""Tokenization function.
|
|
87
|
+
|
|
88
|
+
This is just an example, but in general a some sort of function gluing all
|
|
89
|
+
parts together is recommended. Here the function parses given text and
|
|
90
|
+
outputs list of tokens parsed from it.
|
|
91
|
+
"""
|
|
92
|
+
out = []
|
|
93
|
+
listener = Listener(out)
|
|
94
|
+
runner = StateMachineRunner(definition, listener)
|
|
95
|
+
with runner:
|
|
96
|
+
for char in text:
|
|
97
|
+
runner.dispatch(char)
|
|
98
|
+
runner.dispatch("")
|
|
99
|
+
return out
|
|
100
|
+
```
|
|
101
|
+
|
tinyfsm-0.0.1/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# TinyFSM
|
|
2
|
+
|
|
3
|
+
A tiny and minimal finite state machine for Python.
|
|
4
|
+
|
|
5
|
+
## About
|
|
6
|
+
|
|
7
|
+
**TinyFSM** is a fast and minimal finite state machine engine for Python. It
|
|
8
|
+
runs the finite state defined as list of traversals, where each traversal is
|
|
9
|
+
given by current and next state name and a traversal function that is evaluated
|
|
10
|
+
on currently processed event. Once traversal function returns true, the current
|
|
11
|
+
state is changed to the next state according to matched traversal.
|
|
12
|
+
|
|
13
|
+
This library can be used as a backbone for creating larger scale FSM-based
|
|
14
|
+
tokenizers and parsers.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```shell
|
|
19
|
+
$ pip install tinyfsm
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
Below you'll find a simple text parser that split text into WORD and SPACE
|
|
25
|
+
tokens, where WORD is a consecutive collection of alpha characters, and SPACE
|
|
26
|
+
is consecutive collection of space characters:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from tinyfsm.api import Traversal, StateMachineRunner, EventRejectedError
|
|
30
|
+
|
|
31
|
+
# Declaration of the state machine as list of Traversal objects.
|
|
32
|
+
# Each object contains source state, destination state, and a function that
|
|
33
|
+
# checks if current input (or event) should trigger state traversal.
|
|
34
|
+
definition = [
|
|
35
|
+
Traversal[str]("initial", "word", lambda event: event.isalpha()),
|
|
36
|
+
Traversal[str]("initial", "space", lambda event: event == " "),
|
|
37
|
+
Traversal[str]("word", "word", lambda event: event.isalpha()),
|
|
38
|
+
Traversal[str]("word", "space", lambda event: event == " "),
|
|
39
|
+
Traversal[str]("word", "final", lambda event: event == ""),
|
|
40
|
+
Traversal[str]("space", "space", lambda event: event == " "),
|
|
41
|
+
Traversal[str]("space", "word", lambda event: event.isalpha()),
|
|
42
|
+
Traversal[str]("space", "final", lambda event: event == ""),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
class Listener:
|
|
46
|
+
"""Event listener for the state machine.
|
|
47
|
+
|
|
48
|
+
It listens for events like state change, or input value dispatching end.
|
|
49
|
+
Implementations (like this one) can use these methods to buffer inputs and
|
|
50
|
+
emit tokens.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, output: list[tuple[str, str]]):
|
|
54
|
+
self._output = output
|
|
55
|
+
self._buffer = ""
|
|
56
|
+
|
|
57
|
+
def on_state_change(self, event: str, prev_state: str, current_state: str):
|
|
58
|
+
if prev_state != current_state:
|
|
59
|
+
if prev_state == "word":
|
|
60
|
+
self._output.append(("WORD", self._buffer))
|
|
61
|
+
if prev_state == "space":
|
|
62
|
+
self._output.append(("SPACE", self._buffer))
|
|
63
|
+
self._buffer = ""
|
|
64
|
+
|
|
65
|
+
def on_dispatch_done(self, event: str, current_state: str):
|
|
66
|
+
self._buffer += event
|
|
67
|
+
|
|
68
|
+
def tokenize(text: str) -> list[tuple[str, str]]:
|
|
69
|
+
"""Tokenization function.
|
|
70
|
+
|
|
71
|
+
This is just an example, but in general a some sort of function gluing all
|
|
72
|
+
parts together is recommended. Here the function parses given text and
|
|
73
|
+
outputs list of tokens parsed from it.
|
|
74
|
+
"""
|
|
75
|
+
out = []
|
|
76
|
+
listener = Listener(out)
|
|
77
|
+
runner = StateMachineRunner(definition, listener)
|
|
78
|
+
with runner:
|
|
79
|
+
for char in text:
|
|
80
|
+
runner.dispatch(char)
|
|
81
|
+
runner.dispatch("")
|
|
82
|
+
return out
|
|
83
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tinyfsm"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "A tiny and minimal finite state machine for Python."
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Maciej Wiatrzyk",email = "maciej.wiatrzyk@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9,<4"
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
17
|
+
build-backend = "poetry.core.masonry.api"
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.dev.dependencies]
|
|
20
|
+
pytest = "8.4.2"
|
|
21
|
+
pytest-cov = "^7.1.0"
|
|
22
|
+
mockify = "^0.14.0"
|
|
23
|
+
invoke = "^3.0.3"
|
|
24
|
+
ruff = "^0.15.14"
|
|
25
|
+
bumpify = "^0.5.1"
|
|
26
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .runner import __all__ as _runner_all
|
|
2
|
+
from .exc import __all__ as _exc_all
|
|
3
|
+
from .interface import __all__ as _interface_all
|
|
4
|
+
|
|
5
|
+
from .runner import *
|
|
6
|
+
from .exc import *
|
|
7
|
+
from .interface import *
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
*_runner_all,
|
|
11
|
+
*_exc_all,
|
|
12
|
+
*_interface_all,
|
|
13
|
+
) # type: ignore
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from . import _export_list
|
|
4
|
+
|
|
5
|
+
__all__ = export = _export_list.ExportList() # type: ignore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@export
|
|
9
|
+
class TinyFSMError(Exception):
|
|
10
|
+
"""Common base class for all exception this library may raise."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@export
|
|
14
|
+
class EventRejectedError(TinyFSMError):
|
|
15
|
+
"""Raised when event was rejected by the state machine.
|
|
16
|
+
|
|
17
|
+
Event is rejected only if there was no matching traversal found.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
#: The rejected event object.
|
|
21
|
+
event: Any
|
|
22
|
+
|
|
23
|
+
#: The current state name.
|
|
24
|
+
current_state: str
|
|
25
|
+
|
|
26
|
+
def __init__(self, event: Any, current_state: str):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.event = event
|
|
29
|
+
self.current_state = current_state
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return f"event {self.event!r} was rejected; no traversal found for current state {self.current_state!r}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@export
|
|
36
|
+
class FinalStateNotReached(TinyFSMError):
|
|
37
|
+
"""Raised when closing state machine if final state was not reached.
|
|
38
|
+
|
|
39
|
+
This means that either the input sequence of event is incomplete, or that
|
|
40
|
+
the state machine definition is malformed and final state is
|
|
41
|
+
unreachable.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
#: The last dispatched event.
|
|
45
|
+
last_event: Any
|
|
46
|
+
|
|
47
|
+
#: The name of the final state.
|
|
48
|
+
final_state: str
|
|
49
|
+
|
|
50
|
+
#: The name of the current state.
|
|
51
|
+
current_state: str
|
|
52
|
+
|
|
53
|
+
def __init__(self, last_event: Any, final_state: str, current_state: str):
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.last_event = last_event
|
|
56
|
+
self.final_state = final_state
|
|
57
|
+
self.current_state = current_state
|
|
58
|
+
|
|
59
|
+
def __str__(self) -> str:
|
|
60
|
+
return f"final state {self.final_state!r} was not reached; current state is {self.current_state!r}, last event was {self.last_event!r}"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Callable, Generic, Protocol, TypeVar
|
|
3
|
+
|
|
4
|
+
from . import _export_list
|
|
5
|
+
|
|
6
|
+
__all__ = export = _export_list.ExportList() # type: ignore
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
Tc = TypeVar("Tc", contravariant=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@export
|
|
14
|
+
@dataclasses.dataclass
|
|
15
|
+
class Traversal(Generic[T]):
|
|
16
|
+
"""Object for creating state traversal definitions.
|
|
17
|
+
|
|
18
|
+
This is the basic building block for creating state machines. It is used to
|
|
19
|
+
declare source and destination states, and a traversal function that is
|
|
20
|
+
used to check if current event should cause traversal to the destination
|
|
21
|
+
state.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
#: Source event name.
|
|
25
|
+
src: str
|
|
26
|
+
|
|
27
|
+
#: Destination event name.
|
|
28
|
+
dest: str
|
|
29
|
+
|
|
30
|
+
#: Condition function.
|
|
31
|
+
#:
|
|
32
|
+
#: It will be called with the current event and if the return value is #:
|
|
33
|
+
#: ``True`` then state traversal from :attr:`src` to :attr:`dest`).is
|
|
34
|
+
#: performed. If the current state is not the source state, then the
|
|
35
|
+
#: function will not be called.
|
|
36
|
+
cond: Callable[[T], bool]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@export
|
|
40
|
+
class StateMachineListener(Protocol, Generic[Tc]):
|
|
41
|
+
"""State machine listener protocol.
|
|
42
|
+
|
|
43
|
+
Defines interface to be used by custom state machine listeners receiving
|
|
44
|
+
notifications about event objects and state traversals. Implementations may
|
|
45
|
+
use this to add events to internal buffers, queues etc.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def on_state_change(self, event: Tc, prev_state: str, current_state: str):
|
|
49
|
+
"""Triggered when *event* caused state traversal from *prev_state* to
|
|
50
|
+
*current_state*.
|
|
51
|
+
|
|
52
|
+
This will only be called if one of state traversal condition functions
|
|
53
|
+
returned ``True``.
|
|
54
|
+
|
|
55
|
+
:param event:
|
|
56
|
+
The event that caused traversal.
|
|
57
|
+
|
|
58
|
+
:param prev_state:
|
|
59
|
+
The name of a previous state.
|
|
60
|
+
|
|
61
|
+
:param current_state:
|
|
62
|
+
The name of a current state.
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def on_dispatch_done(self, event: Tc, current_state: str):
|
|
67
|
+
"""Triggered when event dispatching ends.
|
|
68
|
+
|
|
69
|
+
This method will be called for every successfully accepted event even
|
|
70
|
+
if there was no state change.
|
|
71
|
+
|
|
72
|
+
:param event:
|
|
73
|
+
The event object.
|
|
74
|
+
|
|
75
|
+
:param current_state:
|
|
76
|
+
The name of the state the state machine was left in once *event*
|
|
77
|
+
was processed.
|
|
78
|
+
"""
|
|
79
|
+
...
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from typing import Generic, Optional, Sequence, TypeVar
|
|
2
|
+
|
|
3
|
+
from . import _export_list
|
|
4
|
+
from .exc import EventRejectedError, FinalStateNotReached
|
|
5
|
+
from .interface import StateMachineListener, Traversal
|
|
6
|
+
|
|
7
|
+
__all__ = export = _export_list.ExportList() # type: ignore
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@export
|
|
13
|
+
class StateMachineRunner(Generic[T]):
|
|
14
|
+
"""State machine runner.
|
|
15
|
+
|
|
16
|
+
Each instance of this class can only parse single chain of events (as it
|
|
17
|
+
keeps the state between :meth:`dispatch` calls) and therefore a brand new
|
|
18
|
+
instance must be created to parse another chain of events.
|
|
19
|
+
|
|
20
|
+
You can feed state machine with events by using :meth:`dispatch` method.
|
|
21
|
+
Each dispatched event may cause state traversal, or event rejection (if the
|
|
22
|
+
input chain of events could not be parsed using defined state machine). It
|
|
23
|
+
is recommended, although not required, to call :meth:`close` once all the
|
|
24
|
+
events are dispatched.
|
|
25
|
+
|
|
26
|
+
The state machine can also be used as a context manager to automatically
|
|
27
|
+
call :meth:`close` on context exit::
|
|
28
|
+
|
|
29
|
+
sm = StateMachine(...)
|
|
30
|
+
with sm:
|
|
31
|
+
for event in sequence_of_events:
|
|
32
|
+
sm.dispatch(event)
|
|
33
|
+
|
|
34
|
+
:param definition:
|
|
35
|
+
State machine definition as a sequence of state traversals defined
|
|
36
|
+
using :class:`tinyfsm.interface.Traversal` objects.
|
|
37
|
+
|
|
38
|
+
:param listener:
|
|
39
|
+
Instance of :class:`StateMachineListener` protocol.
|
|
40
|
+
|
|
41
|
+
This object will receive notifications from the state machine once it
|
|
42
|
+
is processing events.
|
|
43
|
+
|
|
44
|
+
:param initial_state:
|
|
45
|
+
The name of the initial state.
|
|
46
|
+
|
|
47
|
+
It is required to have at least one initial state name used in the
|
|
48
|
+
state machine definition as a :attr:`tinyfsm.interface.Traversal.src`
|
|
49
|
+
attribute.
|
|
50
|
+
|
|
51
|
+
:param final_state:
|
|
52
|
+
The name of the final state.
|
|
53
|
+
|
|
54
|
+
It is required to have at least one final state name used in the state
|
|
55
|
+
machine definition as a :attr:`tinyfsm.interface.Traversal.dest`
|
|
56
|
+
attribute.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
definition: Sequence[Traversal[T]],
|
|
62
|
+
listener: StateMachineListener[T],
|
|
63
|
+
initial_state: str = "initial",
|
|
64
|
+
final_state: str = "final",
|
|
65
|
+
):
|
|
66
|
+
self.__traversal_map: dict[str, list[Traversal]] = {}
|
|
67
|
+
has_initial = has_final = False
|
|
68
|
+
for traversal in definition:
|
|
69
|
+
if traversal.src == initial_state:
|
|
70
|
+
has_initial = True
|
|
71
|
+
if traversal.dest == final_state:
|
|
72
|
+
has_final = True
|
|
73
|
+
self.__traversal_map.setdefault(traversal.src, []).append(traversal)
|
|
74
|
+
if not has_initial:
|
|
75
|
+
raise TypeError(f"no initial state found: {initial_state}")
|
|
76
|
+
if not has_final:
|
|
77
|
+
raise TypeError(f"no final state found: {final_state}")
|
|
78
|
+
self.__listener = listener
|
|
79
|
+
self.__initial_state = initial_state
|
|
80
|
+
self.__final_state = final_state
|
|
81
|
+
self.__current_state = self.__initial_state
|
|
82
|
+
self.__last_event: Optional[T] = None
|
|
83
|
+
|
|
84
|
+
def __enter__(self) -> "StateMachineRunner":
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def __exit__(self, exc_type, exc, tb):
|
|
88
|
+
if exc is not None:
|
|
89
|
+
return None
|
|
90
|
+
return self.close()
|
|
91
|
+
|
|
92
|
+
def dispatch(self, event: T):
|
|
93
|
+
"""Dispatch event to the state machine.
|
|
94
|
+
|
|
95
|
+
This should be called for each event, and state machine can either
|
|
96
|
+
accept the event, and maybe traverse to a different state in response
|
|
97
|
+
for that event, or reject it by raising :exc:`EventRejectedError`
|
|
98
|
+
exception.
|
|
99
|
+
|
|
100
|
+
:param event:
|
|
101
|
+
The event object to dispatch.
|
|
102
|
+
"""
|
|
103
|
+
self.__last_event = event
|
|
104
|
+
current_state_traversals = self.__traversal_map.get(self.__current_state)
|
|
105
|
+
if current_state_traversals is None:
|
|
106
|
+
raise EventRejectedError(event, self.__current_state)
|
|
107
|
+
print(current_state_traversals)
|
|
108
|
+
for traversal in current_state_traversals:
|
|
109
|
+
if traversal.cond(event):
|
|
110
|
+
next_state = traversal.dest
|
|
111
|
+
self.__listener.on_state_change(event, self.__current_state, next_state)
|
|
112
|
+
self.__current_state = next_state
|
|
113
|
+
break
|
|
114
|
+
else:
|
|
115
|
+
raise EventRejectedError(event, self.__current_state)
|
|
116
|
+
self.__listener.on_dispatch_done(event, self.__current_state)
|
|
117
|
+
|
|
118
|
+
def close(self):
|
|
119
|
+
"""Close this state machine.
|
|
120
|
+
|
|
121
|
+
This method should be called after event dispatching ends. Its role is
|
|
122
|
+
to check if the final state was reached; if the final state was
|
|
123
|
+
reached, the method silently returns. Otherwise it raises
|
|
124
|
+
:exc:`tinyfsm.exc.FinalStateNotReached` error.
|
|
125
|
+
"""
|
|
126
|
+
if not self.is_final():
|
|
127
|
+
raise FinalStateNotReached(self.__last_event, self.__final_state, self.__current_state)
|
|
128
|
+
|
|
129
|
+
def is_final(self) -> bool:
|
|
130
|
+
"""Check if the final state is reached."""
|
|
131
|
+
return self.__current_state == self.__final_state
|