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.
Files changed (79) hide show
  1. chartflow/__init__.py +28 -0
  2. chartflow/_verify_importexport.py +169 -0
  3. chartflow/edit/__init__.py +119 -0
  4. chartflow/edit/code_editor.py +1007 -0
  5. chartflow/edit/code_folder.py +523 -0
  6. chartflow/edit/completer.py +2652 -0
  7. chartflow/edit/context_analyzer.py +521 -0
  8. chartflow/edit/demo.py +469 -0
  9. chartflow/edit/line_number_area.py +548 -0
  10. chartflow/edit/minimap.py +581 -0
  11. chartflow/edit/python_editor.py +509 -0
  12. chartflow/edit/syntax_highlighter.py +291 -0
  13. chartflow/edit/test_editor.py +190 -0
  14. chartflow/flow/__init__.py +31 -0
  15. chartflow/flow/_demo.py +98 -0
  16. chartflow/flow/chart.py +1101 -0
  17. chartflow/flow/editor.py +375 -0
  18. chartflow/flow/flow.py +6 -0
  19. chartflow/flow/qchart.py +1250 -0
  20. chartflow/flow/test/__init__.py +0 -0
  21. chartflow/flow/test/test_chart.py +537 -0
  22. chartflow/flow/test_decorator_sync.py +102 -0
  23. chartflow/fsm/__init__.py +84 -0
  24. chartflow/fsm/decorators.py +395 -0
  25. chartflow/fsm/demo.py +381 -0
  26. chartflow/fsm/demo_pyqt6.py +881 -0
  27. chartflow/fsm/demo_tree.py +46 -0
  28. chartflow/fsm/logic.py +447 -0
  29. chartflow/fsm/meta.py +569 -0
  30. chartflow/fsm/scheduler.py +427 -0
  31. chartflow/fsm/stage.py +2079 -0
  32. chartflow/fsm/stdio.py +66 -0
  33. chartflow/fsm/test/__init__.py +7 -0
  34. chartflow/fsm/test/test_decorators.py +210 -0
  35. chartflow/fsm/test/test_events.py +202 -0
  36. chartflow/fsm/test/test_integration.py +596 -0
  37. chartflow/fsm/test/test_logic.py +371 -0
  38. chartflow/fsm/test/test_meta.py +192 -0
  39. chartflow/fsm/test/test_runtime_param.py +336 -0
  40. chartflow/fsm/test/test_scheduler.py +280 -0
  41. chartflow/fsm/test/test_stage.py +314 -0
  42. chartflow/fsm/test_behavior_events.py +263 -0
  43. chartflow/fsm/test_nested_parallel.py +80 -0
  44. chartflow/fsm/test_stage_spec.py +448 -0
  45. chartflow/fsm/utils.py +34 -0
  46. chartflow/node/__init__.py +66 -0
  47. chartflow/node/_node_base.py +727 -0
  48. chartflow/node/_node_interaction.py +488 -0
  49. chartflow/node/_node_interface.py +426 -0
  50. chartflow/node/_node_markers.py +648 -0
  51. chartflow/node/_node_ports.py +536 -0
  52. chartflow/node/_port_interface.py +111 -0
  53. chartflow/node/arrow_item.py +287 -0
  54. chartflow/node/color_utils.py +113 -0
  55. chartflow/node/connection_item.py +939 -0
  56. chartflow/node/demo.py +258 -0
  57. chartflow/node/demo_arrow_drag.py +46 -0
  58. chartflow/node/demo_decorator.py +63 -0
  59. chartflow/node/demo_ports.py +69 -0
  60. chartflow/node/enums.py +35 -0
  61. chartflow/node/example.py +84 -0
  62. chartflow/node/layout_utils.py +307 -0
  63. chartflow/node/main_window.py +196 -0
  64. chartflow/node/menu_bar.py +240 -0
  65. chartflow/node/node_canvas.py +1730 -0
  66. chartflow/node/node_item.py +754 -0
  67. chartflow/node/popmenu.py +472 -0
  68. chartflow/node/port_item.py +591 -0
  69. chartflow/node/qnode_editor.py +73 -0
  70. chartflow/node/selection_rect.py +53 -0
  71. chartflow/node/style_panel.py +615 -0
  72. chartflow/node/styles.py +63 -0
  73. chartflow/node/test_qnode.py +2336 -0
  74. chartflow/showcase.py +528 -0
  75. chartflow/theme.py +508 -0
  76. chartflow-0.1.dist-info/METADATA +23 -0
  77. chartflow-0.1.dist-info/RECORD +79 -0
  78. chartflow-0.1.dist-info/WHEEL +5 -0
  79. 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
+ ]