python-statemachine 2.3.5__tar.gz → 2.4.0__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.
Files changed (38) hide show
  1. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/PKG-INFO +9 -8
  2. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/README.md +7 -7
  3. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/pyproject.toml +22 -40
  4. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/__init__.py +3 -2
  5. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/callbacks.py +62 -10
  6. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/contrib/diagram.py +0 -4
  7. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/dispatcher.py +5 -3
  8. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/engines/async_.py +1 -0
  9. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/engines/sync.py +1 -0
  10. python_statemachine-2.4.0/statemachine/event.py +131 -0
  11. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/event_data.py +2 -1
  12. python_statemachine-2.4.0/statemachine/events.py +40 -0
  13. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/exceptions.py +3 -2
  14. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/factory.py +47 -12
  15. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/locale/en/LC_MESSAGES/statemachine.po +27 -15
  16. python_statemachine-2.4.0/statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +93 -0
  17. python_statemachine-2.4.0/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +93 -0
  18. python_statemachine-2.4.0/statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +93 -0
  19. python_statemachine-2.4.0/statemachine/spec_parser.py +79 -0
  20. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/state.py +7 -0
  21. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/statemachine.py +15 -23
  22. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/states.py +2 -2
  23. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/transition.py +1 -2
  24. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/transition_list.py +5 -1
  25. python_statemachine-2.3.5/statemachine/event.py +0 -53
  26. python_statemachine-2.3.5/statemachine/events.py +0 -31
  27. python_statemachine-2.3.5/statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +0 -91
  28. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/LICENSE +0 -0
  29. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/contrib/__init__.py +0 -0
  30. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/engines/__init__.py +0 -0
  31. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/graph.py +0 -0
  32. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/i18n.py +0 -0
  33. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/mixins.py +0 -0
  34. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/model.py +0 -0
  35. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/py.typed +0 -0
  36. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/registry.py +0 -0
  37. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/signature.py +0 -0
  38. {python_statemachine-2.3.5 → python_statemachine-2.4.0}/statemachine/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-statemachine
3
- Version: 2.3.5
3
+ Version: 2.4.0
4
4
  Summary: Python Finite State Machines made easy.
5
5
  Home-page: https://github.com/fgmacedo/python-statemachine
6
6
  License: MIT
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.12
23
23
  Classifier: Programming Language :: Python :: 3.13
24
24
  Classifier: Topic :: Software Development :: Libraries
25
25
  Provides-Extra: diagrams
26
+ Requires-Dist: pydot (>=2.0.0) ; (python_full_version > "3.8.0") and (extra == "diagrams")
26
27
  Description-Content-Type: text/markdown
27
28
 
28
29
  # Python StateMachine
@@ -404,7 +405,7 @@ There's a lot more to cover, please take a look at our docs:
404
405
  https://python-statemachine.readthedocs.io.
405
406
 
406
407
 
407
- ## Contributing to the project
408
+ ## Contributing
408
409
 
409
410
  * <a class="github-button" href="https://github.com/fgmacedo/python-statemachine" data-icon="octicon-star" aria-label="Star fgmacedo/python-statemachine on GitHub">Star this project</a>
410
411
  * <a class="github-button" href="https://github.com/fgmacedo/python-statemachine/issues" data-icon="octicon-issue-opened" aria-label="Issue fgmacedo/python-statemachine on GitHub">Open an Issue</a>
@@ -412,18 +413,18 @@ https://python-statemachine.readthedocs.io.
412
413
 
413
414
  - If you found this project helpful, please consider giving it a star on GitHub.
414
415
 
415
- - **Contribute code**: If you would like to contribute code to this project, please submit a pull
416
+ - **Contribute code**: If you would like to contribute code, please submit a pull
416
417
  request. For more information on how to contribute, please see our [contributing.md](contributing.md) file.
417
418
 
418
- - **Report bugs**: If you find any bugs in this project, please report them by opening an issue
419
+ - **Report bugs**: If you find any bugs, please report them by opening an issue
419
420
  on our GitHub issue tracker.
420
421
 
421
- - **Suggest features**: If you have a great idea for a new feature, please let us know by opening
422
- an issue on our GitHub issue tracker.
422
+ - **Suggest features**: If you have an idea for a new feature, of feels something being harder than it should be,
423
+ please let us know by opening an issue on our GitHub issue tracker.
423
424
 
424
- - **Documentation**: Help improve this project's documentation by submitting pull requests.
425
+ - **Documentation**: Help improve documentation by submitting pull requests.
425
426
 
426
- - **Promote the project**: Help spread the word about this project by sharing it on social media,
427
+ - **Promote the project**: Help spread the word by sharing on social media,
427
428
  writing a blog post, or giving a talk about it. Tag me on Twitter
428
429
  [@fgmacedo](https://twitter.com/fgmacedo) so I can share it too!
429
430
 
@@ -377,7 +377,7 @@ There's a lot more to cover, please take a look at our docs:
377
377
  https://python-statemachine.readthedocs.io.
378
378
 
379
379
 
380
- ## Contributing to the project
380
+ ## Contributing
381
381
 
382
382
  * <a class="github-button" href="https://github.com/fgmacedo/python-statemachine" data-icon="octicon-star" aria-label="Star fgmacedo/python-statemachine on GitHub">Star this project</a>
383
383
  * <a class="github-button" href="https://github.com/fgmacedo/python-statemachine/issues" data-icon="octicon-issue-opened" aria-label="Issue fgmacedo/python-statemachine on GitHub">Open an Issue</a>
@@ -385,17 +385,17 @@ https://python-statemachine.readthedocs.io.
385
385
 
386
386
  - If you found this project helpful, please consider giving it a star on GitHub.
387
387
 
388
- - **Contribute code**: If you would like to contribute code to this project, please submit a pull
388
+ - **Contribute code**: If you would like to contribute code, please submit a pull
389
389
  request. For more information on how to contribute, please see our [contributing.md](contributing.md) file.
390
390
 
391
- - **Report bugs**: If you find any bugs in this project, please report them by opening an issue
391
+ - **Report bugs**: If you find any bugs, please report them by opening an issue
392
392
  on our GitHub issue tracker.
393
393
 
394
- - **Suggest features**: If you have a great idea for a new feature, please let us know by opening
395
- an issue on our GitHub issue tracker.
394
+ - **Suggest features**: If you have an idea for a new feature, of feels something being harder than it should be,
395
+ please let us know by opening an issue on our GitHub issue tracker.
396
396
 
397
- - **Documentation**: Help improve this project's documentation by submitting pull requests.
397
+ - **Documentation**: Help improve documentation by submitting pull requests.
398
398
 
399
- - **Promote the project**: Help spread the word about this project by sharing it on social media,
399
+ - **Promote the project**: Help spread the word by sharing on social media,
400
400
  writing a blog post, or giving a talk about it. Tag me on Twitter
401
401
  [@fgmacedo](https://twitter.com/fgmacedo) so I can share it too!
@@ -1,21 +1,19 @@
1
1
  [tool.poetry]
2
2
  name = "python-statemachine"
3
- version = "2.3.5"
3
+ version = "2.4.0"
4
4
  description = "Python Finite State Machines made easy."
5
5
  authors = ["Fernando Macedo <fgmacedo@gmail.com>"]
6
- maintainers = [
7
- "Fernando Macedo <fgmacedo@gmail.com>",
8
- ]
6
+ maintainers = ["Fernando Macedo <fgmacedo@gmail.com>"]
9
7
  license = "MIT license"
10
8
  readme = "README.md"
11
9
  homepage = "https://github.com/fgmacedo/python-statemachine"
12
- packages = [
13
- {include = "statemachine"},
14
- {include = "statemachine/**/*.py" },
15
- ]
10
+ packages = [{ include = "statemachine" }, { include = "statemachine/**/*.py" }]
16
11
  include = [
17
12
  { path = "statemachine/locale/**/*.po", format = "sdist" },
18
- { path = "statemachine/locale/**/*.mo", format = ["sdist", "wheel"] }
13
+ { path = "statemachine/locale/**/*.mo", format = [
14
+ "sdist",
15
+ "wheel",
16
+ ] },
19
17
  ]
20
18
  classifiers = [
21
19
  "Intended Audience :: Developers",
@@ -33,14 +31,14 @@ classifiers = [
33
31
  "Intended Audience :: Developers",
34
32
  ]
35
33
 
36
- [tool.poetry.extras]
37
- diagrams = ["pydot"]
38
-
39
34
  [tool.poetry.dependencies]
40
35
  python = ">=3.7"
36
+ pydot = { version = ">=2.0.0", optional = true, python = ">3.8" }
37
+
38
+ [tool.poetry.extras]
39
+ diagrams = ["pydot"]
41
40
 
42
41
  [tool.poetry.group.dev.dependencies]
43
- pydot = "^2.0.0"
44
42
  ruff = "^0.4.8"
45
43
  pre-commit = "*"
46
44
  mypy = "*"
@@ -59,7 +57,7 @@ pytest-django = { version = "^4.8.0", python = ">3.8" }
59
57
  Sphinx = { version = "*", python = ">3.8" }
60
58
  myst-parser = { version = "*", python = ">3.8" }
61
59
  sphinx-gallery = { version = "*", python = ">3.8" }
62
- pillow = { version ="*", python = ">3.8" }
60
+ pillow = { version = "*", python = ">3.8" }
63
61
  sphinx-autobuild = { version = "*", python = ">3.8" }
64
62
  furo = { version = "^2024.5.6", python = ">3.8" }
65
63
  sphinx-copybutton = { version = "^0.5.2", python = ">3.8" }
@@ -72,9 +70,7 @@ build-backend = "poetry.core.masonry.api"
72
70
  addopts = "--ignore=docs/conf.py --ignore=docs/auto_examples/ --ignore=docs/_build/ --ignore=tests/examples/ --cov --cov-config .coveragerc --doctest-glob='*.md' --doctest-modules --doctest-continue-on-failure --benchmark-autosave --benchmark-group-by=name"
73
71
  doctest_optionflags = "ELLIPSIS IGNORE_EXCEPTION_DETAIL NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL"
74
72
  asyncio_mode = "auto"
75
- markers = [
76
- """slow: marks tests as slow (deselect with '-m "not slow"')""",
77
- ]
73
+ markers = ["""slow: marks tests as slow (deselect with '-m "not slow"')"""]
78
74
  python_files = ["tests.py", "test_*.py", "*_tests.py"]
79
75
 
80
76
  [tool.mypy]
@@ -85,19 +81,11 @@ disable_error_code = "annotation-unchecked"
85
81
  mypy_path = "$MYPY_CONFIG_FILE_DIR/tests/django_project"
86
82
 
87
83
  [[tool.mypy.overrides]]
88
- module = [
89
- 'django.*',
90
- 'pytest.*',
91
- 'pydot.*',
92
- 'sphinx_gallery.*',
93
- ]
84
+ module = ['django.*', 'pytest.*', 'pydot.*', 'sphinx_gallery.*']
94
85
  ignore_missing_imports = true
95
86
 
96
87
  [tool.flake8]
97
- ignore = [
98
- "E231",
99
- "W503",
100
- ]
88
+ ignore = ["E231", "W503"]
101
89
  max-line-length = 99
102
90
 
103
91
  [tool.ruff]
@@ -131,10 +119,10 @@ exclude = [
131
119
 
132
120
  # Enable Pyflakes and pycodestyle rules.
133
121
  select = [
134
- "E", # pycodestyle errors
135
- "W", # pycodestyle warnings
136
- "F", # pyflakes
137
- "I", # isort
122
+ "E", # pycodestyle errors
123
+ "W", # pycodestyle warnings
124
+ "F", # pyflakes
125
+ "I", # isort
138
126
  "UP", # pyupgrade
139
127
  "C", # flake8-comprehensions
140
128
  "B", # flake8-bugbear
@@ -169,14 +157,8 @@ convention = "google"
169
157
  branch = true
170
158
  relative_files = true
171
159
  data_file = ".coverage"
172
- source = [
173
- "statemachine",
174
- ]
175
- omit = [
176
- "*test*.py",
177
- "tmp/*",
178
- "pytest_cov",
179
- ]
160
+ source = ["statemachine"]
161
+ omit = ["*test*.py", "tmp/*", "pytest_cov"]
180
162
  [tool.coverage.report]
181
163
  show_missing = true
182
164
  exclude_lines = [
@@ -190,7 +172,7 @@ exclude_lines = [
190
172
  # Don't complain if tests don't hit defensive assertion code:
191
173
  "raise AssertionError",
192
174
  "raise NotImplementedError",
193
- "if TYPE_CHECKING",
175
+ "if TYPE_CHECKING",
194
176
  ]
195
177
 
196
178
  [tool.coverage.html]
@@ -1,8 +1,9 @@
1
+ from .event import Event
1
2
  from .state import State
2
3
  from .statemachine import StateMachine
3
4
 
4
5
  __author__ = """Fernando Macedo"""
5
6
  __email__ = "fgmacedo@gmail.com"
6
- __version__ = "2.3.5"
7
+ __version__ = "2.4.0"
7
8
 
8
- __all__ = ["StateMachine", "State"]
9
+ __all__ = ["StateMachine", "State", "Event"]
@@ -5,19 +5,30 @@ from collections import deque
5
5
  from enum import IntEnum
6
6
  from enum import IntFlag
7
7
  from enum import auto
8
+ from functools import partial
9
+ from functools import reduce
8
10
  from inspect import isawaitable
9
11
  from inspect import iscoroutinefunction
12
+ from typing import TYPE_CHECKING
10
13
  from typing import Callable
11
14
  from typing import Dict
12
15
  from typing import Generator
13
16
  from typing import Iterable
14
17
  from typing import List
18
+ from typing import Set
15
19
  from typing import Type
16
20
 
17
21
  from .exceptions import AttrNotFound
22
+ from .exceptions import InvalidDefinition
18
23
  from .i18n import _
24
+ from .spec_parser import custom_and
25
+ from .spec_parser import operator_mapping
26
+ from .spec_parser import parse_boolean_expr
19
27
  from .utils import ensure_iterable
20
28
 
29
+ if TYPE_CHECKING:
30
+ from statemachine.dispatcher import Listeners
31
+
21
32
 
22
33
  class CallbackPriority(IntEnum):
23
34
  GENERIC = 0
@@ -54,6 +65,17 @@ def allways_true(*args, **kwargs):
54
65
  return True
55
66
 
56
67
 
68
+ def take_callback(name: str, resolver: "Listeners", not_found_handler: Callable) -> Callable:
69
+ callbacks = list(resolver.search_name(name))
70
+ if len(callbacks) == 0:
71
+ not_found_handler(name)
72
+ return allways_true
73
+ elif len(callbacks) == 1:
74
+ return callbacks[0]
75
+ else:
76
+ return reduce(custom_and, callbacks)
77
+
78
+
57
79
  class CallbackSpec:
58
80
  """Specs about callbacks.
59
81
 
@@ -110,7 +132,16 @@ class CallbackSpec:
110
132
  self.reference = SpecReference.CALLABLE
111
133
  self.attr_name = attr_name
112
134
 
113
- def build(self, resolver) -> Generator["CallbackWrapper", None, None]:
135
+ def _wrap(self, callback):
136
+ condition = self.cond if self.cond is not None else allways_true
137
+ return CallbackWrapper(
138
+ callback=callback,
139
+ condition=condition,
140
+ meta=self,
141
+ unique_key=callback.unique_key,
142
+ )
143
+
144
+ def build(self, resolver: "Listeners") -> Generator["CallbackWrapper", None, None]:
114
145
  """
115
146
  Resolves the `func` into a usable callable.
116
147
 
@@ -118,14 +149,29 @@ class CallbackSpec:
118
149
  resolver (callable): A method responsible to build and return a valid callable that
119
150
  can receive arbitrary parameters like `*args, **kwargs`.
120
151
  """
121
- for callback in resolver.search(self):
122
- condition = self.cond if self.cond is not None else allways_true
123
- yield CallbackWrapper(
124
- callback=callback,
125
- condition=condition,
126
- meta=self,
127
- unique_key=callback.unique_key,
152
+ if (
153
+ not self.is_convention
154
+ and self.group == CallbackGroup.COND
155
+ and self.reference == SpecReference.NAME
156
+ ):
157
+ names_not_found: Set[str] = set()
158
+ take_callback_partial = partial(
159
+ take_callback, resolver=resolver, not_found_handler=names_not_found.add
128
160
  )
161
+ try:
162
+ expression = parse_boolean_expr(self.func, take_callback_partial, operator_mapping)
163
+ except SyntaxError as err:
164
+ raise InvalidDefinition(
165
+ _("Failed to parse boolean expression '{}'").format(self.func)
166
+ ) from err
167
+ if not expression or names_not_found:
168
+ self.names_not_found = names_not_found
169
+ return
170
+ yield self._wrap(expression)
171
+ return
172
+
173
+ for callback in resolver.search(self):
174
+ yield self._wrap(callback)
129
175
 
130
176
 
131
177
  class SpecListGrouper:
@@ -292,7 +338,7 @@ class CallbacksExecutor:
292
338
  def __str__(self):
293
339
  return ", ".join(str(c) for c in self)
294
340
 
295
- def _add(self, spec: CallbackSpec, resolver: Callable):
341
+ def _add(self, spec: CallbackSpec, resolver: "Listeners"):
296
342
  for callback in spec.build(resolver):
297
343
  if callback.unique_key in self.items_already_seen:
298
344
  continue
@@ -300,7 +346,7 @@ class CallbacksExecutor:
300
346
  self.items_already_seen.add(callback.unique_key)
301
347
  insort(self.items, callback)
302
348
 
303
- def add(self, items: Iterable[CallbackSpec], resolver: Callable):
349
+ def add(self, items: Iterable[CallbackSpec], resolver: "Listeners"):
304
350
  """Validate configurations"""
305
351
  for item in items:
306
352
  self._add(item, resolver)
@@ -356,6 +402,12 @@ class CallbacksRegistry:
356
402
  callback for callback in self[meta.group.build_key(specs)] if callback.meta == meta
357
403
  ):
358
404
  continue
405
+ if hasattr(meta, "names_not_found"):
406
+ raise AttrNotFound(
407
+ _("Did not found name '{}' from model or statemachine").format(
408
+ ", ".join(meta.names_not_found)
409
+ ),
410
+ )
359
411
  raise AttrNotFound(
360
412
  _("Did not found name '{}' from model or statemachine").format(meta.func)
361
413
  )
@@ -166,10 +166,6 @@ def quickchart_write_svg(sm: StateMachine, path: str):
166
166
  >>> sm = OrderControl()
167
167
  >>> print(sm._graph().to_string())
168
168
  digraph list {
169
- fontname=Arial;
170
- fontsize=10;
171
- label=OrderControl;
172
- rankdir=LR;
173
169
  ...
174
170
 
175
171
  To give you an example, we included this method that will serialize the dot, request the graph
@@ -10,6 +10,7 @@ from typing import Tuple
10
10
 
11
11
  from .callbacks import SPECS_ALL
12
12
  from .callbacks import SpecReference
13
+ from .event import Event
13
14
  from .signature import SignatureAdapter
14
15
 
15
16
  if TYPE_CHECKING:
@@ -75,7 +76,7 @@ class Listeners:
75
76
 
76
77
  def search(self, spec: "CallbackSpec") -> Generator["Callable", None, None]:
77
78
  if spec.reference is SpecReference.NAME:
78
- yield from self._search_name(spec.func)
79
+ yield from self.search_name(spec.func)
79
80
  return
80
81
  elif spec.reference is SpecReference.CALLABLE:
81
82
  yield self._search_callable(spec)
@@ -111,7 +112,7 @@ class Listeners:
111
112
 
112
113
  return callable_method(spec.attr_name, spec.func, None)
113
114
 
114
- def _search_name(self, name) -> Generator["Callable", None, None]:
115
+ def search_name(self, name) -> Generator["Callable", None, None]:
115
116
  for config in self.items:
116
117
  if name not in config.all_attrs:
117
118
  continue
@@ -121,7 +122,7 @@ class Listeners:
121
122
  yield attr_method(name, config.obj, config.resolver_id)
122
123
  continue
123
124
 
124
- if getattr(func, "_is_sm_event", False):
125
+ if isinstance(func, Event):
125
126
  yield event_method(name, func, config.resolver_id)
126
127
  continue
127
128
 
@@ -143,6 +144,7 @@ def attr_method(attribute, obj, resolver_id) -> Callable:
143
144
  return getter(obj)
144
145
 
145
146
  method.unique_key = f"{attribute}@{resolver_id}" # type: ignore[attr-defined]
147
+ method.__name__ = attribute
146
148
  return method
147
149
 
148
150
 
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
15
15
 
16
16
  class AsyncEngine:
17
17
  def __init__(self, sm: "StateMachine", rtc: bool = True):
18
+ sm._engine = self
18
19
  self.sm = proxy(sm)
19
20
  self._sentinel = object()
20
21
  if not rtc:
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
13
13
 
14
14
  class SyncEngine:
15
15
  def __init__(self, sm: "StateMachine", rtc: bool = True):
16
+ sm._engine = self
16
17
  self.sm = proxy(sm)
17
18
  self._sentinel = object()
18
19
  self._rtc = rtc
@@ -0,0 +1,131 @@
1
+ from inspect import isawaitable
2
+ from typing import TYPE_CHECKING
3
+ from typing import List
4
+ from uuid import uuid4
5
+
6
+ from statemachine.utils import run_async_from_sync
7
+
8
+ from .event_data import TriggerData
9
+
10
+ if TYPE_CHECKING:
11
+ from .statemachine import StateMachine
12
+ from .transition_list import TransitionList
13
+
14
+
15
+ _event_data_kwargs = {
16
+ "event_data",
17
+ "machine",
18
+ "event",
19
+ "model",
20
+ "transition",
21
+ "state",
22
+ "source",
23
+ "target",
24
+ }
25
+
26
+
27
+ class Event(str):
28
+ """An event is triggers a signal that something has happened.
29
+
30
+ They are send to a state machine and allow the state machine to react.
31
+
32
+ An event starts a :ref:`Transition`, which can be thought of as a “cause” that initiates a
33
+ change in the state of the system.
34
+
35
+ See also :ref:`events`.
36
+ """
37
+
38
+ id: str
39
+ """The event identifier."""
40
+
41
+ name: str
42
+ """The event name."""
43
+
44
+ _sm: "StateMachine | None" = None
45
+ """The state machine instance."""
46
+
47
+ _transitions: "TransitionList | None" = None
48
+ _has_real_id = False
49
+
50
+ def __new__(
51
+ cls,
52
+ transitions: "str | TransitionList | None" = None,
53
+ id: "str | None" = None,
54
+ name: "str | None" = None,
55
+ _sm: "StateMachine | None" = None,
56
+ ):
57
+ if isinstance(transitions, str):
58
+ id = transitions
59
+ transitions = None
60
+
61
+ _has_real_id = id is not None
62
+ id = str(id) if _has_real_id else f"__event__{uuid4().hex}"
63
+
64
+ instance = super().__new__(cls, id)
65
+ instance.id = id
66
+ if name:
67
+ instance.name = name
68
+ elif _has_real_id:
69
+ instance.name = str(id).replace("_", " ").capitalize()
70
+ else:
71
+ instance.name = ""
72
+ if transitions:
73
+ instance._transitions = transitions
74
+ instance._has_real_id = _has_real_id
75
+ instance._sm = _sm
76
+ return instance
77
+
78
+ def __repr__(self):
79
+ return f"{type(self).__name__}({self.id!r})"
80
+
81
+ def is_same_event(self, *_args, event: "str | None" = None, **_kwargs) -> bool:
82
+ return self == event
83
+
84
+ def __get__(self, instance, owner):
85
+ """By implementing this method `Event` can be used as a property descriptor
86
+
87
+ When attached to a SM class, if the user tries to get the `Event` instance,
88
+ we intercept here and return a `BoundEvent` instance, so the user can call
89
+ it as a method with the correct SM instance.
90
+
91
+ """
92
+ if instance is None:
93
+ return self
94
+ return BoundEvent(id=self.id, name=self.name, _sm=instance)
95
+
96
+ def __call__(self, *args, **kwargs):
97
+ """Send this event to the current state machine.
98
+
99
+ Triggering an event on a state machine means invoking or sending a signal, initiating the
100
+ process that may result in executing a transition.
101
+ """
102
+ # The `__call__` is declared here to help IDEs knowing that an `Event`
103
+ # can be called as a method. But it is not meant to be called without
104
+ # an SM instance. Such SM instance is provided by `__get__` method when
105
+ # used as a property descriptor.
106
+
107
+ machine = self._sm
108
+ kwargs = {k: v for k, v in kwargs.items() if k not in _event_data_kwargs}
109
+ trigger_data = TriggerData(
110
+ machine=machine,
111
+ event=self,
112
+ args=args,
113
+ kwargs=kwargs,
114
+ )
115
+ machine._put_nonblocking(trigger_data)
116
+ result = machine._processing_loop()
117
+ if not isawaitable(result):
118
+ return result
119
+ return run_async_from_sync(result)
120
+
121
+ def split( # type: ignore[override]
122
+ self, sep: "str | None" = None, maxsplit: int = -1
123
+ ) -> List["Event"]:
124
+ result = super().split(sep, maxsplit)
125
+ if len(result) == 1:
126
+ return [self]
127
+ return [Event(event) for event in result]
128
+
129
+
130
+ class BoundEvent(Event):
131
+ pass
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
4
4
  from typing import Any
5
5
 
6
6
  if TYPE_CHECKING:
7
+ from .event import Event
7
8
  from .state import State
8
9
  from .statemachine import StateMachine
9
10
  from .transition import Transition
@@ -13,7 +14,7 @@ if TYPE_CHECKING:
13
14
  class TriggerData:
14
15
  machine: "StateMachine"
15
16
 
16
- event: str
17
+ event: "Event"
17
18
  """The Event that was triggered."""
18
19
 
19
20
  model: Any = field(init=False)
@@ -0,0 +1,40 @@
1
+ from statemachine.event import Event
2
+
3
+ from .utils import ensure_iterable
4
+
5
+
6
+ class Events:
7
+ """A collection of event names."""
8
+
9
+ def __init__(self):
10
+ self._items: list[Event] = []
11
+
12
+ def __repr__(self):
13
+ sep = " " if len(self._items) > 1 else ""
14
+ return sep.join(item for item in self._items)
15
+
16
+ def __iter__(self):
17
+ return iter(self._items)
18
+
19
+ def add(self, events):
20
+ if events is None:
21
+ return self
22
+
23
+ unprepared = ensure_iterable(events)
24
+ for events in unprepared:
25
+ for event in events.split(" "):
26
+ if event in self._items:
27
+ continue
28
+ if isinstance(event, Event):
29
+ self._items.append(event)
30
+ else:
31
+ self._items.append(Event(id=event, name=event))
32
+
33
+ return self
34
+
35
+ def match(self, event: str):
36
+ return any(e == event for e in self)
37
+
38
+ def _replace(self, old, new):
39
+ self._items.remove(old)
40
+ self._items.append(new)
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
3
3
  from .i18n import _
4
4
 
5
5
  if TYPE_CHECKING:
6
+ from .event import Event
6
7
  from .state import State
7
8
 
8
9
 
@@ -31,8 +32,8 @@ class AttrNotFound(InvalidDefinition):
31
32
  class TransitionNotAllowed(StateMachineError):
32
33
  "Raised when there's no transition that can run from the current :ref:`state`."
33
34
 
34
- def __init__(self, event: str, state: "State"):
35
+ def __init__(self, event: "Event", state: "State"):
35
36
  self.event = event
36
37
  self.state = state
37
- msg = _("Can't {} when in {}.").format(self.event, self.state.name)
38
+ msg = _("Can't {} when in {}.").format(self.event.name, self.state.name)
38
39
  super().__init__(msg)