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,427 @@
1
+ from __future__ import annotations
2
+ """
3
+ qfsm Scheduler Module
4
+
5
+ A singleton scheduler class that manages Logic instances in priority queues,
6
+ handling the 4-phase lifecycle: _check, _enter, _step, _exit.
7
+ """
8
+
9
+ from PyQt6.QtWidgets import QApplication
10
+
11
+
12
+
13
+ import bisect
14
+ import sys
15
+ import traceback
16
+ from typing import Any, Self
17
+
18
+ from chartflow.fsm.utils import GetES
19
+
20
+ # Qt integration for processEvents during long operations
21
+ _qt_app = None
22
+ def _process_qt_events():
23
+ """Process Qt events if available, to keep UI responsive during long operations."""
24
+ global _qt_app
25
+ if _qt_app is None:
26
+ try:
27
+ _qt_app = QApplication.instance()
28
+ except ImportError:
29
+ pass
30
+ if _qt_app:
31
+ _qt_app.processEvents()
32
+
33
+
34
+ # Forward reference for Logic type - avoids circular imports
35
+ # Logic class is expected to have: NAME, PRIORITY, alive, enabled, _isEntered
36
+ # and methods: _check(), _enter(), _step(), _exit()
37
+
38
+
39
+ class Scheduler:
40
+ """
41
+ Singleton scheduler for managing Logic instances.
42
+
43
+ The Scheduler maintains priority-sorted queues of Logic instances and
44
+ orchestrates their lifecycle through four phases:
45
+ 1. Check - validate instance state via _check()
46
+ 2. Enter - initialize instances via _enter()
47
+ 3. Step - execute logic via _step()
48
+ 4. Exit - cleanup dead instances via _exit()
49
+
50
+ Higher PRIORITY values are processed first.
51
+
52
+ Example:
53
+ >>> scheduler = Scheduler()
54
+ >>> scheduler.login(MyLogicClass)
55
+ >>> instance = scheduler.add("MyLogic", some_instance)
56
+ >>> scheduler.handle() # Process one tick
57
+ """
58
+
59
+ _instance: Scheduler | None = None
60
+ _global_enabled: bool = True
61
+
62
+ def __new__(cls) -> Self:
63
+ """Singleton pattern - ensures only one Scheduler instance exists."""
64
+ if cls._instance is None:
65
+ cls._instance = super().__new__(cls)
66
+ cls._instance._initialized = False
67
+ return cls._instance
68
+
69
+ def __init__(self) -> None:
70
+ """Initialize the Scheduler with empty queues."""
71
+ if self._initialized:
72
+ return
73
+
74
+ # Registered class prototypes: {name: class}
75
+ self.prototypes: dict[str, type] = {}
76
+
77
+ # Active Logic instances, sorted by PRIORITY (descending)
78
+ self.updates: list[Any] = []
79
+
80
+ # Logic instances waiting to be added to updates
81
+ self.waits: list[Any] = []
82
+
83
+ # Instance-level enable flag
84
+ self._enabled: bool = True
85
+
86
+ # Tick counter - increments each handle() call
87
+ self._tick: int = 0
88
+
89
+ # Event system
90
+ self._eda = GetES()
91
+
92
+ # Listen for system_next event to trigger handle()
93
+ self._eda.listenFor("system_next", self._onNextTick)
94
+
95
+ self._initialized = True
96
+
97
+ def login(self, prototype: type) -> None:
98
+ """
99
+ Register a Logic class prototype with the Scheduler.
100
+
101
+ Validates that the class has a valid NAME attribute.
102
+ Called automatically by StageMachineLogicMeta during class creation.
103
+
104
+ Args:
105
+ prototype: A Logic subclass to register
106
+
107
+ Raises:
108
+ ValueError: If NAME is empty or not defined
109
+
110
+ Example:
111
+ >>> class MyLogic(Logic):
112
+ ... NAME = "MyLogic"
113
+ >>> Scheduler().login(MyLogic)
114
+ """
115
+ name: str = getattr(prototype, 'NAME', '')
116
+
117
+ if not name:
118
+ raise ValueError(
119
+ f"Cannot register {prototype.__name__}: "
120
+ f"NAME attribute is empty or not defined"
121
+ )
122
+
123
+ if name[-1].isdigit():
124
+ raise ValueError(
125
+ f"Cannot register {prototype.__name__}: "
126
+ f"NAME '{name}' must not end with a digit"
127
+ )
128
+
129
+ if name in self.prototypes:
130
+ # Allow re-registration for hot-reloading scenarios
131
+ pass
132
+
133
+ self.prototypes[name] = prototype
134
+
135
+ def add(self, obj_or_name: Any, *args: Any) -> Any | None:
136
+ """
137
+ Add a Logic instance to the scheduling queue.
138
+
139
+ Can accept either:
140
+ - A Logic instance directly
141
+ - A string name of a registered prototype to instantiate
142
+
143
+ Args:
144
+ obj_or_name: Either a Logic instance or registered class name
145
+ *args: Arguments to pass when instantiating from name
146
+
147
+ Returns:
148
+ The added Logic instance, or None if creation failed
149
+
150
+ Example:
151
+ >>> # Add by instance
152
+ >>> scheduler.add(MyLogic())
153
+ >>> # Add by name with arguments
154
+ >>> scheduler.add("MyLogic", some_instance)
155
+ """
156
+ instance: Any | None = None
157
+
158
+ match obj_or_name:
159
+ case str(name):
160
+ # Create from prototype name
161
+ if name not in self.prototypes:
162
+ raise KeyError(
163
+ f"Unknown Logic prototype: '{name}'. "
164
+ f"Registered: {list(self.prototypes.keys())}"
165
+ )
166
+ prototype = self.prototypes[name]
167
+ instance = prototype(*args)
168
+
169
+ case obj if hasattr(obj, '_check') and hasattr(obj, '_step'):
170
+ # Add existing instance (duck typing for Logic-like objects)
171
+ instance = obj
172
+
173
+ case _:
174
+ raise TypeError(
175
+ f"Expected Logic-like instance or str name, got {type(obj_or_name).__name__}"
176
+ )
177
+
178
+ if instance is not None:
179
+ # Check for duplicate ID in waits or updates
180
+ instance_id = id(instance)
181
+ for existing in self.waits:
182
+ if id(existing) == instance_id:
183
+ return instance # Already in waits, skip
184
+ for existing in self.updates:
185
+ if id(existing) == instance_id:
186
+ return instance # Already in updates, skip
187
+ self.waits.append(instance)
188
+
189
+ return instance
190
+
191
+ def handle(self) -> None:
192
+ """
193
+ Main processing loop - executes one tick of the scheduler.
194
+
195
+ Performs four phases in order:
196
+ 1. **Promote**: Move instances from waits to updates (sorted by PRIORITY)
197
+ 2. **Check**: Call _check() on all instances in updates
198
+ 3. **Enter**: Call _enter() on instances that haven't entered yet
199
+ 4. **Step**: Call _step() on all entered instances
200
+ 5. **Exit**: Remove dead instances (alive=False) and call _exit()
201
+
202
+ The scheduler respects both global and instance-level enable flags.
203
+ Disabled instances are skipped during processing.
204
+
205
+ Example:
206
+ >>> scheduler = Scheduler()
207
+ >>> while running:
208
+ ... scheduler.handle()
209
+ """
210
+ if not (self._enabled and Scheduler._global_enabled):
211
+ return
212
+
213
+ # Increment tick counter
214
+ self._tick += 1
215
+
216
+ # Phase 1: Promote waiting instances to updates queue
217
+ self._promote_waits()
218
+
219
+ if not self.updates:
220
+ return
221
+
222
+ # Phase 2: Check - validate all instances
223
+ self._phase_check()
224
+
225
+ # Phase 3: Enter - initialize new instances
226
+ self._phase_enter()
227
+
228
+ # Phase 4: Step - execute logic
229
+ self._phase_step()
230
+
231
+ # Phase 5: Exit - cleanup dead instances
232
+ self._phase_exit()
233
+
234
+ def _promote_waits(self) -> None:
235
+ """Move instances from waits to updates, maintaining priority order."""
236
+ for instance in self.waits:
237
+ self._insert_by_priority(instance)
238
+ self.waits.clear()
239
+
240
+ def _onNextTick(self, _data: Any) -> None:
241
+ """Handle system_next event by calling handle()."""
242
+ self.handle()
243
+
244
+ def Cleanup(self) -> None:
245
+ """Cleanup resources - cancel event listener."""
246
+ if hasattr(self, '_eda') and self._eda:
247
+ self._eda.cancelListen("system_next", self._onNextTick)
248
+
249
+ def __del__(self) -> None:
250
+ """Destructor - ensure cleanup is called."""
251
+ self.Cleanup()
252
+
253
+ def _insert_by_priority(self, instance: Any) -> None:
254
+ """
255
+ Insert instance into updates using binary search for efficiency.
256
+
257
+ Higher PRIORITY values are placed first (descending order).
258
+ Uses bisect for O(log n) insertion.
259
+ """
260
+ priority = getattr(instance, 'PRIORITY', 0)
261
+
262
+ # Find insertion point for descending order
263
+ # We negate priority since bisect assumes ascending order
264
+ insertion_point = bisect.bisect_left(
265
+ [-getattr(obj, 'PRIORITY', 0) for obj in self.updates],
266
+ -priority
267
+ )
268
+ self.updates.insert(insertion_point, instance)
269
+
270
+ def _phase_check(self) -> None:
271
+ """Phase 2: Call _check() on all instances."""
272
+ for instance in self.updates:
273
+ if instance.alive:
274
+ try:
275
+ instance._check(False)
276
+ except Exception:
277
+ # Errors in _check don't stop other instances, but print traceback
278
+ exc_type, exc_value, exc_tb = sys.exc_info()
279
+ tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
280
+ print(f"[{instance.name}] Error in _check:\n{tb_str}")
281
+
282
+ def _phase_enter(self) -> None:
283
+ """Phase 3: Call _enter() on instances not yet entered."""
284
+ for instance in self.updates:
285
+ if not instance.enabled or not instance.alive:
286
+ continue
287
+
288
+ # Check if already entered via _isEntered attribute
289
+ if not getattr(instance, '_isEntered', False):
290
+ try:
291
+ instance._enter()
292
+ instance._isEntered = True
293
+ except Exception:
294
+ # Mark as entered even on error to prevent retry loops, but print traceback
295
+ exc_type, exc_value, exc_tb = sys.exc_info()
296
+ tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
297
+ print(f"[{instance.name}] Error in _enter:\n{tb_str}")
298
+ instance._isEntered = True
299
+
300
+ def _phase_step(self) -> None:
301
+ """Phase 4: Call _step() on all entered, enabled instances."""
302
+ for instance in self.updates:
303
+ # Process Qt events before each instance to handle pending UI updates
304
+ _process_qt_events()
305
+
306
+ if not instance.enabled or not instance.alive:
307
+ continue
308
+
309
+ if getattr(instance, '_isEntered', False):
310
+ try:
311
+ instance._step()
312
+ except Exception:
313
+ # Errors in _step don't stop other instances, but print traceback
314
+ exc_type, exc_value, exc_tb = sys.exc_info()
315
+ tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
316
+ print(f"[{instance.name}] Error in _step:\n{tb_str}")
317
+
318
+ def _phase_exit(self) -> None:
319
+ """Phase 5: Remove dead instances and call _exit()."""
320
+ # Find indices of dead instances (alive=False)
321
+ dead_indices: list[int] = [
322
+ i for i, inst in enumerate(self.updates)
323
+ if not inst.alive
324
+ ]
325
+
326
+ # Process in reverse order to maintain valid indices
327
+ for idx in reversed(dead_indices):
328
+ instance = self.updates.pop(idx)
329
+ try:
330
+ instance._exit()
331
+ except Exception:
332
+ # Errors in _exit don't stop cleanup, but print traceback
333
+ exc_type, exc_value, exc_tb = sys.exc_info()
334
+ tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
335
+ print(f"[{instance.name}] Error in _exit:\n{tb_str}")
336
+
337
+ def enable(self) -> None:
338
+ """Enable scheduling at the instance level."""
339
+ self._enabled = True
340
+
341
+ def disable(self) -> None:
342
+ """Disable scheduling at the instance level."""
343
+ self._enabled = False
344
+
345
+ @staticmethod
346
+ def Enable() -> None:
347
+ """Enable scheduling globally (static method)."""
348
+ Scheduler._global_enabled = True
349
+
350
+ @staticmethod
351
+ def Disable() -> None:
352
+ """Disable scheduling globally (static method)."""
353
+ Scheduler._global_enabled = False
354
+
355
+ @property
356
+ def enabled(self) -> bool:
357
+ """Check if scheduler is enabled (both instance and global level)."""
358
+ return self._enabled and Scheduler._global_enabled
359
+
360
+ @property
361
+ def tick(self) -> int:
362
+ """Current tick counter (read-only)."""
363
+ return self._tick
364
+
365
+ def __len__(self) -> int:
366
+ """Return total number of managed instances."""
367
+ return len(self.updates) + len(self.waits)
368
+
369
+ def __contains__(self, instance: Any) -> bool:
370
+ """Check if an instance is managed by this scheduler."""
371
+ return instance in self.updates or instance in self.waits
372
+
373
+ def __repr__(self) -> str:
374
+ """String representation showing queue sizes."""
375
+ return (
376
+ f"{self.__class__.__name__}("
377
+ f"prototypes={len(self.prototypes)}, "
378
+ f"updates={len(self.updates)}, "
379
+ f"waits={len(self.waits)})"
380
+ )
381
+
382
+
383
+ def listenFor(self, event: str, callback: Any, *sources: str) -> Self:
384
+ """
385
+ Register an event listener.
386
+
387
+ Args:
388
+ event: Event name to listen for
389
+ callback: Function to call when event occurs
390
+ *sources: Optional source filters
391
+
392
+ Returns:
393
+ self for method chaining
394
+ """
395
+ self._eda.listenFor(event, callback, *sources)
396
+ return self
397
+
398
+ def cancelListen(self, event: str, callback: Any | None = None) -> bool:
399
+ """
400
+ Cancel an event listener.
401
+
402
+ Args:
403
+ event: Event name to stop listening for
404
+ callback: Specific callback to remove (None = all)
405
+
406
+ Returns:
407
+ True if listener was removed
408
+ """
409
+ return self._eda.cancelListen(event, callback)
410
+
411
+ def pushEvent(self, event: str, data: Any = None, *targets: str) -> int:
412
+ """
413
+ Push an event to listeners.
414
+
415
+ Args:
416
+ event: Event name to push
417
+ data: Event data
418
+ *targets: Optional target filters
419
+
420
+ Returns:
421
+ Number of listeners that received the event
422
+ """
423
+ return self._eda.pushEvent(event, data, *targets, source="system")
424
+
425
+
426
+
427
+ __all__ = ['Scheduler']