chartflow 0.1__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.
- chartflow/__init__.py +28 -0
- chartflow/_verify_importexport.py +169 -0
- chartflow/edit/__init__.py +119 -0
- chartflow/edit/code_editor.py +1007 -0
- chartflow/edit/code_folder.py +523 -0
- chartflow/edit/completer.py +2652 -0
- chartflow/edit/context_analyzer.py +521 -0
- chartflow/edit/demo.py +469 -0
- chartflow/edit/line_number_area.py +548 -0
- chartflow/edit/minimap.py +581 -0
- chartflow/edit/python_editor.py +509 -0
- chartflow/edit/syntax_highlighter.py +291 -0
- chartflow/edit/test_editor.py +190 -0
- chartflow/flow/__init__.py +31 -0
- chartflow/flow/_demo.py +98 -0
- chartflow/flow/chart.py +1101 -0
- chartflow/flow/editor.py +375 -0
- chartflow/flow/flow.py +6 -0
- chartflow/flow/qchart.py +1250 -0
- chartflow/flow/test/__init__.py +0 -0
- chartflow/flow/test/test_chart.py +537 -0
- chartflow/flow/test_decorator_sync.py +102 -0
- chartflow/fsm/__init__.py +84 -0
- chartflow/fsm/decorators.py +395 -0
- chartflow/fsm/demo.py +381 -0
- chartflow/fsm/demo_pyqt6.py +881 -0
- chartflow/fsm/demo_tree.py +46 -0
- chartflow/fsm/logic.py +447 -0
- chartflow/fsm/meta.py +569 -0
- chartflow/fsm/scheduler.py +427 -0
- chartflow/fsm/stage.py +2079 -0
- chartflow/fsm/stdio.py +66 -0
- chartflow/fsm/test/__init__.py +7 -0
- chartflow/fsm/test/test_decorators.py +210 -0
- chartflow/fsm/test/test_events.py +202 -0
- chartflow/fsm/test/test_integration.py +596 -0
- chartflow/fsm/test/test_logic.py +371 -0
- chartflow/fsm/test/test_meta.py +192 -0
- chartflow/fsm/test/test_runtime_param.py +336 -0
- chartflow/fsm/test/test_scheduler.py +280 -0
- chartflow/fsm/test/test_stage.py +314 -0
- chartflow/fsm/test_behavior_events.py +263 -0
- chartflow/fsm/test_nested_parallel.py +80 -0
- chartflow/fsm/test_stage_spec.py +448 -0
- chartflow/fsm/utils.py +34 -0
- chartflow/node/__init__.py +66 -0
- chartflow/node/_node_base.py +727 -0
- chartflow/node/_node_interaction.py +488 -0
- chartflow/node/_node_interface.py +426 -0
- chartflow/node/_node_markers.py +648 -0
- chartflow/node/_node_ports.py +536 -0
- chartflow/node/_port_interface.py +111 -0
- chartflow/node/arrow_item.py +287 -0
- chartflow/node/color_utils.py +113 -0
- chartflow/node/connection_item.py +939 -0
- chartflow/node/demo.py +258 -0
- chartflow/node/demo_arrow_drag.py +46 -0
- chartflow/node/demo_decorator.py +63 -0
- chartflow/node/demo_ports.py +69 -0
- chartflow/node/enums.py +35 -0
- chartflow/node/example.py +84 -0
- chartflow/node/layout_utils.py +307 -0
- chartflow/node/main_window.py +196 -0
- chartflow/node/menu_bar.py +240 -0
- chartflow/node/node_canvas.py +1730 -0
- chartflow/node/node_item.py +754 -0
- chartflow/node/popmenu.py +472 -0
- chartflow/node/port_item.py +591 -0
- chartflow/node/qnode_editor.py +73 -0
- chartflow/node/selection_rect.py +53 -0
- chartflow/node/style_panel.py +615 -0
- chartflow/node/styles.py +63 -0
- chartflow/node/test_qnode.py +2336 -0
- chartflow/showcase.py +528 -0
- chartflow/theme.py +508 -0
- chartflow-0.1.dist-info/METADATA +23 -0
- chartflow-0.1.dist-info/RECORD +79 -0
- chartflow-0.1.dist-info/WHEEL +5 -0
- chartflow-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QFSM - Quick Finite State Machine Library
|
|
3
|
+
|
|
4
|
+
A Python 3.14+ finite state machine library with decorators and event system.
|
|
5
|
+
|
|
6
|
+
Basic Usage:
|
|
7
|
+
from chartflow.fsm import Stage, Logic, Scheduler
|
|
8
|
+
from chartflow.fsm.decorators import recursive, listen, sequence
|
|
9
|
+
|
|
10
|
+
class MyLogic(Logic):
|
|
11
|
+
NAME = "MyLogic"
|
|
12
|
+
|
|
13
|
+
def initial(self, inst):
|
|
14
|
+
return "running"
|
|
15
|
+
|
|
16
|
+
def running(self, inst):
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
# Auto-registered with Scheduler
|
|
20
|
+
logic = MyLogic()
|
|
21
|
+
|
|
22
|
+
# Run scheduler
|
|
23
|
+
scheduler = Scheduler()
|
|
24
|
+
scheduler.handle()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from chartflow.fsm.meta import (
|
|
28
|
+
StageMachineMeta,
|
|
29
|
+
GetPrototype,
|
|
30
|
+
ListPrototypes,
|
|
31
|
+
ClearPrototypes,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from chartflow.fsm.scheduler import Scheduler
|
|
35
|
+
|
|
36
|
+
from chartflow.fsm.stage import Stage, _NamedNode
|
|
37
|
+
|
|
38
|
+
from chartflow.fsm.logic import Logic
|
|
39
|
+
|
|
40
|
+
from chartflow.fsm.decorators import (
|
|
41
|
+
recursive,
|
|
42
|
+
listen,
|
|
43
|
+
behavior,
|
|
44
|
+
sequence,
|
|
45
|
+
selector,
|
|
46
|
+
parallel,
|
|
47
|
+
process_fsm_markers,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from chartflow.fsm.utils import (
|
|
51
|
+
EventSystem,
|
|
52
|
+
GetES,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Note: chartflow is a standalone library. It intentionally does NOT register its
|
|
56
|
+
# classes into any external registry (e.g. qwork's), so importing chartflow has no
|
|
57
|
+
# side effects on other libraries. If you want chartflow classes in a registry,
|
|
58
|
+
# call the registry's register API explicitly from your application code.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__version__ = '0.1.0'
|
|
62
|
+
__all__ = [
|
|
63
|
+
# Core classes
|
|
64
|
+
'StageMachineMeta',
|
|
65
|
+
'Scheduler',
|
|
66
|
+
'Stage',
|
|
67
|
+
'Logic',
|
|
68
|
+
'_NamedNode',
|
|
69
|
+
# Decorators
|
|
70
|
+
'recursive',
|
|
71
|
+
'listen',
|
|
72
|
+
'behavior',
|
|
73
|
+
'sequence',
|
|
74
|
+
'selector',
|
|
75
|
+
'parallel',
|
|
76
|
+
'process_fsm_markers',
|
|
77
|
+
# Events
|
|
78
|
+
'EventSystem',
|
|
79
|
+
'GetES',
|
|
80
|
+
# Utility functions
|
|
81
|
+
'GetPrototype',
|
|
82
|
+
'ListPrototypes',
|
|
83
|
+
'ClearPrototypes',
|
|
84
|
+
]
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""
|
|
3
|
+
QFSM Decorators - Finite State Machine decorators for Python 3.14+
|
|
4
|
+
|
|
5
|
+
This module provides decorators for marking states with various behaviors
|
|
6
|
+
in a finite state machine system.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import functools
|
|
11
|
+
from typing import Any, Callable, Concatenate, ParamSpec, TypeVar
|
|
12
|
+
|
|
13
|
+
# Type variables for better type inference
|
|
14
|
+
P = ParamSpec('P')
|
|
15
|
+
R = TypeVar('R')
|
|
16
|
+
T = TypeVar('T')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _StateMarker:
|
|
20
|
+
"""Base descriptor for state markers using descriptor protocol."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, attr_name: str, value: Any) -> None:
|
|
23
|
+
self.attr_name = attr_name
|
|
24
|
+
self.value = value
|
|
25
|
+
|
|
26
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
27
|
+
# Initialize the class-level attribute when descriptor is assigned
|
|
28
|
+
if not hasattr(owner, self.attr_name):
|
|
29
|
+
setattr(owner, self.attr_name, set() if isinstance(self.value, set) else {})
|
|
30
|
+
|
|
31
|
+
# Add this method to the marker set/dict
|
|
32
|
+
marker = getattr(owner, self.attr_name)
|
|
33
|
+
if isinstance(marker, set):
|
|
34
|
+
marker.add(name)
|
|
35
|
+
elif isinstance(marker, dict):
|
|
36
|
+
marker[name] = self.value
|
|
37
|
+
|
|
38
|
+
def __get__(self, instance: Any, owner: type) -> Any:
|
|
39
|
+
return self.value
|
|
40
|
+
|
|
41
|
+
def __set__(self, instance: Any, value: Any) -> None:
|
|
42
|
+
raise AttributeError(f"Cannot modify {self.attr_name} marker")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _RecursiveDescriptor:
|
|
46
|
+
"""Descriptor that marks a method as recursive (can execute multiple times per tick)."""
|
|
47
|
+
|
|
48
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
49
|
+
if not hasattr(owner, '__recursive__'):
|
|
50
|
+
owner.__recursive__ = set()
|
|
51
|
+
owner.__recursive__.add(name)
|
|
52
|
+
|
|
53
|
+
def __get__(self, instance: Any, owner: type) -> bool:
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _ListenDescriptor:
|
|
58
|
+
"""Descriptor that marks a method as an event listener."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, event_name: str, sources: tuple[str, ...]) -> None:
|
|
61
|
+
self.event_name = event_name
|
|
62
|
+
self.sources = sources
|
|
63
|
+
|
|
64
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
65
|
+
if not hasattr(owner, '__listens__'):
|
|
66
|
+
owner.__listens__ = {}
|
|
67
|
+
|
|
68
|
+
if self.event_name not in owner.__listens__:
|
|
69
|
+
owner.__listens__[self.event_name] = []
|
|
70
|
+
|
|
71
|
+
owner.__listens__[self.event_name].append((name, self.sources))
|
|
72
|
+
|
|
73
|
+
def __get__(self, instance: Any, owner: type) -> tuple[str, tuple[str, ...]]:
|
|
74
|
+
return (self.event_name, self.sources)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _BehaviorDescriptor:
|
|
78
|
+
"""Descriptor that marks a state with transition behavior."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, target: str | None = None,
|
|
81
|
+
children: list[str] | None = None,
|
|
82
|
+
composite: str | None = None) -> None:
|
|
83
|
+
self.target = target
|
|
84
|
+
self.children = children
|
|
85
|
+
self.composite = composite
|
|
86
|
+
|
|
87
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
88
|
+
if not hasattr(owner, '__behaviors__'):
|
|
89
|
+
owner.__behaviors__ = {}
|
|
90
|
+
|
|
91
|
+
if self.children is not None:
|
|
92
|
+
# Composite behavior
|
|
93
|
+
owner.__behaviors__[name] = {
|
|
94
|
+
'children': self.children,
|
|
95
|
+
'composite': self.composite
|
|
96
|
+
}
|
|
97
|
+
else:
|
|
98
|
+
# Simple behavior
|
|
99
|
+
owner.__behaviors__[name] = {
|
|
100
|
+
'children': self.target,
|
|
101
|
+
'composite': None
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def __get__(self, instance: Any, owner: type) -> dict[str, Any]:
|
|
105
|
+
if self.children is not None:
|
|
106
|
+
return {'children': self.children, 'composite': self.composite}
|
|
107
|
+
return {'children': self.target, 'composite': None}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# =============================================================================
|
|
111
|
+
# Public Decorators
|
|
112
|
+
# =============================================================================
|
|
113
|
+
|
|
114
|
+
def recursive(method: Callable[Concatenate[T, P], R]) -> Callable[Concatenate[T, P], R]:
|
|
115
|
+
"""
|
|
116
|
+
Marks a state method as recursive.
|
|
117
|
+
|
|
118
|
+
Recursive states can be called multiple times per tick until they
|
|
119
|
+
return a new state transition.
|
|
120
|
+
|
|
121
|
+
The marked method name is stored in class.__recursive__ set.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
class MyFSM:
|
|
125
|
+
@recursive
|
|
126
|
+
def processing(self):
|
|
127
|
+
# This can be called multiple times per tick
|
|
128
|
+
return self.next_state
|
|
129
|
+
"""
|
|
130
|
+
@functools.wraps(method)
|
|
131
|
+
def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
132
|
+
return method(self, *args, **kwargs)
|
|
133
|
+
|
|
134
|
+
# Mark the wrapper with the recursive descriptor
|
|
135
|
+
wrapper._recursive_marker = True
|
|
136
|
+
wrapper.__name__ = method.__name__
|
|
137
|
+
wrapper.__doc__ = method.__doc__
|
|
138
|
+
|
|
139
|
+
return wrapper
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def listen(event_name: str, *sources: str) -> Callable[[Callable[Concatenate[T, P], R]], Callable[Concatenate[T, P], R]]:
|
|
143
|
+
"""
|
|
144
|
+
Marks a method as an event listener.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
event_name: The name of the event to listen for
|
|
148
|
+
*sources: Optional source filters for the event
|
|
149
|
+
|
|
150
|
+
The listener info is stored in class.__listens__ as:
|
|
151
|
+
{event_name: [(func_name, sources), ...]}
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
class MyFSM:
|
|
155
|
+
@listen('player_died', 'game', 'combat')
|
|
156
|
+
def on_player_died(self, data):
|
|
157
|
+
print(f"Player died: {data}")
|
|
158
|
+
"""
|
|
159
|
+
def decorator(method: Callable[Concatenate[T, P], R]) -> Callable[Concatenate[T, P], R]:
|
|
160
|
+
@functools.wraps(method)
|
|
161
|
+
def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
162
|
+
return method(self, *args, **kwargs)
|
|
163
|
+
|
|
164
|
+
if method.__name__.startswith("_"):
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"[def] Cannot @listen method with name startswith '_'. ('{method.__name__}')"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Store listen metadata on the wrapper
|
|
170
|
+
wrapper._listen_event = event_name
|
|
171
|
+
wrapper._listen_sources = sources
|
|
172
|
+
wrapper.__name__ = method.__name__
|
|
173
|
+
wrapper.__doc__ = method.__doc__
|
|
174
|
+
|
|
175
|
+
return wrapper
|
|
176
|
+
|
|
177
|
+
return decorator
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def behavior(target: str) -> Callable[[Callable[Concatenate[T, P], R]], Callable[Concatenate[T, P], R]]:
|
|
181
|
+
"""
|
|
182
|
+
Marks a simple state transition behavior.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
target: The target state name to transition to
|
|
186
|
+
|
|
187
|
+
The behavior is stored in class.__behaviors__ as:
|
|
188
|
+
{state_name: {'children': target, 'composite': None}}
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
class MyFSM:
|
|
192
|
+
@behavior('running')
|
|
193
|
+
def idle(self):
|
|
194
|
+
pass # Will transition to 'running'
|
|
195
|
+
"""
|
|
196
|
+
def decorator(method: Callable[Concatenate[T, P], R]) -> Callable[Concatenate[T, P], R]:
|
|
197
|
+
@functools.wraps(method)
|
|
198
|
+
def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
199
|
+
return method(self, *args, **kwargs)
|
|
200
|
+
|
|
201
|
+
# Store behavior metadata
|
|
202
|
+
wrapper._behavior_target = target
|
|
203
|
+
wrapper._behavior_composite = None
|
|
204
|
+
wrapper.__name__ = method.__name__
|
|
205
|
+
wrapper.__doc__ = method.__doc__
|
|
206
|
+
|
|
207
|
+
return wrapper
|
|
208
|
+
|
|
209
|
+
return decorator
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def sequence(*states: str) -> Callable[[Callable[Concatenate[T, P], R]], Callable[Concatenate[T, P], R]]:
|
|
213
|
+
"""
|
|
214
|
+
Marks a sequence composite behavior.
|
|
215
|
+
|
|
216
|
+
A sequence executes all child states in order and fails if any child fails.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
*states: The child state names to execute in sequence
|
|
220
|
+
|
|
221
|
+
The behavior is stored in class.__behaviors__ as:
|
|
222
|
+
{state_name: {'children': [...], 'composite': 'sequence'}}
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
class MyFSM:
|
|
226
|
+
@sequence('approach', 'attack', 'retreat')
|
|
227
|
+
def combat_sequence(self):
|
|
228
|
+
pass # Executes approach, then attack, then retreat
|
|
229
|
+
"""
|
|
230
|
+
def decorator(method: Callable[Concatenate[T, P], R]) -> Callable[Concatenate[T, P], R]:
|
|
231
|
+
@functools.wraps(method)
|
|
232
|
+
def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
233
|
+
return method(self, *args, **kwargs)
|
|
234
|
+
|
|
235
|
+
# Store sequence metadata
|
|
236
|
+
wrapper._behavior_children = list(states)
|
|
237
|
+
wrapper._behavior_composite = 'sequence'
|
|
238
|
+
wrapper.__name__ = method.__name__
|
|
239
|
+
wrapper.__doc__ = method.__doc__
|
|
240
|
+
|
|
241
|
+
return wrapper
|
|
242
|
+
|
|
243
|
+
return decorator
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def selector(*states: str) -> Callable[[Callable[Concatenate[T, P], R]], Callable[Concatenate[T, P], R]]:
|
|
247
|
+
"""
|
|
248
|
+
Marks a selector composite behavior.
|
|
249
|
+
|
|
250
|
+
A selector executes child states until one succeeds (returns a truthy value).
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
*states: The child state names to try in order
|
|
254
|
+
|
|
255
|
+
The behavior is stored in class.__behaviors__ as:
|
|
256
|
+
{state_name: {'children': [...], 'composite': 'selector'}}
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
class MyFSM:
|
|
260
|
+
@selector('attack_melee', 'attack_ranged', 'flee')
|
|
261
|
+
def choose_attack(self):
|
|
262
|
+
pass # Tries melee, then ranged, then flees
|
|
263
|
+
"""
|
|
264
|
+
def decorator(method: Callable[Concatenate[T, P], R]) -> Callable[Concatenate[T, P], R]:
|
|
265
|
+
@functools.wraps(method)
|
|
266
|
+
def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
267
|
+
return method(self, *args, **kwargs)
|
|
268
|
+
|
|
269
|
+
# Store selector metadata
|
|
270
|
+
wrapper._behavior_children = list(states)
|
|
271
|
+
wrapper._behavior_composite = 'selector'
|
|
272
|
+
wrapper.__name__ = method.__name__
|
|
273
|
+
wrapper.__doc__ = method.__doc__
|
|
274
|
+
|
|
275
|
+
return wrapper
|
|
276
|
+
|
|
277
|
+
return decorator
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def parallel(*states: str) -> Callable[[Callable[Concatenate[T, P], R]], Callable[Concatenate[T, P], R]]:
|
|
281
|
+
"""
|
|
282
|
+
Marks a parallel composite behavior.
|
|
283
|
+
|
|
284
|
+
A parallel executes all child states and succeeds if any succeeds.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
*states: The child state names to execute in parallel
|
|
288
|
+
|
|
289
|
+
The behavior is stored in class.__behaviors__ as:
|
|
290
|
+
{state_name: {'children': [...], 'composite': 'parallel'}}
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
class MyFSM:
|
|
294
|
+
@parallel('check_enemies', 'check_allies', 'check_obstacles')
|
|
295
|
+
def assess_situation(self):
|
|
296
|
+
pass # Runs all checks in parallel
|
|
297
|
+
"""
|
|
298
|
+
def decorator(method: Callable[Concatenate[T, P], R]) -> Callable[Concatenate[T, P], R]:
|
|
299
|
+
@functools.wraps(method)
|
|
300
|
+
def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
301
|
+
return method(self, *args, **kwargs)
|
|
302
|
+
|
|
303
|
+
# Store parallel metadata
|
|
304
|
+
wrapper._behavior_children = list(states)
|
|
305
|
+
wrapper._behavior_composite = 'parallel'
|
|
306
|
+
wrapper.__name__ = method.__name__
|
|
307
|
+
wrapper.__doc__ = method.__doc__
|
|
308
|
+
|
|
309
|
+
return wrapper
|
|
310
|
+
|
|
311
|
+
return decorator
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# =============================================================================
|
|
315
|
+
# Class Decorator for Processing Markers
|
|
316
|
+
# =============================================================================
|
|
317
|
+
|
|
318
|
+
def process_fsm_markers(cls: type[T]) -> type[T]:
|
|
319
|
+
"""
|
|
320
|
+
Class decorator that processes all FSM markers on methods.
|
|
321
|
+
|
|
322
|
+
This should be applied to any class using the FSM decorators to ensure
|
|
323
|
+
the __recursive__, __listens__, and __behaviors__ attributes are properly
|
|
324
|
+
populated.
|
|
325
|
+
|
|
326
|
+
Example:
|
|
327
|
+
@process_fsm_markers
|
|
328
|
+
class MyFSM:
|
|
329
|
+
@recursive
|
|
330
|
+
def processing(self): ...
|
|
331
|
+
|
|
332
|
+
@listen('event')
|
|
333
|
+
def on_event(self, data): ...
|
|
334
|
+
"""
|
|
335
|
+
# Initialize class-level attributes if not present
|
|
336
|
+
if not hasattr(cls, '__recursive__'):
|
|
337
|
+
cls.__recursive__ = set()
|
|
338
|
+
if not hasattr(cls, '__listens__'):
|
|
339
|
+
cls.__listens__ = {}
|
|
340
|
+
if not hasattr(cls, '__behaviors__'):
|
|
341
|
+
cls.__behaviors__ = {}
|
|
342
|
+
|
|
343
|
+
# Scan all methods for markers
|
|
344
|
+
for name in dir(cls):
|
|
345
|
+
if name.startswith('_'):
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
attr = getattr(cls, name)
|
|
350
|
+
except AttributeError:
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
if not callable(attr):
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
# Check for recursive marker
|
|
357
|
+
if hasattr(attr, '_recursive_marker'):
|
|
358
|
+
cls.__recursive__.add(name)
|
|
359
|
+
|
|
360
|
+
# Check for listen marker
|
|
361
|
+
if hasattr(attr, '_listen_event'):
|
|
362
|
+
event = attr._listen_event
|
|
363
|
+
sources = attr._listen_sources
|
|
364
|
+
if event not in cls.__listens__:
|
|
365
|
+
cls.__listens__[event] = []
|
|
366
|
+
cls.__listens__[event].append((name, sources))
|
|
367
|
+
|
|
368
|
+
# Check for behavior marker
|
|
369
|
+
if hasattr(attr, '_behavior_target'):
|
|
370
|
+
cls.__behaviors__[name] = {
|
|
371
|
+
'children': attr._behavior_target,
|
|
372
|
+
'composite': None
|
|
373
|
+
}
|
|
374
|
+
elif hasattr(attr, '_behavior_children'):
|
|
375
|
+
cls.__behaviors__[name] = {
|
|
376
|
+
'children': attr._behavior_children,
|
|
377
|
+
'composite': attr._behavior_composite
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return cls
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# =============================================================================
|
|
384
|
+
# Convenience Exports
|
|
385
|
+
# =============================================================================
|
|
386
|
+
|
|
387
|
+
__all__ = [
|
|
388
|
+
'recursive',
|
|
389
|
+
'listen',
|
|
390
|
+
'behavior',
|
|
391
|
+
'sequence',
|
|
392
|
+
'selector',
|
|
393
|
+
'parallel',
|
|
394
|
+
'process_fsm_markers',
|
|
395
|
+
]
|