python-statemachine 3.1.1__py3-none-any.whl → 3.2.0__py3-none-any.whl

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 (60) hide show
  1. {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/METADATA +16 -3
  2. python_statemachine-3.2.0.dist-info/RECORD +72 -0
  3. {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/WHEEL +1 -1
  4. statemachine/__init__.py +1 -1
  5. statemachine/callbacks.py +5 -11
  6. statemachine/configuration.py +5 -6
  7. statemachine/contrib/diagram/__init__.py +15 -6
  8. statemachine/contrib/diagram/extract.py +23 -24
  9. statemachine/contrib/diagram/formatter.py +5 -7
  10. statemachine/contrib/diagram/model.py +9 -11
  11. statemachine/contrib/diagram/renderers/dot.py +20 -26
  12. statemachine/contrib/diagram/renderers/mermaid.py +36 -40
  13. statemachine/contrib/diagram/renderers/table.py +7 -9
  14. statemachine/contrib/weighted.py +7 -11
  15. statemachine/dispatcher.py +13 -12
  16. statemachine/engines/async_.py +5 -6
  17. statemachine/engines/base.py +12 -14
  18. statemachine/event.py +1 -2
  19. statemachine/exceptions.py +1 -1
  20. statemachine/factory.py +12 -16
  21. statemachine/graph.py +2 -2
  22. statemachine/invoke.py +12 -11
  23. statemachine/io/__init__.py +45 -225
  24. statemachine/io/{scxml/actions.py → actions.py} +158 -288
  25. statemachine/io/builder.py +195 -0
  26. statemachine/io/class_factory.py +236 -0
  27. statemachine/io/evaluators.py +275 -0
  28. statemachine/io/interpreter.py +128 -0
  29. statemachine/io/{scxml/invoke.py → invoke.py} +77 -49
  30. statemachine/io/json/__init__.py +1 -0
  31. statemachine/io/json/reader.py +27 -0
  32. statemachine/io/loader.py +161 -0
  33. statemachine/io/model.py +268 -0
  34. statemachine/io/native.py +402 -0
  35. statemachine/io/ports.py +83 -0
  36. statemachine/io/schemas/statechart.schema.json +258 -0
  37. statemachine/io/scxml/__init__.py +12 -0
  38. statemachine/io/scxml/processor.py +23 -253
  39. statemachine/io/scxml/{parser.py → reader.py} +64 -47
  40. statemachine/io/system_variables.py +184 -0
  41. statemachine/io/validation.py +44 -0
  42. statemachine/io/yaml/__init__.py +1 -0
  43. statemachine/io/yaml/reader.py +65 -0
  44. statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
  45. statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
  46. statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
  47. statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +20 -22
  48. statemachine/orderedset.py +3 -3
  49. statemachine/registry.py +1 -4
  50. statemachine/signature.py +2 -5
  51. statemachine/spec_parser.py +171 -42
  52. statemachine/state.py +5 -6
  53. statemachine/statemachine.py +18 -20
  54. statemachine/states.py +3 -5
  55. statemachine/transition.py +3 -4
  56. statemachine/transition_list.py +4 -5
  57. statemachine/transition_mixin.py +1 -1
  58. python_statemachine-3.1.1.dist-info/RECORD +0 -58
  59. statemachine/io/scxml/schema.py +0 -175
  60. {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-statemachine
3
- Version: 3.1.1
3
+ Version: 3.2.0
4
4
  Summary: Python Finite State Machines made easy.
5
5
  Project-URL: homepage, https://github.com/fgmacedo/python-statemachine
6
6
  Author-email: Fernando Macedo <fgmacedo@gmail.com>
@@ -13,7 +13,6 @@ Classifier: Framework :: Django
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Natural Language :: English
16
- Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
@@ -21,9 +20,16 @@ Classifier: Programming Language :: Python :: 3.13
21
20
  Classifier: Programming Language :: Python :: 3.14
22
21
  Classifier: Topic :: Home Automation
23
22
  Classifier: Topic :: Software Development :: Libraries
24
- Requires-Python: >=3.9
23
+ Requires-Python: >=3.10
25
24
  Provides-Extra: diagrams
26
25
  Requires-Dist: pydot>=4.0.1; extra == 'diagrams'
26
+ Provides-Extra: io
27
+ Requires-Dist: jsonschema>=4.18; extra == 'io'
28
+ Requires-Dist: pyyaml>=6.0; extra == 'io'
29
+ Provides-Extra: validation
30
+ Requires-Dist: jsonschema>=4.18; extra == 'validation'
31
+ Provides-Extra: yaml
32
+ Requires-Dist: pyyaml>=6.0; extra == 'yaml'
27
33
  Description-Content-Type: text/markdown
28
34
 
29
35
  # Python StateMachine
@@ -400,6 +406,13 @@ To generate diagrams, install with the `diagrams` extra (requires
400
406
  pip install python-statemachine[diagrams]
401
407
  ```
402
408
 
409
+ To load statecharts from declarative documents, install the IO extras
410
+ (`yaml` for YAML, `validation` for `validate=True`, or `io` for both):
411
+
412
+ ```
413
+ pip install python-statemachine[io]
414
+ ```
415
+
403
416
 
404
417
  ## Contributing
405
418
 
@@ -0,0 +1,72 @@
1
+ statemachine/__init__.py,sha256=QiHL_KGtaPxEuPtKCpwb6DssApC1jcalKYrkfNgMKc4,445
2
+ statemachine/callbacks.py,sha256=LKX9GnDhN64yYh2EvDEYNJg0-N61Kmyd0aqTZKxujM4,14526
3
+ statemachine/configuration.py,sha256=MblvaBwEOyooYc4Vzo-qeryF9sEmx9mdtZDY0Gf_uRU,5991
4
+ statemachine/dispatcher.py,sha256=DyRqwgr7gXom312BIPsNZiueq5n8jNp1dBDExm7mG_g,8109
5
+ statemachine/event.py,sha256=hYVvsNpsY0-OSebwDEpxdLXadoJS_G5pmDe34hRqnFk,7645
6
+ statemachine/event_data.py,sha256=7_hoE0cp-0uW71Kk3erO3eYM6anTT5X3slGMcehIGIo,3111
7
+ statemachine/events.py,sha256=11GyX8GYpsSwvW0v1b4zHUGhzKRoqNjQYtuDj8NvqOM,1212
8
+ statemachine/exceptions.py,sha256=N6iTrin-uMyw31mbXbO6FUj0V5WGvfKaNZe6L2pV0IE,1314
9
+ statemachine/factory.py,sha256=qIgDxBwa5xa0lgvTaV4QPqdYqqkXZCzlSq0sX4TjZvo,14640
10
+ statemachine/graph.py,sha256=ngQ31YAetfQLQvdBU5cHwLQcD-wJL92axPJ2sUeCu1g,1999
11
+ statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
12
+ statemachine/invoke.py,sha256=5B7XNvBFnQaKByoYM1VbRlJvJyEwXn1C0QKUCaRjmpU,24908
13
+ statemachine/mixins.py,sha256=8qxZZfBwVdFcr3oPFVWHGzgmAubH6VgQus5x3c6VEE0,1706
14
+ statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
15
+ statemachine/orderedset.py,sha256=7y4Hu6vVD0LHTSJ5iTB9F-faRw0326EubR3AOrpZAHE,2570
16
+ statemachine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ statemachine/registry.py,sha256=TmHfVU7HgaG43slG5lh8EL-8237Fm8r2UkwDj8u-t0c,700
18
+ statemachine/signature.py,sha256=-IiRYJScx4cDKny3ictQvHwQBbaCyxWpRKSgQksMT0E,9834
19
+ statemachine/spec_parser.py,sha256=fjiUjtzTAk6UliNsdbeInqrnp38cfHeYVlgMrbqrzxc,13450
20
+ statemachine/state.py,sha256=kSHeJh1BlSpeDPxMKCCpwIJqzx4MPdXYZ1S24kki4os,15426
21
+ statemachine/statemachine.py,sha256=27TRypjsh3lwlAGZAwqrt-Cnka65uBoaxMWrU27gg6M,19941
22
+ statemachine/states.py,sha256=NRHtATtb-2Ac2nwzSn5OfHwzOSxI2FLeNR8o1v84USs,4827
23
+ statemachine/transition.py,sha256=JtGABHJ6inZrneI70bgu4ir2BZ2YQagcYX9l6TcsHSc,6912
24
+ statemachine/transition_list.py,sha256=zZuGWB86fL2ux-orIE7CdeCcnhaS5p2njIy8h2SuyVY,4361
25
+ statemachine/transition_mixin.py,sha256=i9ZLDGPgE2zIkTn7IAEMjscn1gnI-HJMtxX-YG447tY,3002
26
+ statemachine/utils.py,sha256=taCgrsRDcQQLweyq607AVZyI0NMukRP-HGpO8TiVNgk,1527
27
+ statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ statemachine/contrib/timeout.py,sha256=VXkdLpxNeUKihkbtLv8XTWdG-meQlSFtxJfcxXgV8Uw,2251
29
+ statemachine/contrib/weighted.py,sha256=lzcopcY0EzjJb-N4APJJc7AlgZIPtthTbBizKaDtOIk,6787
30
+ statemachine/contrib/diagram/__init__.py,sha256=mvTluFDnhE2OzTeOhyU7aQebl79LIOsvOiNFAVIldww,7730
31
+ statemachine/contrib/diagram/__main__.py,sha256=iPpiz09xKqtAjrhONS99OYp6R2dQ6Anbhw1qPIN8ELo,80
32
+ statemachine/contrib/diagram/extract.py,sha256=oNy67KToH7zxgEIPavOvgbpoYTSkM3iWB-Zas1_lcqY,9854
33
+ statemachine/contrib/diagram/formatter.py,sha256=lYdBGO3VSZnM9OipyNsuA6StMEEjrRdnksm9MmzKXBA,4724
34
+ statemachine/contrib/diagram/model.py,sha256=pn7dzrt8U8TOn4A-Jg1Rc3b1UUlTgs75WfR2x-xtEuc,1451
35
+ statemachine/contrib/diagram/sphinx_ext.py,sha256=HcEXU-aYz8da02XPkZKpF5HDF0XPBGGg9FCz8MtI1C8,10338
36
+ statemachine/contrib/diagram/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ statemachine/contrib/diagram/renderers/dot.py,sha256=VkWMVP1DLl1YUEUZVmBVSNQVbAG516v3hQcK4PePWD8,19772
38
+ statemachine/contrib/diagram/renderers/mermaid.py,sha256=3N1z0klonL768exBcWp3WLMuWUgCpRso2UhpdnvHYhY,13424
39
+ statemachine/contrib/diagram/renderers/table.py,sha256=PLVfwTIZHtv7uK3smAfN1owAOEunkBRHyEkh5ygVcfw,3711
40
+ statemachine/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ statemachine/engines/async_.py,sha256=_n7x5IDu6bucBjE7PFdHBzYwrEcPd-iRxDfMc4FE9HI,22537
42
+ statemachine/engines/base.py,sha256=kAgpDu8iTejPBXp_4casK_rQuHkL_G3Njph8iuXzDiw,37954
43
+ statemachine/engines/sync.py,sha256=F_bt4z1KLLcFCP77E9SNWDJMWiMTexE0iOU2XAd_x5A,9153
44
+ statemachine/io/__init__.py,sha256=i5rGVI1ETu1hH0W8qtQMqK0WrFCYLPdV-s5VXh9vOSo,2062
45
+ statemachine/io/actions.py,sha256=FmRjPFJkcQwtxSEaOXbvu3S5vETai2j5r8mC_4eZ8Og,18624
46
+ statemachine/io/builder.py,sha256=yNaQrtFfbs55uRd-loZ-Vs5061liwM-np-B_2ZuJO1I,8350
47
+ statemachine/io/class_factory.py,sha256=H8zxsfrjLw7L_X2g2hoXb2CjpHxTftsoBmPBWIcen-4,9557
48
+ statemachine/io/evaluators.py,sha256=tMcovIr6dhysLRaxUjYbNmPu3jvOzpHOZDjevUnGEMU,10640
49
+ statemachine/io/interpreter.py,sha256=FQUXUlt1PSPzFHyixiLNXcN-7KYBM_i5RAFPRPLu6TE,5567
50
+ statemachine/io/invoke.py,sha256=wbeSJSlDLEvS7hXSznpMT7wd8GDcVsQSXGgFr67zVrI,9877
51
+ statemachine/io/loader.py,sha256=GbCF0MoLxcRa84zZOZPsJpdIUeodFGcsFoWE_Wk4bqQ,6442
52
+ statemachine/io/model.py,sha256=mTJvlbDJndX78pHfyDdEaqc9zTBWVA-OFA5xzVZbz6Y,8695
53
+ statemachine/io/native.py,sha256=HjIOKDQIGExLMglcm8VGbga71Y1Zw-pBbKYWP-y_oiY,13758
54
+ statemachine/io/ports.py,sha256=XoIAQfMujkEnH38t4iu766yiHycb89TNrA3owtBJU2U,2956
55
+ statemachine/io/system_variables.py,sha256=7skuJgBL8giqzaNEyWd783xOZtVRSUHF6ETwt9fVcd4,6223
56
+ statemachine/io/validation.py,sha256=6qiDx0gMkPAehSxJu_mrW4nEF_sebHwa5Dr_njclRcw,1662
57
+ statemachine/io/json/__init__.py,sha256=tFWoOrD9RuKEqkOpI0XifNRP7rguSGY4k-RaSCV_GnM,38
58
+ statemachine/io/json/reader.py,sha256=40V5T-yB1i0Qg-MkK00KHRi7eDQQk4ajfzmKtKXylHs,789
59
+ statemachine/io/schemas/statechart.schema.json,sha256=5yNIS4M7OV6GE2doAWMk_Jct-qDyb5kQ-Dq95Es9KbM,7862
60
+ statemachine/io/scxml/__init__.py,sha256=iGS763p9fZ6A7NVuEVpDVOS303q0dr5N4kVfOZ37jsk,660
61
+ statemachine/io/scxml/processor.py,sha256=Ru_YQAA1OVXNS9AqfN2myL5POq-C4kI1Vpef-eO7k2M,1532
62
+ statemachine/io/scxml/reader.py,sha256=CNUI9ARDJswRZDKqJSBY98Uoa5rQJsIU-kS9Uu5cmzg,17057
63
+ statemachine/io/yaml/__init__.py,sha256=oMapUmquV7hb5qIGMfjDZmDHNq2fqqjJw6LOgGNKeC4,79
64
+ statemachine/io/yaml/reader.py,sha256=mf2FaBttxQuCniSM-vHhSYDGaNLQdzRLoMUHO2kZ0xA,2208
65
+ statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=_HrrNLQK1krOb56x5Pf1S6LihwaPwhN9_T0PUvumRA0,5338
66
+ statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po,sha256=O15jEhCNATAu6O2WhQ9RhQISB6Ueje2xbbTfEotnVSc,7948
67
+ statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=X6-fU4JfK7VPFCRuLQ-4J-YUMF9N_RwhZ1DdCUvn74Y,5814
68
+ statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po,sha256=0_54wD7bl3RBMlYyokNFUdgMCx_1MbKzclxBUwmCNEs,5363
69
+ python_statemachine-3.2.0.dist-info/METADATA,sha256=Vdy46mBCvmA04ruIBaTS9XklmK72ip3yycmrzgCWu48,12642
70
+ python_statemachine-3.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
71
+ python_statemachine-3.2.0.dist-info/licenses/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
72
+ python_statemachine-3.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
statemachine/__init__.py CHANGED
@@ -8,7 +8,7 @@ from .statemachine import TModel
8
8
 
9
9
  __author__ = """Fernando Macedo"""
10
10
  __email__ = "fgmacedo@gmail.com"
11
- __version__ = "3.1.1"
11
+ __version__ = "3.2.0"
12
12
 
13
13
  __all__ = [
14
14
  "StateChart",
statemachine/callbacks.py CHANGED
@@ -2,23 +2,17 @@ import asyncio
2
2
  from bisect import insort
3
3
  from collections import defaultdict
4
4
  from collections import deque
5
+ from collections.abc import Callable
5
6
  from enum import IntEnum
6
7
  from enum import IntFlag
7
8
  from enum import auto
8
9
  from functools import partial
9
10
  from inspect import isawaitable
10
- from typing import TYPE_CHECKING
11
- from typing import Callable
12
- from typing import Dict
13
- from typing import List
14
11
 
15
12
  from .exceptions import AttrNotFound
16
13
  from .i18n import _
17
14
  from .utils import ensure_iterable
18
15
 
19
- if TYPE_CHECKING:
20
- from typing import Set
21
-
22
16
 
23
17
  def allways_true(*args, **kwargs):
24
18
  return True
@@ -66,7 +60,7 @@ class CallbackSpec:
66
60
  before any real call is performed.
67
61
  """
68
62
 
69
- names_not_found: "Set[str] | None" = None
63
+ names_not_found: "set[str] | None" = None
70
64
  """List of names that were not found on the model or statemachine"""
71
65
 
72
66
  def __init__(
@@ -155,9 +149,9 @@ class CallbackSpecList:
155
149
  """List of {ref}`CallbackSpec` instances"""
156
150
 
157
151
  def __init__(self, factory=CallbackSpec):
158
- self.items: List[CallbackSpec] = []
152
+ self.items: list[CallbackSpec] = []
159
153
  self.conventional_specs = set()
160
- self._groupers: Dict[CallbackGroup, SpecListGrouper] = {}
154
+ self._groupers: dict[CallbackGroup, SpecListGrouper] = {}
161
155
  self.factory = factory
162
156
 
163
157
  def __repr__(self):
@@ -380,7 +374,7 @@ class CallbacksExecutor:
380
374
 
381
375
  class CallbacksRegistry:
382
376
  def __init__(self) -> None:
383
- self._registry: Dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
377
+ self._registry: dict[str, CallbacksExecutor] = defaultdict(CallbacksExecutor)
384
378
  self.has_async_callbacks: bool = False
385
379
 
386
380
  def __getitem__(self, key: str) -> CallbacksExecutor:
@@ -1,8 +1,7 @@
1
+ from collections.abc import Mapping
2
+ from collections.abc import MutableSet
1
3
  from typing import TYPE_CHECKING
2
4
  from typing import Any
3
- from typing import Dict
4
- from typing import Mapping
5
- from typing import MutableSet
6
5
 
7
6
  from .exceptions import InvalidStateValue
8
7
  from .i18n import _
@@ -34,10 +33,10 @@ class Configuration:
34
33
 
35
34
  def __init__(
36
35
  self,
37
- instance_states: "Mapping[str, State]",
36
+ instance_states: "Mapping[Any, State]",
38
37
  model: Any,
39
38
  state_field: str,
40
- states_map: "Dict[Any, State]",
39
+ states_map: "dict[Any, State]",
41
40
  ):
42
41
  self._instance_states = instance_states
43
42
  self._model = model
@@ -85,7 +84,7 @@ class Configuration:
85
84
 
86
85
  # Normalize inline (avoid second getattr via _read_from_model)
87
86
  values = raw if isinstance(raw, MutableSet) else (raw,)
88
- result = OrderedSet(self._instance_states[self._states_map[v].id] for v in values)
87
+ result = OrderedSet(self._instance_states[v] for v in values)
89
88
  self._cached = result
90
89
  self._cached_value = raw
91
90
  return result
@@ -1,13 +1,14 @@
1
1
  import importlib
2
+ from typing import TYPE_CHECKING
2
3
  from urllib.parse import quote
3
4
  from urllib.request import urlopen
4
5
 
5
6
  from .extract import extract
6
7
  from .formatter import formatter as formatter
7
- from .renderers.dot import DotRenderer
8
- from .renderers.dot import DotRendererConfig
9
- from .renderers.mermaid import MermaidRenderer
10
- from .renderers.mermaid import MermaidRendererConfig
8
+
9
+ if TYPE_CHECKING:
10
+ from .renderers.dot import DotRendererConfig
11
+ from .renderers.mermaid import MermaidRendererConfig
11
12
 
12
13
 
13
14
  class DotGraphMachine:
@@ -40,7 +41,9 @@ class DotGraphMachine:
40
41
  def __init__(self, machine):
41
42
  self.machine = machine
42
43
 
43
- def _build_config(self) -> DotRendererConfig:
44
+ def _build_config(self) -> "DotRendererConfig":
45
+ from .renderers.dot import DotRendererConfig
46
+
44
47
  return DotRendererConfig(
45
48
  graph_rankdir=self.graph_rankdir,
46
49
  font_name=self.font_name,
@@ -51,6 +54,8 @@ class DotGraphMachine:
51
54
  )
52
55
 
53
56
  def get_graph(self):
57
+ from .renderers.dot import DotRenderer
58
+
54
59
  ir = extract(self.machine)
55
60
  renderer = DotRenderer(config=self._build_config())
56
61
  return renderer.render(ir)
@@ -69,7 +74,9 @@ class MermaidGraphMachine:
69
74
  def __init__(self, machine):
70
75
  self.machine = machine
71
76
 
72
- def _build_config(self) -> MermaidRendererConfig:
77
+ def _build_config(self) -> "MermaidRendererConfig":
78
+ from .renderers.mermaid import MermaidRendererConfig
79
+
73
80
  return MermaidRendererConfig(
74
81
  direction=self.direction,
75
82
  active_fill=self.active_fill,
@@ -77,6 +84,8 @@ class MermaidGraphMachine:
77
84
  )
78
85
 
79
86
  def get_mermaid(self) -> str:
87
+ from .renderers.mermaid import MermaidRenderer
88
+
80
89
  ir = extract(self.machine)
81
90
  renderer = MermaidRenderer(config=self._build_config())
82
91
  return renderer.render(ir)
@@ -1,7 +1,4 @@
1
1
  from typing import TYPE_CHECKING
2
- from typing import List
3
- from typing import Set
4
- from typing import Union
5
2
 
6
3
  from .model import ActionType
7
4
  from .model import DiagramAction
@@ -11,12 +8,14 @@ from .model import DiagramTransition
11
8
  from .model import StateType
12
9
 
13
10
  if TYPE_CHECKING:
11
+ from typing import TypeAlias
12
+
14
13
  from statemachine.state import State
15
14
  from statemachine.statemachine import StateChart
16
15
  from statemachine.transition import Transition
17
16
 
18
17
  # A StateChart class or instance — both expose the same structural metadata.
19
- MachineRef = Union["StateChart", "type[StateChart]"]
18
+ MachineRef: TypeAlias = StateChart | type[StateChart]
20
19
 
21
20
 
22
21
  def _determine_state_type(state: "State") -> StateType:
@@ -50,8 +49,8 @@ def _actions_getter(machine: "MachineRef"):
50
49
  return getter
51
50
 
52
51
 
53
- def _extract_state_actions(state: "State", getter) -> List[DiagramAction]:
54
- actions: List[DiagramAction] = []
52
+ def _extract_state_actions(state: "State", getter) -> list[DiagramAction]:
53
+ actions: list[DiagramAction] = []
55
54
 
56
55
  entry = str(getter(state.enter))
57
56
  exit_ = str(getter(state.exit))
@@ -82,7 +81,7 @@ def _extract_state(
82
81
  is_active = state.value in active_values
83
82
  is_parallel_area = bool(state.parent and getattr(state.parent, "parallel", False))
84
83
 
85
- children: List[DiagramState] = []
84
+ children: list[DiagramState] = []
86
85
  for substate in state.states:
87
86
  children.append(_extract_state(substate, machine, getter, active_values))
88
87
  for history_state in getattr(state, "history", []):
@@ -116,8 +115,8 @@ def _format_event_names(transition: "Transition") -> str:
116
115
 
117
116
  all_ids = {str(e) for e in events}
118
117
 
119
- seen_ids: Set[str] = set()
120
- display: List[str] = []
118
+ seen_ids: set[str] = set()
119
+ display: list[str] = []
121
120
  for event in events:
122
121
  eid = str(event)
123
122
  # Skip dot-form aliases (e.g. "done.invoke.X") when the underscore
@@ -131,9 +130,9 @@ def _format_event_names(transition: "Transition") -> str:
131
130
  return " ".join(display)
132
131
 
133
132
 
134
- def _extract_transitions_from_state(state: "State") -> List[DiagramTransition]:
133
+ def _extract_transitions_from_state(state: "State") -> list[DiagramTransition]:
135
134
  """Extract transitions from a single state (non-recursive)."""
136
- result: List[DiagramTransition] = []
135
+ result: list[DiagramTransition] = []
137
136
  for transition in state.transitions:
138
137
  targets = transition.targets if transition.targets else []
139
138
  target_ids = [t.id for t in targets]
@@ -152,9 +151,9 @@ def _extract_transitions_from_state(state: "State") -> List[DiagramTransition]:
152
151
  return result
153
152
 
154
153
 
155
- def _extract_all_transitions(states) -> List[DiagramTransition]:
154
+ def _extract_all_transitions(states) -> list[DiagramTransition]:
156
155
  """Recursively extract transitions from all states."""
157
- result: List[DiagramTransition] = []
156
+ result: list[DiagramTransition] = []
158
157
  for state in states:
159
158
  result.extend(_extract_transitions_from_state(state))
160
159
  if state.states:
@@ -166,9 +165,9 @@ def _extract_all_transitions(states) -> List[DiagramTransition]:
166
165
  return result
167
166
 
168
167
 
169
- def _collect_compound_ids(states: List[DiagramState]) -> Set[str]:
168
+ def _collect_compound_ids(states: list[DiagramState]) -> set[str]:
170
169
  """Collect IDs of states that have children (compound/parallel)."""
171
- result: Set[str] = set()
170
+ result: set[str] = set()
172
171
  for state in states:
173
172
  if state.children:
174
173
  result.add(state.id)
@@ -177,12 +176,12 @@ def _collect_compound_ids(states: List[DiagramState]) -> Set[str]:
177
176
 
178
177
 
179
178
  def _collect_bidirectional_compound_ids(
180
- transitions: List[DiagramTransition],
181
- compound_ids: Set[str],
182
- ) -> Set[str]:
179
+ transitions: list[DiagramTransition],
180
+ compound_ids: set[str],
181
+ ) -> set[str]:
183
182
  """Find compound states that have both outgoing and incoming explicit edges."""
184
- outgoing: Set[str] = set()
185
- incoming: Set[str] = set()
183
+ outgoing: set[str] = set()
184
+ incoming: set[str] = set()
186
185
  for t in transitions:
187
186
  if t.is_internal:
188
187
  continue
@@ -198,8 +197,8 @@ def _collect_bidirectional_compound_ids(
198
197
 
199
198
 
200
199
  def _mark_initial_transitions(
201
- transitions: List[DiagramTransition],
202
- compound_ids: Set[str],
200
+ transitions: list[DiagramTransition],
201
+ compound_ids: set[str],
203
202
  ) -> None:
204
203
  """Mark implicit initial transitions (compound state → child, no event)."""
205
204
  for t in transitions:
@@ -207,7 +206,7 @@ def _mark_initial_transitions(
207
206
  t.is_initial = True
208
207
 
209
208
 
210
- def _resolve_initial_states(states: List[DiagramState]) -> None:
209
+ def _resolve_initial_states(states: list[DiagramState]) -> None:
211
210
  """Ensure exactly one state per level has is_initial=True.
212
211
 
213
212
  Skips parallel areas and history states. Falls back to document order
@@ -268,7 +267,7 @@ def extract(machine_or_class: "MachineRef") -> DiagramGraph:
268
267
  if isinstance(machine, StateChart) and hasattr(machine, "configuration_values"):
269
268
  active_values = set(machine.configuration_values)
270
269
 
271
- states: List[DiagramState] = []
270
+ states: list[DiagramState] = []
272
271
  for state in machine.states:
273
272
  states.append(_extract_state(state, machine, getter, active_values))
274
273
 
@@ -16,24 +16,22 @@ A module-level :data:`formatter` instance is the single public entry point::
16
16
  ...
17
17
  """
18
18
 
19
+ from collections.abc import Callable
19
20
  from typing import TYPE_CHECKING
20
- from typing import Callable
21
- from typing import Dict
22
- from typing import List
23
21
 
24
22
  if TYPE_CHECKING:
25
- from typing import Union
23
+ from typing import TypeAlias
26
24
 
27
25
  from statemachine.statemachine import StateChart
28
26
 
29
- MachineRef = Union["StateChart", "type[StateChart]"]
27
+ MachineRef: TypeAlias = StateChart | type[StateChart]
30
28
 
31
29
 
32
30
  class Formatter:
33
31
  """Unified facade for rendering state machines in multiple text formats."""
34
32
 
35
33
  def __init__(self) -> None:
36
- self._formats: Dict[str, "Callable[[MachineRef], str]"] = {}
34
+ self._formats: dict[str, "Callable[[MachineRef], str]"] = {}
37
35
 
38
36
  def register_format(
39
37
  self, *names: str
@@ -78,7 +76,7 @@ class Formatter:
78
76
  )
79
77
  return renderer_fn(machine_or_class)
80
78
 
81
- def supported_formats(self) -> List[str]:
79
+ def supported_formats(self) -> list[str]:
82
80
  """Return sorted list of all registered format names (including aliases)."""
83
81
  return sorted(self._formats)
84
82
 
@@ -1,8 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from dataclasses import field
3
3
  from enum import Enum
4
- from typing import List
5
- from typing import Set
6
4
 
7
5
 
8
6
  class StateType(Enum):
@@ -36,8 +34,8 @@ class DiagramState:
36
34
  id: str
37
35
  name: str
38
36
  type: StateType
39
- actions: List[DiagramAction] = field(default_factory=list)
40
- children: List["DiagramState"] = field(default_factory=list)
37
+ actions: list[DiagramAction] = field(default_factory=list)
38
+ children: list["DiagramState"] = field(default_factory=list)
41
39
  is_active: bool = False
42
40
  is_parallel_area: bool = False
43
41
  is_initial: bool = False
@@ -46,10 +44,10 @@ class DiagramState:
46
44
  @dataclass
47
45
  class DiagramTransition:
48
46
  source: str
49
- targets: List[str] = field(default_factory=list)
47
+ targets: list[str] = field(default_factory=list)
50
48
  event: str = ""
51
- guards: List[str] = field(default_factory=list)
52
- actions: List[str] = field(default_factory=list)
49
+ guards: list[str] = field(default_factory=list)
50
+ actions: list[str] = field(default_factory=list)
53
51
  is_internal: bool = False
54
52
  is_initial: bool = False
55
53
 
@@ -57,7 +55,7 @@ class DiagramTransition:
57
55
  @dataclass
58
56
  class DiagramGraph:
59
57
  name: str
60
- states: List[DiagramState] = field(default_factory=list)
61
- transitions: List[DiagramTransition] = field(default_factory=list)
62
- compound_state_ids: Set[str] = field(default_factory=set)
63
- bidirectional_compound_ids: Set[str] = field(default_factory=set)
58
+ states: list[DiagramState] = field(default_factory=list)
59
+ transitions: list[DiagramTransition] = field(default_factory=list)
60
+ compound_state_ids: set[str] = field(default_factory=set)
61
+ bidirectional_compound_ids: set[str] = field(default_factory=set)
@@ -1,10 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from dataclasses import field
3
3
  from typing import Any
4
- from typing import Dict
5
- from typing import List
6
- from typing import Optional
7
- from typing import Set
8
4
 
9
5
  import pydot
10
6
 
@@ -31,9 +27,9 @@ class DotRendererConfig:
31
27
  state_active_penwidth: int = 2
32
28
  state_active_fillcolor: str = "turquoise"
33
29
  transition_font_size: str = "10"
34
- graph_attrs: Dict[str, str] = field(default_factory=dict)
35
- node_attrs: Dict[str, str] = field(default_factory=dict)
36
- edge_attrs: Dict[str, str] = field(default_factory=dict)
30
+ graph_attrs: dict[str, str] = field(default_factory=dict)
31
+ node_attrs: dict[str, str] = field(default_factory=dict)
32
+ edge_attrs: dict[str, str] = field(default_factory=dict)
37
33
 
38
34
 
39
35
  class DotRenderer:
@@ -45,10 +41,10 @@ class DotRenderer:
45
41
  - Refined graph/node/edge defaults
46
42
  """
47
43
 
48
- def __init__(self, config: Optional[DotRendererConfig] = None):
44
+ def __init__(self, config: DotRendererConfig | None = None):
49
45
  self.config = config or DotRendererConfig()
50
- self._compound_ids: Set[str] = set()
51
- self._compound_bidir_ids: Set[str] = set()
46
+ self._compound_ids: set[str] = set()
47
+ self._compound_bidir_ids: set[str] = set()
52
48
 
53
49
  def render(self, graph: DiagramGraph) -> pydot.Dot:
54
50
  """Render a DiagramGraph to a pydot.Dot object."""
@@ -120,10 +116,10 @@ class DotRenderer:
120
116
 
121
117
  def _render_states(
122
118
  self,
123
- states: List[DiagramState],
124
- transitions: List[DiagramTransition],
119
+ states: list[DiagramState],
120
+ transitions: list[DiagramTransition],
125
121
  parent_graph: "pydot.Dot | pydot.Subgraph",
126
- extra_nodes: Optional[List[pydot.Node]] = None,
122
+ extra_nodes: list[pydot.Node] | None = None,
127
123
  ) -> None:
128
124
  """Render states and transitions into the parent graph."""
129
125
  initial_state = next((s for s in states if s.is_initial), None)
@@ -177,7 +173,7 @@ class DotRenderer:
177
173
 
178
174
  @staticmethod
179
175
  def _place_extra_nodes(
180
- extra_nodes: Optional[List[pydot.Node]],
176
+ extra_nodes: list[pydot.Node] | None,
181
177
  atomic_subgraph: pydot.Subgraph,
182
178
  parent_graph: "pydot.Dot | pydot.Subgraph",
183
179
  has_atomic: bool,
@@ -211,7 +207,7 @@ class DotRenderer:
211
207
  initial_node = self._create_initial_node(initial_node_id)
212
208
  added_to_atomic = False
213
209
 
214
- extra: Dict[str, Any] = {}
210
+ extra: dict[str, Any] = {}
215
211
  if initial_state.children:
216
212
  extra["lhead"] = f"cluster_{initial_state.id}"
217
213
 
@@ -308,7 +304,7 @@ class DotRenderer:
308
304
  def _build_html_table_label(
309
305
  self,
310
306
  state: DiagramState,
311
- actions: List[DiagramAction],
307
+ actions: list[DiagramAction],
312
308
  ) -> str:
313
309
  """Build an HTML TABLE label with UML compartments (name | actions).
314
310
 
@@ -357,7 +353,7 @@ class DotRenderer:
357
353
  height=0.3,
358
354
  )
359
355
 
360
- def _create_compound_anchor_nodes(self, state: DiagramState) -> List[pydot.Node]:
356
+ def _create_compound_anchor_nodes(self, state: DiagramState) -> list[pydot.Node]:
361
357
  """Create invisible anchor nodes for edge routing inside a compound cluster.
362
358
 
363
359
  These nodes are injected into the children's atomic_subgraph so they
@@ -433,7 +429,7 @@ class DotRenderer:
433
429
  def _add_transitions_for_state(
434
430
  self,
435
431
  state: DiagramState,
436
- all_transitions: List[DiagramTransition],
432
+ all_transitions: list[DiagramTransition],
437
433
  graph: "pydot.Dot | pydot.Subgraph",
438
434
  ) -> None:
439
435
  """Add edges for all non-internal transitions originating from this state."""
@@ -446,11 +442,9 @@ class DotRenderer:
446
442
  for edge in self._create_edges(transition):
447
443
  graph.add_edge(edge)
448
444
 
449
- def _create_edges(self, transition: DiagramTransition) -> List[pydot.Edge]:
445
+ def _create_edges(self, transition: DiagramTransition) -> list[pydot.Edge]:
450
446
  """Create pydot.Edge objects for a transition."""
451
- target_ids: List[Optional[str]] = (
452
- list(transition.targets) if transition.targets else [None]
453
- )
447
+ target_ids: list[str | None] = list(transition.targets) if transition.targets else [None]
454
448
 
455
449
  cond = ", ".join(transition.guards)
456
450
  cond_html = f"<br/>[{_escape_html(cond)}]" if cond else ""
@@ -463,7 +457,7 @@ class DotRenderer:
463
457
  def _create_single_edge(
464
458
  self,
465
459
  transition: DiagramTransition,
466
- target_id: Optional[str],
460
+ target_id: str | None,
467
461
  index: int,
468
462
  cond_html: str,
469
463
  ) -> pydot.Edge:
@@ -483,10 +477,10 @@ class DotRenderer:
483
477
  def _resolve_edge_endpoints(
484
478
  self,
485
479
  transition: DiagramTransition,
486
- target_id: Optional[str],
487
- ) -> "tuple[str, str, Dict[str, Any]]":
480
+ target_id: str | None,
481
+ ) -> "tuple[str, str, dict[str, Any]]":
488
482
  """Resolve source/destination node IDs and cluster attributes for an edge."""
489
- extra: Dict[str, Any] = {}
483
+ extra: dict[str, Any] = {}
490
484
  source_is_compound = transition.source in self._compound_ids
491
485
  target_is_compound = target_id is not None and target_id in self._compound_ids
492
486