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,596 @@
1
+ """Integration tests for qfsm library"""
2
+
3
+ from chartflow.fsm import Stage, sequence, selector
4
+ from chartflow.fsm import Stage, recursive
5
+ from chartflow.fsm import Stage, sequence
6
+
7
+
8
+ import pytest
9
+ from chartflow.fsm import Stage, Logic, Scheduler
10
+ from chartflow.fsm.decorators import recursive, listen, sequence, selector
11
+ from chartflow.fsm.meta import clear_prototypes
12
+
13
+
14
+ @pytest.fixture(autouse=True)
15
+ def reset_state():
16
+ """Reset state before each test"""
17
+ Scheduler._instance = None
18
+ clear_prototypes()
19
+ Stage._Stage__id__ = {}
20
+ Logic._Logic__names__ = {}
21
+ Logic._Logic__types__ = {}
22
+ yield
23
+ Scheduler._instance = None
24
+
25
+
26
+ class TestFullWorkflow:
27
+ """Test complete state machine workflows"""
28
+
29
+ def test_traffic_light_workflow(self):
30
+ """Test traffic light state machine"""
31
+
32
+ class TrafficLight(Logic):
33
+ NAME = "TrafficLight"
34
+ LIMIT = 3
35
+
36
+ def __init__(self):
37
+ self.transitions = []
38
+ super().__init__()
39
+
40
+ def initial(self, inst):
41
+ return "red"
42
+
43
+ def red(self, inst):
44
+ return "green"
45
+
46
+ def green(self, inst):
47
+ return "yellow"
48
+
49
+ @recursive
50
+ def yellow(self, inst):
51
+ return "red"
52
+
53
+ def onChanged(self, src, dst, inst):
54
+ self.transitions.append((src, dst))
55
+
56
+ light = TrafficLight()
57
+ scheduler = Scheduler()
58
+
59
+ # First tick - initial to red
60
+ scheduler.handle()
61
+ assert light._current == "red"
62
+
63
+ # Second tick - red to green
64
+ scheduler.handle()
65
+ assert light._current == "green"
66
+
67
+ # Third tick - green to yellow, then yellow -> red (recursive in same tick)
68
+ scheduler.handle()
69
+ assert light._current == "red" # Yellow is recursive, immediately goes to red
70
+
71
+ def test_counter_workflow(self):
72
+ """Test counter with recursive state"""
73
+
74
+ class Counter(Logic):
75
+ NAME = "Counter"
76
+ LIMIT = 10
77
+
78
+ def __init__(self):
79
+ self.count = 0
80
+ super().__init__()
81
+
82
+ @recursive
83
+ def counting(self, inst):
84
+ self.count += 1
85
+ if self.count >= 5:
86
+ return "done"
87
+ return "counting"
88
+
89
+ def done(self, inst):
90
+ return None
91
+
92
+ counter = Counter()
93
+ scheduler = Scheduler()
94
+
95
+ counter._current = "counting"
96
+ scheduler.handle()
97
+
98
+ assert counter.count == 5
99
+ assert counter._current == "done"
100
+
101
+
102
+ class TestMultipleInstances:
103
+ """Test multiple interacting logic instances"""
104
+
105
+ def test_priority_ordering(self):
106
+ """Test that higher priority instances are processed first"""
107
+
108
+ class LowPriority(Logic):
109
+ NAME = "LowPriority"
110
+ PRIORITY = 0
111
+
112
+ def __init__(self):
113
+ self.order = None
114
+ super().__init__()
115
+
116
+ def state(self, inst):
117
+ if self.order is None:
118
+ self.order = Logic._Logic__execution_order
119
+ return None
120
+
121
+ class HighPriority(Logic):
122
+ NAME = "HighPriority"
123
+ PRIORITY = 10
124
+
125
+ def __init__(self):
126
+ self.order = None
127
+ super().__init__()
128
+
129
+ def state(self, inst):
130
+ if self.order is None:
131
+ self.order = Logic._Logic__execution_order
132
+ return None
133
+
134
+ # Track execution order
135
+ Logic._Logic__execution_order = 0
136
+
137
+ low = LowPriority()
138
+ high = HighPriority()
139
+
140
+ scheduler = Scheduler()
141
+ scheduler.handle()
142
+
143
+ # Both should have been processed
144
+ assert low._isEntered
145
+ assert high._isEntered
146
+
147
+ def test_instance_referencing(self):
148
+ """Test that instances can reference each other"""
149
+
150
+ class Worker(Logic):
151
+ NAME = "Worker"
152
+
153
+ def __init__(self):
154
+ self.partner = None
155
+ super().__init__()
156
+
157
+ def state(self, inst):
158
+ if self.partner is None:
159
+ # Find another worker
160
+ others = self.ref("Worker")
161
+ for other in others:
162
+ if other is not self:
163
+ self.partner = other
164
+ break
165
+ return None
166
+
167
+ worker1 = Worker()
168
+ worker2 = Worker()
169
+
170
+ scheduler = Scheduler()
171
+ scheduler.handle()
172
+
173
+ assert worker1.partner is worker2 or worker1.partner is None
174
+ assert worker2.partner is worker1 or worker2.partner is None
175
+
176
+
177
+ class TestEventDriven:
178
+ """Test event-driven state changes"""
179
+
180
+ def test_event_listener(self):
181
+ """Test that @listen decorator correctly marks methods as listeners"""
182
+
183
+ class EventDriven(Logic):
184
+ NAME = "EventDriven"
185
+
186
+ def __init__(self):
187
+ super().__init__()
188
+
189
+ def waiting(self, inst):
190
+ return None
191
+
192
+ @listen("trigger")
193
+ def on_trigger(self, data):
194
+ pass
195
+
196
+ logic = EventDriven()
197
+
198
+ # Verify __listens__ is correctly populated
199
+ assert "trigger" in logic.__listens__
200
+ assert "on_trigger" in [h[0] for h in logic.__listens__["trigger"]]
201
+ # Verify @listen methods are not in __states__
202
+ assert "on_trigger" not in logic.__states__
203
+
204
+
205
+ class TestBehaviorTree:
206
+ """Test behavior tree composites in integration"""
207
+
208
+ def test_sequence_all_succeed(self):
209
+ """Test sequence when all children succeed"""
210
+
211
+ class SequenceTest(Logic):
212
+ NAME = "SequenceTest"
213
+
214
+ def __init__(self):
215
+ self.executed = []
216
+ super().__init__()
217
+
218
+ @sequence("step2", "step3")
219
+ def step1(self, it):
220
+ self.executed.append(1)
221
+ return True
222
+
223
+ def step2(self, it):
224
+ self.executed.append(2)
225
+ return True
226
+
227
+ def step3(self, it):
228
+ self.executed.append(3)
229
+ return True
230
+
231
+ logic = SequenceTest()
232
+ scheduler = Scheduler()
233
+
234
+ # Tick-based execution: each handle() executes one step
235
+ scheduler.handle() # Executes step1 (enters sequence) and step2
236
+ scheduler.handle() # Executes step3
237
+
238
+ assert 2 in logic.executed
239
+ assert 3 in logic.executed
240
+
241
+ def test_sequence_fails_on_false(self):
242
+ """Test sequence stops when a child returns False"""
243
+
244
+ class SequenceFailTest(Logic):
245
+ NAME = "SequenceFailTest"
246
+
247
+ def __init__(self):
248
+ self.executed = []
249
+ super().__init__()
250
+
251
+ @sequence("step2", "step3")
252
+ def step1(self, it):
253
+ return True
254
+
255
+ def step2(self, it):
256
+ self.executed.append(2)
257
+ return False # Fail - should stop sequence
258
+
259
+ def step3(self, it):
260
+ self.executed.append(3)
261
+ return True
262
+
263
+ logic = SequenceFailTest()
264
+ scheduler = Scheduler()
265
+
266
+ scheduler.handle()
267
+
268
+ assert 2 in logic.executed
269
+ assert 3 not in logic.executed # step3 should not execute
270
+
271
+ def test_selector_integration(self):
272
+ """Test selector composite in full workflow"""
273
+
274
+ class SelectorTest(Logic):
275
+ NAME = "SelectorTest"
276
+
277
+ def __init__(self):
278
+ self.tried = []
279
+ super().__init__()
280
+
281
+ @selector("option1", "option2")
282
+ def selector_state(self, it):
283
+ self.tried.append("selector")
284
+ return False # Fail, try options
285
+
286
+ def option1(self, it):
287
+ self.tried.append("option1")
288
+ return True # Success
289
+
290
+ def option2(self, it):
291
+ self.tried.append("option2")
292
+ return None
293
+
294
+ logic = SelectorTest()
295
+ scheduler = Scheduler()
296
+
297
+ logic._current = "selector_state"
298
+
299
+ scheduler.handle()
300
+
301
+ # Selector executes children directly, not the parent state's method
302
+ assert "option1" in logic.tried
303
+
304
+ def test_behavior_tree_with_listen_decorator(self):
305
+ """Test that @listen methods are not treated as states"""
306
+
307
+ class BehaviorWithListen(Logic):
308
+ NAME = "BehaviorWithListen"
309
+
310
+ def __init__(self):
311
+ self.executed = []
312
+ self.event_received = False
313
+ super().__init__()
314
+
315
+ @sequence("step2", "step3")
316
+ def step1(self, it):
317
+ self.executed.append(1)
318
+ return True
319
+
320
+ def step2(self, it):
321
+ self.executed.append(2)
322
+ return True
323
+
324
+ def step3(self, it):
325
+ self.executed.append(3)
326
+ return True
327
+
328
+ @listen("test_event")
329
+ def on_test_event(self, data):
330
+ self.event_received = True
331
+
332
+ logic = BehaviorWithListen()
333
+ scheduler = Scheduler()
334
+
335
+ # Verify __states__ doesn't include on_test_event
336
+ assert "on_test_event" not in logic.__states__
337
+ assert "step1" in logic.__states__
338
+ assert "step2" in logic.__states__
339
+ assert "step3" in logic.__states__
340
+
341
+ # Verify __listens__ is correctly populated
342
+ assert "test_event" in logic.__listens__
343
+
344
+ # Tick-based execution: each handle() executes one step
345
+ scheduler.handle() # Executes step1 (enters sequence) and step2
346
+ scheduler.handle() # Executes step3
347
+
348
+ # Verify sequence executed
349
+ assert 2 in logic.executed
350
+ assert 3 in logic.executed
351
+
352
+ def test_sequence_running_resume(self):
353
+ """Test that RUNNING (None) pauses and resumes from same child, skipping parent."""
354
+
355
+ class RunningTest(Logic):
356
+ NAME = "RunningTest"
357
+
358
+ def __init__(self):
359
+ self.first_calls = 0
360
+ self.second_calls = 0
361
+ super().__init__()
362
+
363
+ @sequence("child1", "child2")
364
+ def parent(self, it):
365
+ self.first_calls += 1
366
+ it.now = self.now # Set marker on first call
367
+ return True
368
+
369
+ def child1(self, it):
370
+ self.second_calls += 1
371
+ # On first tick, return RUNNING (None)
372
+ # On subsequent ticks, if it.now != self.now, succeed
373
+ if not hasattr(it, 'now'):
374
+ it.now = 0
375
+ if it.now == self.now:
376
+ return None # RUNNING
377
+ it.now = self.now
378
+ return True
379
+
380
+ def child2(self, it):
381
+ return True
382
+
383
+ logic = RunningTest()
384
+ scheduler = Scheduler()
385
+
386
+ # Tick 1: parent executes, child1 returns RUNNING
387
+ scheduler.handle()
388
+ assert logic.first_calls == 1 # Parent executed once
389
+ assert logic.second_calls == 1 # Child1 executed once
390
+
391
+ # Tick 2: should resume child1 directly, skip parent
392
+ scheduler.handle()
393
+ assert logic.first_calls == 1 # Parent still only executed once
394
+ assert logic.second_calls == 2 # Child1 executed again (resumed)
395
+ # Sequence pauses after child1 succeeds (tick-based execution)
396
+
397
+ # Tick 3: continue sequence, execute child2
398
+ scheduler.handle()
399
+ assert logic.first_calls == 1 # Parent still only executed once
400
+ assert logic.second_calls == 2 # Child1 not executed again
401
+ # Sequence completes after child2
402
+
403
+ # Tick 4: new sequence starts from beginning
404
+ scheduler.handle()
405
+ assert logic.first_calls == 2 # Parent executed again
406
+ assert logic.second_calls == 3 # Child1 executed again
407
+
408
+ def test_nested_behavior_tree_indentation(self, capsys):
409
+ """Test that nested behavior trees have correct indentation in output."""
410
+
411
+ class TestLogic(Stage):
412
+ NAME = "TestIndent"
413
+
414
+ @sequence("child_seq", "child_sel")
415
+ def root(self, it):
416
+ return True
417
+
418
+ @sequence("leaf_a", "leaf_b")
419
+ def child_seq(self, it):
420
+ return True
421
+
422
+ @selector("leaf_c", "leaf_d")
423
+ def child_sel(self, it):
424
+ return True
425
+
426
+ def leaf_a(self, it):
427
+ return True
428
+
429
+ def leaf_b(self, it):
430
+ return True
431
+
432
+ def leaf_c(self, it):
433
+ return False
434
+
435
+ def leaf_d(self, it):
436
+ return True
437
+
438
+ logic = TestLogic()
439
+ logic.handleStage()
440
+
441
+ captured = capsys.readouterr()
442
+ output = captured.out
443
+
444
+ # 老版本输出格式:使用 std.info,行为树内部转换使用 [...] 作为 tag
445
+ # 例如:[...] root -> child_seq
446
+ lines = output.split('\n')
447
+
448
+ # 检查是否有行为树内部转换的输出(使用 [...] 作为 tag)
449
+ bt_transitions = []
450
+ for line in lines:
451
+ if '[...]' in line and '->' in line:
452
+ bt_transitions.append(line)
453
+
454
+ # 应该至少有行为树内部的转换输出
455
+ assert len(bt_transitions) > 0, f"Expected behavior tree transitions with [...] tag, got: {output}"
456
+
457
+ # 检查是否包含从 root 到 child_seq 的转换
458
+ root_to_child = any('root -> child_seq' in line for line in bt_transitions)
459
+ assert root_to_child, f"Expected transition root -> child_seq, got: {bt_transitions}"
460
+
461
+
462
+ class TestLifecycleManagement:
463
+ """Test instance lifecycle management"""
464
+
465
+ def test_instance_cleanup(self):
466
+ """Test that dead instances are cleaned up"""
467
+
468
+ class ShortLived(Logic):
469
+ NAME = "ShortLived"
470
+
471
+ def __init__(self):
472
+ self.ticks = 0
473
+ super().__init__()
474
+
475
+ def state(self, inst):
476
+ self.ticks += 1
477
+ if self.ticks >= 2:
478
+ self.kill()
479
+ return None
480
+
481
+ logic = ShortLived()
482
+ scheduler = Scheduler()
483
+
484
+ assert logic in scheduler
485
+
486
+ scheduler.handle()
487
+ assert logic.alive
488
+
489
+ scheduler.handle()
490
+ assert not logic.alive
491
+
492
+ scheduler.handle()
493
+ assert logic not in scheduler
494
+
495
+ def test_enable_disable(self):
496
+ """Test that disabled instances are skipped"""
497
+
498
+ class Toggleable(Logic):
499
+ NAME = "Toggleable"
500
+
501
+ def __init__(self):
502
+ self.ticks = 0
503
+ super().__init__()
504
+
505
+ def state(self, inst):
506
+ self.ticks += 1
507
+ return None
508
+
509
+ logic = Toggleable()
510
+ scheduler = Scheduler()
511
+
512
+ scheduler.handle()
513
+ assert logic.ticks == 1
514
+
515
+ logic.disable()
516
+ scheduler.handle()
517
+ assert logic.ticks == 1 # Should not increment
518
+
519
+ logic.enable()
520
+ scheduler.handle()
521
+ assert logic.ticks == 2
522
+
523
+
524
+ class TestRecursiveExecution:
525
+ """Test recursive state execution with None return"""
526
+
527
+ def test_recursive_with_none_return(self):
528
+ """Test that recursive state with None return re-executes"""
529
+
530
+ class TestLogic(Stage):
531
+ NAME = "TestRecursive"
532
+
533
+ def __init__(self):
534
+ self.count = 0
535
+ super().__init__()
536
+
537
+ @recursive
538
+ def counter(self, it):
539
+ self.count += 1
540
+ if self.count >= 3:
541
+ return "done"
542
+ return None # Should re-execute because of @recursive
543
+
544
+ def done(self, it):
545
+ return None
546
+
547
+ logic = TestLogic()
548
+ logic._current = "counter"
549
+
550
+ # Single tick should execute counter 3 times
551
+ logic.handleStage(log=False)
552
+
553
+ assert logic.count == 3, f"Expected count=3, got {logic.count}"
554
+ assert logic.current == "done"
555
+
556
+
557
+ class TestBehaviorTreeBREAK:
558
+ """Test behavior tree BREAK to external state"""
559
+
560
+ def test_break_to_external_prints_transition(self, capsys):
561
+ """Test that BREAK to external state prints the transition"""
562
+
563
+ class TestLogic(Stage):
564
+ NAME = "TestBREAK"
565
+
566
+ @sequence("child1", "child2")
567
+ def root(self, it):
568
+ return True
569
+
570
+ def child1(self, it):
571
+ return True
572
+
573
+ def child2(self, it):
574
+ return "external" # BREAK to external state
575
+
576
+ def external(self, it):
577
+ return "exit"
578
+
579
+ def exit(self, it):
580
+ return None
581
+
582
+ logic = TestLogic()
583
+ # Execute multiple ticks to complete the behavior tree
584
+ for _ in range(5):
585
+ logic.handleStage()
586
+ if logic.current not in ["root", "child1", "child2"]:
587
+ break
588
+
589
+ captured = capsys.readouterr()
590
+ output = captured.out
591
+
592
+ # Should print the BREAK transition
593
+ assert "child2 -> external" in output, f"Expected 'child2 -> external' in output, got: {output}"
594
+
595
+ # Should use [LogicName.fbm] tag for external transition
596
+ assert f"[{logic.name}.fbm]" in output, f"Expected '[{logic.name}.fbm]' tag in output"