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.
- {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/METADATA +16 -3
- python_statemachine-3.2.0.dist-info/RECORD +72 -0
- {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +5 -11
- statemachine/configuration.py +5 -6
- statemachine/contrib/diagram/__init__.py +15 -6
- statemachine/contrib/diagram/extract.py +23 -24
- statemachine/contrib/diagram/formatter.py +5 -7
- statemachine/contrib/diagram/model.py +9 -11
- statemachine/contrib/diagram/renderers/dot.py +20 -26
- statemachine/contrib/diagram/renderers/mermaid.py +36 -40
- statemachine/contrib/diagram/renderers/table.py +7 -9
- statemachine/contrib/weighted.py +7 -11
- statemachine/dispatcher.py +13 -12
- statemachine/engines/async_.py +5 -6
- statemachine/engines/base.py +12 -14
- statemachine/event.py +1 -2
- statemachine/exceptions.py +1 -1
- statemachine/factory.py +12 -16
- statemachine/graph.py +2 -2
- statemachine/invoke.py +12 -11
- statemachine/io/__init__.py +45 -225
- statemachine/io/{scxml/actions.py → actions.py} +158 -288
- statemachine/io/builder.py +195 -0
- statemachine/io/class_factory.py +236 -0
- statemachine/io/evaluators.py +275 -0
- statemachine/io/interpreter.py +128 -0
- statemachine/io/{scxml/invoke.py → invoke.py} +77 -49
- statemachine/io/json/__init__.py +1 -0
- statemachine/io/json/reader.py +27 -0
- statemachine/io/loader.py +161 -0
- statemachine/io/model.py +268 -0
- statemachine/io/native.py +402 -0
- statemachine/io/ports.py +83 -0
- statemachine/io/schemas/statechart.schema.json +258 -0
- statemachine/io/scxml/__init__.py +12 -0
- statemachine/io/scxml/processor.py +23 -253
- statemachine/io/scxml/{parser.py → reader.py} +64 -47
- statemachine/io/system_variables.py +184 -0
- statemachine/io/validation.py +44 -0
- statemachine/io/yaml/__init__.py +1 -0
- statemachine/io/yaml/reader.py +65 -0
- statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +20 -22
- statemachine/orderedset.py +3 -3
- statemachine/registry.py +1 -4
- statemachine/signature.py +2 -5
- statemachine/spec_parser.py +171 -42
- statemachine/state.py +5 -6
- statemachine/statemachine.py +18 -20
- statemachine/states.py +3 -5
- statemachine/transition.py +3 -4
- statemachine/transition_list.py +4 -5
- statemachine/transition_mixin.py +1 -1
- python_statemachine-3.1.1.dist-info/RECORD +0 -58
- statemachine/io/scxml/schema.py +0 -175
- {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.
|
|
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.
|
|
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,,
|
statemachine/__init__.py
CHANGED
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: "
|
|
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:
|
|
152
|
+
self.items: list[CallbackSpec] = []
|
|
159
153
|
self.conventional_specs = set()
|
|
160
|
-
self._groupers:
|
|
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:
|
|
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:
|
statemachine/configuration.py
CHANGED
|
@@ -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[
|
|
36
|
+
instance_states: "Mapping[Any, State]",
|
|
38
37
|
model: Any,
|
|
39
38
|
state_field: str,
|
|
40
|
-
states_map: "
|
|
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[
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
from .renderers.
|
|
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 =
|
|
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) ->
|
|
54
|
-
actions:
|
|
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:
|
|
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:
|
|
120
|
-
display:
|
|
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") ->
|
|
133
|
+
def _extract_transitions_from_state(state: "State") -> list[DiagramTransition]:
|
|
135
134
|
"""Extract transitions from a single state (non-recursive)."""
|
|
136
|
-
result:
|
|
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) ->
|
|
154
|
+
def _extract_all_transitions(states) -> list[DiagramTransition]:
|
|
156
155
|
"""Recursively extract transitions from all states."""
|
|
157
|
-
result:
|
|
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:
|
|
168
|
+
def _collect_compound_ids(states: list[DiagramState]) -> set[str]:
|
|
170
169
|
"""Collect IDs of states that have children (compound/parallel)."""
|
|
171
|
-
result:
|
|
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:
|
|
181
|
-
compound_ids:
|
|
182
|
-
) ->
|
|
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:
|
|
185
|
-
incoming:
|
|
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:
|
|
202
|
-
compound_ids:
|
|
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:
|
|
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:
|
|
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
|
|
23
|
+
from typing import TypeAlias
|
|
26
24
|
|
|
27
25
|
from statemachine.statemachine import StateChart
|
|
28
26
|
|
|
29
|
-
MachineRef =
|
|
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:
|
|
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) ->
|
|
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:
|
|
40
|
-
children:
|
|
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:
|
|
47
|
+
targets: list[str] = field(default_factory=list)
|
|
50
48
|
event: str = ""
|
|
51
|
-
guards:
|
|
52
|
-
actions:
|
|
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:
|
|
61
|
-
transitions:
|
|
62
|
-
compound_state_ids:
|
|
63
|
-
bidirectional_compound_ids:
|
|
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:
|
|
35
|
-
node_attrs:
|
|
36
|
-
edge_attrs:
|
|
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:
|
|
44
|
+
def __init__(self, config: DotRendererConfig | None = None):
|
|
49
45
|
self.config = config or DotRendererConfig()
|
|
50
|
-
self._compound_ids:
|
|
51
|
-
self._compound_bidir_ids:
|
|
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:
|
|
124
|
-
transitions:
|
|
119
|
+
states: list[DiagramState],
|
|
120
|
+
transitions: list[DiagramTransition],
|
|
125
121
|
parent_graph: "pydot.Dot | pydot.Subgraph",
|
|
126
|
-
extra_nodes:
|
|
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:
|
|
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:
|
|
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:
|
|
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) ->
|
|
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:
|
|
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) ->
|
|
445
|
+
def _create_edges(self, transition: DiagramTransition) -> list[pydot.Edge]:
|
|
450
446
|
"""Create pydot.Edge objects for a transition."""
|
|
451
|
-
target_ids:
|
|
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:
|
|
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:
|
|
487
|
-
) -> "tuple[str, str,
|
|
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:
|
|
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
|
|