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,46 @@
1
+ # -*- coding: utf-8 -*-
2
+ import time
3
+ from random import random
4
+ from chartflow.fsm import *
5
+
6
+
7
+ class MyLogic(Logic):
8
+ NAME = "My"
9
+
10
+ @sequence("second", "third")
11
+ def first(self, it):
12
+ it.now = self.now # 在 BlackBoard 上动态添加 now 属性
13
+ return random() < 0.7
14
+
15
+
16
+ def second(self, it):
17
+ if it.now == self.now:
18
+ return None
19
+ return random() > 0.5
20
+
21
+
22
+ @selector("A", "B")
23
+ def third(self, it):
24
+ return random() < 0.5
25
+
26
+ def A(self, it):
27
+ if random() < 0.25: return
28
+ return random() < 0.5
29
+
30
+ def B(self, it):
31
+ if random() < 0.25: return
32
+ return random() < 0.5
33
+
34
+
35
+ @listen("system")
36
+ def my_handle(self, data):
37
+ print(f"{self.name} recv event: {data}")
38
+
39
+
40
+ # 创建调度器
41
+ sch = Scheduler()
42
+ cl0 = Logic("My") # inst 默认是 BlackBoard()
43
+
44
+ for i in range(10):
45
+ print("TICK{}".format(sch.tick))
46
+ sch.handle()
chartflow/fsm/logic.py ADDED
@@ -0,0 +1,447 @@
1
+ from __future__ import annotations
2
+ """
3
+ qfsm Logic Module - Scheduled state machine instances
4
+
5
+ This module provides the Logic class which extends Stage with scheduler
6
+ integration, lifecycle management, and instance referencing capabilities.
7
+ """
8
+
9
+
10
+ from typing import Any, ClassVar, Self
11
+
12
+ from chartflow.fsm.stage import Stage
13
+ from chartflow.fsm.scheduler import Scheduler
14
+
15
+
16
+ class Logic(Stage):
17
+ """
18
+ Scheduled state machine instance with lifecycle management.
19
+
20
+ Logic extends Stage with:
21
+ - Automatic registration with the Scheduler singleton
22
+ - 4-phase lifecycle (_check, _enter, _step, _exit)
23
+ - Instance referencing (ref, refLast, refNext, etc.)
24
+ - Priority-based execution
25
+
26
+ Class Attributes:
27
+ NAME: The display name for this logic type (required)
28
+ PRIORITY: Execution priority (higher = earlier, default: 0)
29
+ LOG: Whether to log state transitions (default: True)
30
+
31
+ Instance Attributes:
32
+ alive: Whether instance should continue processing
33
+ enabled: Whether instance is currently enabled
34
+ uid: Unique instance identifier
35
+ priority: Execution priority for this instance
36
+
37
+ Example:
38
+ class EnemyAI(Logic):
39
+ NAME = "EnemyAI"
40
+ PRIORITY = 10
41
+
42
+ def __init__(self, enemy):
43
+ super().__init__(enemy)
44
+ self.health = 100
45
+
46
+ def idle(self):
47
+ if self._inst.player_nearby:
48
+ return "chase"
49
+
50
+ def chase(self):
51
+ if not self._inst.player_nearby:
52
+ return "idle"
53
+ self._inst.move_toward_player()
54
+
55
+ Lifecycle:
56
+ 1. __init__ -> auto-registers with Scheduler
57
+ 2. _check -> validate if should run
58
+ 3. _enter -> first-time initialization (calls onStart)
59
+ 4. _step -> per-tick execution (calls handleStage, onStep)
60
+ 5. _exit -> cleanup when dead (calls onStop)
61
+ """
62
+
63
+ # Class attributes
64
+ NAME: ClassVar[str] = "Logic"
65
+ PRIORITY: ClassVar[int] = 0
66
+ LOG: ClassVar[bool] = True
67
+
68
+ # Class-level instance tracking by name (name -> {uid: instance})
69
+ # Using explicit mangled name to allow tests to reset state
70
+ _Logic__names__: ClassVar[dict[str, dict[int, Logic]]] = {}
71
+
72
+ def __init__(self, inst: Any = None) -> None:
73
+ """
74
+ Initialize Logic and auto-register with Scheduler.
75
+
76
+ Args:
77
+ inst: The bound instance (e.g., game entity)
78
+
79
+ Note:
80
+ Automatically calls Scheduler().add(self) to register
81
+ for scheduled execution.
82
+ """
83
+ # Instance lifecycle attributes
84
+ self._alive: bool = True
85
+ self._enabled: bool = True
86
+ self._isEntered: bool = False
87
+
88
+ super().__init__(inst)
89
+
90
+ # Register in __names__ dictionary for instance referencing
91
+ if self.NAME not in Logic._Logic__names__:
92
+ Logic._Logic__names__[self.NAME] = {}
93
+ Logic._Logic__names__[self.NAME][self.uid] = self
94
+
95
+ # Auto-register with scheduler
96
+ Scheduler().add(self)
97
+
98
+
99
+ # ========================================================================
100
+ # Lifecycle Methods (called by Scheduler)
101
+ # ========================================================================
102
+
103
+ def _check(self, is_seek: bool) -> bool:
104
+ """
105
+ Check if instance should run this tick.
106
+
107
+ Called by Scheduler during the check phase.
108
+
109
+ Args:
110
+ is_seek: Whether this is a seek/check-only pass
111
+
112
+ Returns:
113
+ True if instance should continue processing
114
+
115
+ Override this to add custom validation logic.
116
+ Example:
117
+ def _check(self, is_seek):
118
+ # Only run if entity is active
119
+ return self._alive and self._inst.is_active
120
+ """
121
+ return self._alive and self._enabled
122
+
123
+ def _enter(self) -> None:
124
+ """
125
+ Called on first entry (before first _step).
126
+
127
+ Called by Scheduler during the enter phase.
128
+ Override onStart() for custom initialization.
129
+ """
130
+ self._isEntered = True
131
+ self.onStart(self._inst)
132
+
133
+ def _step(self) -> None:
134
+ """
135
+ Called each tick during execution.
136
+
137
+ Called by Scheduler during the step phase.
138
+ Executes handleStage() and calls onStep().
139
+ """
140
+ self.handleStage(log=self.LOG)
141
+ self.onStep(self._inst)
142
+
143
+ def _exit(self) -> None:
144
+ """
145
+ Called when instance is removed (alive=False).
146
+
147
+ Called by Scheduler during the exit phase.
148
+ Unregisters instance and calls onStop().
149
+ """
150
+ self.onStop(self._inst)
151
+
152
+ # ========================================================================
153
+ # Lifecycle Hooks (override in subclasses)
154
+ # ========================================================================
155
+
156
+ def onStart(self, inst:Any) -> None:
157
+ """Hook called on first entry. Override for initialization."""
158
+ pass
159
+
160
+ def onStep(self, inst:Any) -> None:
161
+ """Hook called each tick after handleStage. Override for per-tick logic."""
162
+ pass
163
+
164
+ def onStop(self, inst:Any) -> None:
165
+ """Hook called on removal. Override for cleanup."""
166
+ pass
167
+
168
+ def onKilled(self, inst:Any) -> None:
169
+ """Hook called when instance is killed. Override for death handling."""
170
+ pass
171
+
172
+ # ========================================================================
173
+ # Reference Methods
174
+ # ========================================================================
175
+
176
+ def ref(self, name: str) -> Logic | list[Logic] | None:
177
+ """
178
+ Get instance(s) by name.
179
+
180
+ Args:
181
+ name: The NAME of the Logic class to find
182
+
183
+ Returns:
184
+ - Single Logic instance if only one exists
185
+ - List of Logic instances if multiple exist
186
+ - None if no instances exist
187
+
188
+ Example:
189
+ >>> enemy = self.ref("EnemyAI")
190
+ >>> players = self.ref("PlayerController") # May return list
191
+ """
192
+ # First try to find by NAME (e.g., "TestLogic")
193
+ instances = Logic._Logic__names__.get(name, {})
194
+ if instances:
195
+ values = list(instances.values())
196
+ if len(values) == 1:
197
+ return values[0]
198
+ return values.copy()
199
+
200
+ # If not found, try to find by full name (e.g., "TestLogic0")
201
+ # Parse name to extract NAME and uid
202
+ for name_key, name_instances in Logic._Logic__names__.items():
203
+ for uid, instance in name_instances.items():
204
+ if instance.name == name:
205
+ return instance
206
+
207
+ return None
208
+
209
+ def refLast(self) -> Logic | None:
210
+ """
211
+ Get the previous instance with the same NAME.
212
+
213
+ Returns:
214
+ Previous instance in the same NAME group, or None
215
+
216
+ Example:
217
+ >>> prev = self.refLast() # Get previous EnemyAI instance
218
+ """
219
+ instances = Logic._Logic__names__.get(self.NAME, {})
220
+ if not instances:
221
+ return None
222
+
223
+ # Get sorted list of uids to find previous
224
+ uids = sorted(instances.keys())
225
+ try:
226
+ idx = uids.index(self.uid)
227
+ if idx > 0:
228
+ return instances[uids[idx - 1]]
229
+ except ValueError:
230
+ pass
231
+ return None
232
+
233
+ def refNext(self) -> Logic | None:
234
+ """
235
+ Get the next instance with the same NAME.
236
+
237
+ Returns:
238
+ Next instance in the same NAME group, or None
239
+
240
+ Example:
241
+ >>> next_enemy = self.refNext() # Get next EnemyAI instance
242
+ """
243
+ instances = Logic._Logic__names__.get(self.NAME, {})
244
+ if not instances:
245
+ return None
246
+
247
+ # Get sorted list of uids to find next
248
+ uids = sorted(instances.keys())
249
+ try:
250
+ idx = uids.index(self.uid)
251
+ if idx < len(uids) - 1:
252
+ return instances[uids[idx + 1]]
253
+ except ValueError:
254
+ pass
255
+ return None
256
+
257
+ def refAdjacent(self) -> tuple[Logic | None, Logic | None]:
258
+ """
259
+ Get both previous and next instances with the same NAME.
260
+
261
+ Returns:
262
+ Tuple of (previous_instance, next_instance)
263
+
264
+ Example:
265
+ >>> prev, next = self.refAdjacent()
266
+ >>> if prev:
267
+ ... print(f"Previous: {prev.uid}")
268
+ >>> if next:
269
+ ... print(f"Next: {next.uid}")
270
+ """
271
+ return (self.refLast(), self.refNext())
272
+
273
+ def refHead(self) -> Logic | None:
274
+ """
275
+ Get the first instance with the same NAME.
276
+
277
+ Returns:
278
+ First instance in the NAME group, or None
279
+
280
+ Example:
281
+ >>> first_enemy = self.refHead() # Get first EnemyAI
282
+ """
283
+ instances = Logic._Logic__names__.get(self.NAME, {})
284
+ if not instances:
285
+ return None
286
+ # Return instance with smallest uid
287
+ min_uid = min(instances.keys())
288
+ return instances[min_uid]
289
+
290
+ def refEnd(self) -> Logic | None:
291
+ """
292
+ Get the last instance with the same NAME.
293
+
294
+ Returns:
295
+ Last instance in the NAME group, or None
296
+
297
+ Example:
298
+ >>> last_enemy = self.refEnd() # Get last EnemyAI
299
+ """
300
+ instances = Logic._Logic__names__.get(self.NAME, {})
301
+ if not instances:
302
+ return None
303
+ # Return instance with largest uid
304
+ max_uid = max(instances.keys())
305
+ return instances[max_uid]
306
+
307
+ # ========================================================================
308
+ # Properties
309
+ # ========================================================================
310
+
311
+ @property
312
+ def scheduler(self):
313
+ return Scheduler()
314
+
315
+ @property
316
+ def alive(self) -> bool:
317
+ """Whether instance should continue processing."""
318
+ return self._alive
319
+
320
+ @alive.setter
321
+ def alive(self, value: bool) -> None:
322
+ """Set alive state. Setting to False triggers removal."""
323
+ self._alive = value
324
+ if not value:
325
+ self.onKilled(self._inst)
326
+
327
+ @property
328
+ def enabled(self) -> bool:
329
+ """Whether instance is currently enabled."""
330
+ return self._enabled
331
+
332
+ @enabled.setter
333
+ def enabled(self, value: bool) -> None:
334
+ """Enable or disable instance processing."""
335
+ self._enabled = value
336
+
337
+ @property
338
+ def priority(self) -> int:
339
+ """Execution priority (higher = earlier)."""
340
+ return self.PRIORITY
341
+
342
+ # ========================================================================
343
+ # Control Methods
344
+ # ========================================================================
345
+
346
+ def kill(self) -> None:
347
+ """Mark instance for removal (sets alive=False)."""
348
+ self._alive = False
349
+ # Remove from __names__ registry
350
+ if self.NAME in Logic._Logic__names__:
351
+ if self.uid in Logic._Logic__names__[self.NAME]:
352
+ del Logic._Logic__names__[self.NAME][self.uid]
353
+ # Clean up empty dicts
354
+ if not Logic._Logic__names__[self.NAME]:
355
+ del Logic._Logic__names__[self.NAME]
356
+
357
+ def enable(self) -> None:
358
+ """Enable instance processing."""
359
+ self._enabled = True
360
+
361
+ def disable(self) -> None:
362
+ """Disable instance processing."""
363
+ self._enabled = False
364
+
365
+
366
+ # ========================================================================
367
+ # Scheduling Methods
368
+ # ========================================================================
369
+
370
+ def schedule(self, delay: int, callback: Any) -> None:
371
+ """
372
+ Schedule a callback to run after a delay.
373
+
374
+ Args:
375
+ delay: Number of ticks to wait
376
+ callback: Function to call
377
+ """
378
+ # Store scheduled callback (implementation depends on scheduler)
379
+ if not hasattr(self, '_scheduled'):
380
+ self._scheduled: list[tuple[int, Any]] = []
381
+ self._scheduled.append((delay, callback))
382
+
383
+ def cancelSchedule(self, callback: Any | None = None) -> bool:
384
+ """
385
+ Cancel scheduled callbacks.
386
+
387
+ Args:
388
+ callback: Specific callback to cancel (None = all)
389
+
390
+ Returns:
391
+ True if any callbacks were cancelled
392
+ """
393
+ if not hasattr(self, '_scheduled'):
394
+ return False
395
+
396
+ if callback is None:
397
+ removed = len(self._scheduled) > 0
398
+ self._scheduled.clear()
399
+ return removed
400
+
401
+ original_len = len(self._scheduled)
402
+ self._scheduled = [(d, cb) for d, cb in self._scheduled if cb != callback]
403
+ return len(self._scheduled) < original_len
404
+
405
+ # ========================================================================
406
+ # Queue Methods (for priority queue manipulation)
407
+ # ========================================================================
408
+
409
+ def productQueue(self) -> None:
410
+ """Move to front of priority queue (highest priority)."""
411
+ # This would be implemented by the scheduler
412
+ pass
413
+
414
+ def showQueue(self) -> None:
415
+ """Show current position in queue (for debugging)."""
416
+ scheduler = Scheduler()
417
+ if self in scheduler.updates:
418
+ idx = scheduler.updates.index(self)
419
+ print(f"{self._name} at position {idx}")
420
+
421
+ def jumpQueue(self, target: Logic) -> None:
422
+ """Jump to position relative to target instance."""
423
+ # Implementation depends on scheduler internals
424
+ pass
425
+
426
+ def accelerate(self, amount: int = 1) -> None:
427
+ """Increase priority temporarily."""
428
+ # Implementation depends on scheduler internals
429
+ pass
430
+
431
+ def __repr__(self) -> str:
432
+ """Detailed string representation."""
433
+ return (
434
+ f"{self.__class__.__name__}("
435
+ f"name='{self._name}', "
436
+ f"uid={self._uid}, "
437
+ f"alive={self._alive}, "
438
+ f"enabled={self._enabled}, "
439
+ f"state={self._current}, "
440
+ f"priority={self.PRIORITY}"
441
+ f")"
442
+ )
443
+
444
+
445
+ __all__ = [
446
+ 'Logic',
447
+ ]