tinyfsm 0.0.1__tar.gz → 0.0.2__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.
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: tinyfsm
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: A tiny and minimal finite state machine for Python.
5
5
  License: MIT
6
+ License-File: LICENSE.txt
6
7
  Author: Maciej Wiatrzyk
7
8
  Author-email: maciej.wiatrzyk@gmail.com
8
9
  Requires-Python: >=3.9,<4
@@ -13,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
13
14
  Classifier: Programming Language :: Python :: 3.11
14
15
  Classifier: Programming Language :: Python :: 3.12
15
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
16
18
  Description-Content-Type: text/markdown
17
19
 
18
20
  # TinyFSM
@@ -38,57 +40,42 @@ $ pip install tinyfsm
38
40
 
39
41
  ## Quickstart
40
42
 
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:
43
+ Here's a simple text tokenizer written using **TinyFSM** library that splits
44
+ text composed of alphanumeric characters into groups of words and numbers and
45
+ returns list of ``(token_name, token_data)`` tuples:
44
46
 
45
47
  ```python
46
- from tinyfsm.api import Traversal, StateMachineRunner, EventRejectedError
48
+ from tinyfsm.api import Traversal, StateMachineRunner, InputRejectedError
47
49
 
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
50
  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 == ""),
51
+ Traversal[str]("initial", "word", str.isalpha),
52
+ Traversal[str]("initial", "number", str.isdigit),
53
+ Traversal[str]("initial", "final", lambda input: input == ""),
54
+ Traversal[str]("word", "word", str.isalpha),
55
+ Traversal[str]("word", "number", str.isdigit),
56
+ Traversal[str]("word", "final", lambda input: input == ""),
57
+ Traversal[str]("number", "number", str.isdigit),
58
+ Traversal[str]("number", "word", str.isalpha),
59
+ Traversal[str]("number", "final", lambda input: input == ""),
60
60
  ]
61
61
 
62
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
63
  def __init__(self, output: list[tuple[str, str]]):
71
64
  self._output = output
72
65
  self._buffer = ""
73
66
 
74
- def on_state_change(self, event: str, prev_state: str, current_state: str):
67
+ def on_state_change(self, input: str, prev_state: str, current_state: str):
75
68
  if prev_state != current_state:
76
69
  if prev_state == "word":
77
70
  self._output.append(("WORD", self._buffer))
78
- if prev_state == "space":
79
- self._output.append(("SPACE", self._buffer))
71
+ if prev_state == "number":
72
+ self._output.append(("NUMBER", self._buffer))
80
73
  self._buffer = ""
81
74
 
82
- def on_dispatch_done(self, event: str, current_state: str):
83
- self._buffer += event
75
+ def on_dispatch_done(self, input: str, current_state: str):
76
+ self._buffer += input
84
77
 
85
78
  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
79
  out = []
93
80
  listener = Listener(out)
94
81
  runner = StateMachineRunner(definition, listener)
@@ -99,3 +86,15 @@ def tokenize(text: str) -> list[tuple[str, str]]:
99
86
  return out
100
87
  ```
101
88
 
89
+ And the tokenizer from above will split any text composed of just words and
90
+ numbers into something like this:
91
+
92
+ ```python
93
+ out = tokenize("foo123bar456")
94
+ print(out) # Would print: [("WORD", "foo"), ("NUMBER", "123"), ("WORD", "bar"), ("NUMBER", "456")]
95
+ ```
96
+
97
+ # License
98
+
99
+ This project is released under the terms of the MIT license.
100
+
@@ -0,0 +1,80 @@
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
+ Here's a simple text tokenizer written using **TinyFSM** library that splits
25
+ text composed of alphanumeric characters into groups of words and numbers and
26
+ returns list of ``(token_name, token_data)`` tuples:
27
+
28
+ ```python
29
+ from tinyfsm.api import Traversal, StateMachineRunner, InputRejectedError
30
+
31
+ definition = [
32
+ Traversal[str]("initial", "word", str.isalpha),
33
+ Traversal[str]("initial", "number", str.isdigit),
34
+ Traversal[str]("initial", "final", lambda input: input == ""),
35
+ Traversal[str]("word", "word", str.isalpha),
36
+ Traversal[str]("word", "number", str.isdigit),
37
+ Traversal[str]("word", "final", lambda input: input == ""),
38
+ Traversal[str]("number", "number", str.isdigit),
39
+ Traversal[str]("number", "word", str.isalpha),
40
+ Traversal[str]("number", "final", lambda input: input == ""),
41
+ ]
42
+
43
+ class Listener:
44
+ def __init__(self, output: list[tuple[str, str]]):
45
+ self._output = output
46
+ self._buffer = ""
47
+
48
+ def on_state_change(self, input: str, prev_state: str, current_state: str):
49
+ if prev_state != current_state:
50
+ if prev_state == "word":
51
+ self._output.append(("WORD", self._buffer))
52
+ if prev_state == "number":
53
+ self._output.append(("NUMBER", self._buffer))
54
+ self._buffer = ""
55
+
56
+ def on_dispatch_done(self, input: str, current_state: str):
57
+ self._buffer += input
58
+
59
+ def tokenize(text: str) -> list[tuple[str, str]]:
60
+ out = []
61
+ listener = Listener(out)
62
+ runner = StateMachineRunner(definition, listener)
63
+ with runner:
64
+ for char in text:
65
+ runner.dispatch(char)
66
+ runner.dispatch("")
67
+ return out
68
+ ```
69
+
70
+ And the tokenizer from above will split any text composed of just words and
71
+ numbers into something like this:
72
+
73
+ ```python
74
+ out = tokenize("foo123bar456")
75
+ print(out) # Would print: [("WORD", "foo"), ("NUMBER", "123"), ("WORD", "bar"), ("NUMBER", "456")]
76
+ ```
77
+
78
+ # License
79
+
80
+ This project is released under the terms of the MIT license.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tinyfsm"
3
- version = "0.0.1"
3
+ version = "0.0.2"
4
4
  description = "A tiny and minimal finite state machine for Python."
5
5
  authors = [
6
6
  {name = "Maciej Wiatrzyk",email = "maciej.wiatrzyk@gmail.com"}
@@ -0,0 +1 @@
1
+ __version__ = "0.0.2"
@@ -11,38 +11,38 @@ class TinyFSMError(Exception):
11
11
 
12
12
 
13
13
  @export
14
- class EventRejectedError(TinyFSMError):
15
- """Raised when event was rejected by the state machine.
14
+ class InputRejectedError(TinyFSMError):
15
+ """Raised when input was rejected by the state machine.
16
16
 
17
- Event is rejected only if there was no matching traversal found.
17
+ Input is rejected only if there was no matching traversal found or if all
18
+ traversal functions returned ``False`` for that input.
18
19
  """
19
20
 
20
- #: The rejected event object.
21
- event: Any
21
+ #: The rejected input object.
22
+ input: Any
22
23
 
23
24
  #: The current state name.
24
25
  current_state: str
25
26
 
26
- def __init__(self, event: Any, current_state: str):
27
+ def __init__(self, input: Any, current_state: str):
27
28
  super().__init__()
28
- self.event = event
29
+ self.input = input
29
30
  self.current_state = current_state
30
31
 
31
32
  def __str__(self) -> str:
32
- return f"event {self.event!r} was rejected; no traversal found for current state {self.current_state!r}"
33
+ return f"input {self.input!r} was rejected; no traversal found for current state {self.current_state!r}"
33
34
 
34
35
 
35
36
  @export
36
37
  class FinalStateNotReached(TinyFSMError):
37
38
  """Raised when closing state machine if final state was not reached.
38
39
 
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.
40
+ This means that either the input sequence is incomplete, or that the state
41
+ machine definition is malformed and final state is unreachable.
42
42
  """
43
43
 
44
- #: The last dispatched event.
45
- last_event: Any
44
+ #: The last dispatched input.
45
+ last_input: Any
46
46
 
47
47
  #: The name of the final state.
48
48
  final_state: str
@@ -50,11 +50,11 @@ class FinalStateNotReached(TinyFSMError):
50
50
  #: The name of the current state.
51
51
  current_state: str
52
52
 
53
- def __init__(self, last_event: Any, final_state: str, current_state: str):
53
+ def __init__(self, last_input: Any, final_state: str, current_state: str):
54
54
  super().__init__()
55
- self.last_event = last_event
55
+ self.last_input = last_input
56
56
  self.final_state = final_state
57
57
  self.current_state = current_state
58
58
 
59
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}"
60
+ return f"final state {self.final_state!r} was not reached; current state is {self.current_state!r}, last input was {self.last_input!r}"
@@ -21,19 +21,16 @@ class Traversal(Generic[T]):
21
21
  state.
22
22
  """
23
23
 
24
- #: Source event name.
25
- src: str
24
+ #: Source state name.
25
+ source_state: str
26
26
 
27
- #: Destination event name.
28
- dest: str
27
+ #: Target state name.
28
+ target_state: str
29
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]
30
+ #: The function to be called for each input when current state is
31
+ #: :attr:`source_state`; if the function returns ``True``, current state is
32
+ #: changed to :attr:`target_state`.
33
+ traverse_func: Callable[[T], bool]
37
34
 
38
35
 
39
36
  @export
@@ -45,15 +42,15 @@ class StateMachineListener(Protocol, Generic[Tc]):
45
42
  use this to add events to internal buffers, queues etc.
46
43
  """
47
44
 
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
45
+ def on_state_change(self, input: Tc, prev_state: str, current_state: str):
46
+ """Triggered when *input* caused state traversal from *prev_state* to
50
47
  *current_state*.
51
48
 
52
- This will only be called if one of state traversal condition functions
53
- returned ``True``.
49
+ This will only be called if one of traverse functions (see
50
+ :attr:`Traversal.traverse_func`) returned ``True``.
54
51
 
55
- :param event:
56
- The event that caused traversal.
52
+ :param input:
53
+ The input that caused traversal.
57
54
 
58
55
  :param prev_state:
59
56
  The name of a previous state.
@@ -63,14 +60,14 @@ class StateMachineListener(Protocol, Generic[Tc]):
63
60
  """
64
61
  ...
65
62
 
66
- def on_dispatch_done(self, event: Tc, current_state: str):
67
- """Triggered when event dispatching ends.
63
+ def on_dispatch_done(self, input: Tc, current_state: str):
64
+ """Triggered when input dispatching ends.
68
65
 
69
- This method will be called for every successfully accepted event even
66
+ This method will be called for every successfully accepted input even
70
67
  if there was no state change.
71
68
 
72
- :param event:
73
- The event object.
69
+ :param input:
70
+ The input object.
74
71
 
75
72
  :param current_state:
76
73
  The name of the state the state machine was left in once *event*
@@ -1,7 +1,7 @@
1
1
  from typing import Generic, Optional, Sequence, TypeVar
2
2
 
3
3
  from . import _export_list
4
- from .exc import EventRejectedError, FinalStateNotReached
4
+ from .exc import InputRejectedError, FinalStateNotReached
5
5
  from .interface import StateMachineListener, Traversal
6
6
 
7
7
  __all__ = export = _export_list.ExportList() # type: ignore
@@ -66,11 +66,11 @@ class StateMachineRunner(Generic[T]):
66
66
  self.__traversal_map: dict[str, list[Traversal]] = {}
67
67
  has_initial = has_final = False
68
68
  for traversal in definition:
69
- if traversal.src == initial_state:
69
+ if traversal.source_state == initial_state:
70
70
  has_initial = True
71
- if traversal.dest == final_state:
71
+ if traversal.target_state == final_state:
72
72
  has_final = True
73
- self.__traversal_map.setdefault(traversal.src, []).append(traversal)
73
+ self.__traversal_map.setdefault(traversal.source_state, []).append(traversal)
74
74
  if not has_initial:
75
75
  raise TypeError(f"no initial state found: {initial_state}")
76
76
  if not has_final:
@@ -79,7 +79,7 @@ class StateMachineRunner(Generic[T]):
79
79
  self.__initial_state = initial_state
80
80
  self.__final_state = final_state
81
81
  self.__current_state = self.__initial_state
82
- self.__last_event: Optional[T] = None
82
+ self.__last_input: Optional[T] = None
83
83
 
84
84
  def __enter__(self) -> "StateMachineRunner":
85
85
  return self
@@ -89,42 +89,40 @@ class StateMachineRunner(Generic[T]):
89
89
  return None
90
90
  return self.close()
91
91
 
92
- def dispatch(self, event: T):
93
- """Dispatch event to the state machine.
92
+ def dispatch(self, input: T):
93
+ """Dispatch input to the state machine.
94
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.
95
+ This should be called for each input, and state machine can either
96
+ accept the input, and maybe traverse to a different state, or reject it
97
+ by raising :exc:`InputRejectedError` exception.
99
98
 
100
- :param event:
101
- The event object to dispatch.
99
+ :param input:
100
+ The current input to dispatch.
102
101
  """
103
- self.__last_event = event
102
+ self.__last_input = input
104
103
  current_state_traversals = self.__traversal_map.get(self.__current_state)
105
104
  if current_state_traversals is None:
106
- raise EventRejectedError(event, self.__current_state)
107
- print(current_state_traversals)
105
+ raise InputRejectedError(input, self.__current_state)
108
106
  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)
107
+ if traversal.traverse_func(input):
108
+ next_state = traversal.target_state
109
+ self.__listener.on_state_change(input, self.__current_state, next_state)
112
110
  self.__current_state = next_state
113
111
  break
114
112
  else:
115
- raise EventRejectedError(event, self.__current_state)
116
- self.__listener.on_dispatch_done(event, self.__current_state)
113
+ raise InputRejectedError(input, self.__current_state)
114
+ self.__listener.on_dispatch_done(input, self.__current_state)
117
115
 
118
116
  def close(self):
119
117
  """Close this state machine.
120
118
 
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
119
+ This method should be called shortly after all inputs are dispatched.
120
+ Its role is to ensure that the final state was reached; if the final
121
+ state was reached, the method silently returns. Otherwise it raises
124
122
  :exc:`tinyfsm.exc.FinalStateNotReached` error.
125
123
  """
126
124
  if not self.is_final():
127
- raise FinalStateNotReached(self.__last_event, self.__final_state, self.__current_state)
125
+ raise FinalStateNotReached(self.__last_input, self.__final_state, self.__current_state)
128
126
 
129
127
  def is_final(self) -> bool:
130
128
  """Check if the final state is reached."""
tinyfsm-0.0.1/README.md DELETED
@@ -1,83 +0,0 @@
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
- ```
@@ -1 +0,0 @@
1
- __version__ = "0.0.1"
File without changes
File without changes
File without changes