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,314 @@
1
+ """Tests for Stage class"""
2
+
3
+ import pytest
4
+ from chartflow.fsm.stage import Stage, _NamedNode
5
+ from chartflow.fsm.meta import clear_prototypes, StageMachineMeta
6
+ from chartflow.fsm.decorators import recursive, behavior, sequence, selector, parallel
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def reset_state():
11
+ """Reset state before each test"""
12
+ clear_prototypes()
13
+ StageMachineMeta.__ID_PROTOS__ = {}
14
+ yield
15
+
16
+
17
+ class TestNamedNode:
18
+ """Test _NamedNode base class"""
19
+
20
+ def test_parent_child_relationship(self):
21
+ """Test parent-child relationships"""
22
+ parent = _NamedNode()
23
+ parent._name = "parent"
24
+ child = _NamedNode()
25
+ child._name = "child"
26
+
27
+ parent._add_child(child)
28
+
29
+ assert child.parent is parent
30
+ assert child in parent._children
31
+
32
+ def test_remove_child(self):
33
+ """Test removing a child"""
34
+ parent = _NamedNode()
35
+ parent._name = "parent"
36
+ child = _NamedNode()
37
+ child._name = "child"
38
+
39
+ parent._add_child(child)
40
+ parent._remove_child(child)
41
+
42
+ assert child.parent is None
43
+ assert child not in parent._children
44
+
45
+
46
+ class TestStageInit:
47
+ """Test Stage initialization"""
48
+
49
+ def test_stage_requires_name(self):
50
+ """Test that Stage requires NAME"""
51
+
52
+ class BadStage(Stage):
53
+ NAME = ""
54
+
55
+ with pytest.raises(ValueError):
56
+ BadStage()
57
+
58
+ def test_stage_rejects_digit_suffix_name(self):
59
+ """Test that Stage rejects NAME ending with digit"""
60
+
61
+ with pytest.raises(ValueError):
62
+ class BadStage(Stage):
63
+ NAME = "BadStage1"
64
+
65
+ def test_stage_generates_unique_ids(self):
66
+ """Test that Stage generates unique IDs"""
67
+
68
+ class TestStage(Stage):
69
+ NAME = "TestStage"
70
+
71
+ stage1 = TestStage()
72
+ stage2 = TestStage()
73
+
74
+ assert stage1.uid != stage2.uid
75
+ assert stage1.uid == 0
76
+ assert stage2.uid == 1
77
+
78
+ def test_stage_full_name(self):
79
+ """Test that Stage generates full name with uid"""
80
+
81
+ class TestStage(Stage):
82
+ NAME = "TestStage"
83
+
84
+ stage = TestStage()
85
+
86
+ assert stage.name == "TestStage0"
87
+ assert stage._name == "TestStage0"
88
+
89
+
90
+ class TestStageStates:
91
+ """Test state execution"""
92
+
93
+ def test_stage_executes_initial_state(self):
94
+ """Test that stage executes initial state"""
95
+
96
+ class TestStage(Stage):
97
+ NAME = "TestStage"
98
+
99
+ def initial(self, inst):
100
+ return None
101
+
102
+ stage = TestStage()
103
+ stage._current = "initial"
104
+
105
+ result = stage.handleStage(log=False)
106
+
107
+ assert stage.current == "initial"
108
+
109
+ def test_stage_transitions_state(self):
110
+ """Test that stage transitions on state return"""
111
+
112
+ class TestStage(Stage):
113
+ NAME = "TestStage"
114
+
115
+ def state_a(self, inst):
116
+ return "state_b"
117
+
118
+ def state_b(self, inst):
119
+ return None
120
+
121
+ stage = TestStage()
122
+ stage._current = "state_a"
123
+
124
+ stage.handleStage(log=False)
125
+
126
+ assert stage.current == "state_b"
127
+
128
+ def test_stage_recursive_execution(self):
129
+ """Test recursive state execution"""
130
+
131
+ class TestStage(Stage):
132
+ NAME = "TestStage"
133
+
134
+ @recursive
135
+ def counter(self, inst):
136
+ if not hasattr(self, 'count'):
137
+ self.count = 0
138
+ self.count += 1
139
+ if self.count >= 3:
140
+ return "done"
141
+ return "counter"
142
+
143
+ def done(self, inst):
144
+ return None
145
+
146
+ stage = TestStage()
147
+ stage._current = "counter"
148
+
149
+ stage.handleStage(log=False)
150
+
151
+ assert stage.count == 3
152
+ assert stage.current == "done"
153
+
154
+
155
+ class TestStageBehaviorTree:
156
+ """Test behavior tree composites"""
157
+
158
+ def test_behavior_transition(self):
159
+ """Test @behavior decorator"""
160
+
161
+ class TestStage(Stage):
162
+ NAME = "TestStage"
163
+
164
+ @behavior("second")
165
+ def first(self, inst):
166
+ return True # Success triggers behavior
167
+
168
+ def second(self, inst):
169
+ return None
170
+
171
+ stage = TestStage()
172
+ stage._current = "first"
173
+
174
+ stage.handleStage(log=False)
175
+
176
+ assert stage.current == "second"
177
+
178
+ def test_sequence_execution(self):
179
+ """Test @sequence decorator"""
180
+
181
+ class TestStage(Stage):
182
+ NAME = "TestStage"
183
+
184
+ @sequence("b", "c")
185
+ def a(self, inst):
186
+ return True
187
+
188
+ def b(self, inst):
189
+ return True
190
+
191
+ def c(self, inst):
192
+ return None
193
+
194
+ stage = TestStage()
195
+ stage._current = "a"
196
+
197
+ # First call enters sequence
198
+ stage.handleStage(log=False)
199
+ # Should be at "b" now
200
+ assert stage.current == "b"
201
+
202
+ def test_selector_execution(self):
203
+ """Test @selector decorator"""
204
+
205
+ class TestStage(Stage):
206
+ NAME = "TestStage"
207
+
208
+ @selector("b", "c")
209
+ def a(self, inst):
210
+ return False # Failure triggers selector
211
+
212
+ def b(self, inst):
213
+ return True # Success stops selector
214
+
215
+ def c(self, inst):
216
+ return None
217
+
218
+ stage = TestStage()
219
+ stage._current = "a"
220
+
221
+ stage.handleStage(log=False)
222
+
223
+ # After a fails, should try b
224
+ assert stage.current == "b"
225
+
226
+
227
+ class TestStageHooks:
228
+ """Test lifecycle hooks"""
229
+
230
+ def test_onchanged_hook(self):
231
+ """Test onChanged hook is called"""
232
+
233
+ class TestStage(Stage):
234
+ NAME = "TestStage"
235
+
236
+ def __init__(self):
237
+ super().__init__()
238
+ self.changes = []
239
+
240
+ def state_a(self, inst):
241
+ return "state_b"
242
+
243
+ def state_b(self, inst):
244
+ return None
245
+
246
+ def onChanged(self, src, dst, inst):
247
+ self.changes.append((src, dst))
248
+
249
+ stage = TestStage()
250
+ stage._current = "state_a"
251
+
252
+ stage.handleStage(log=False)
253
+
254
+ assert ("state_a", "state_b") in stage.changes
255
+
256
+ def test_onloading_hook(self):
257
+ """Test onLoading hook is called"""
258
+
259
+ class TestStage(Stage):
260
+ NAME = "TestStage"
261
+
262
+ def __init__(self):
263
+ self.loaded = False
264
+ super().__init__()
265
+
266
+ def onLoading(self, it):
267
+ self.loaded = True
268
+
269
+ stage = TestStage()
270
+
271
+ assert stage.loaded
272
+
273
+
274
+ class TestStageProperties:
275
+ """Test Stage properties"""
276
+
277
+ def test_inst_property(self):
278
+ """Test inst property"""
279
+
280
+ class TestStage(Stage):
281
+ NAME = "TestStage"
282
+
283
+ mock_inst = object()
284
+ stage = TestStage(mock_inst)
285
+
286
+ assert stage.inst is mock_inst
287
+
288
+ def test_current_property(self):
289
+ """Test current property"""
290
+
291
+ class TestStage(Stage):
292
+ NAME = "TestStage"
293
+
294
+ def my_state(self, inst):
295
+ return None
296
+
297
+ stage = TestStage()
298
+ stage._current = "my_state"
299
+
300
+ assert stage.current == "my_state"
301
+
302
+ def test_current_setter_validates(self):
303
+ """Test current setter validates state"""
304
+
305
+ class TestStage(Stage):
306
+ NAME = "TestStage"
307
+
308
+ def valid_state(self, inst):
309
+ return None
310
+
311
+ stage = TestStage()
312
+
313
+ with pytest.raises(ValueError):
314
+ stage.current = "invalid_state"
@@ -0,0 +1,263 @@
1
+ """Test behavior tree events - verify CHANGED events are triggered for child nodes"""
2
+
3
+ import random
4
+ import sys
5
+ import time
6
+ sys.path.insert(0, 'F:\\Python\\Python314\\Lib\\site-packages')
7
+
8
+ from chartflow.fsm import Stage
9
+ from chartflow.fsm.decorators import parallel, sequence, selector
10
+
11
+
12
+ class TestStage(Stage):
13
+ """Test stage with behavior tree - captures events synchronously"""
14
+ NAME = "TestStage"
15
+ LOG = True # Enable to see state transitions in console
16
+
17
+ def __init__(self):
18
+ super().__init__()
19
+ self.events = []
20
+
21
+ def onChanged(self, src, dst, inst):
22
+ """Synchronously capture state changes"""
23
+ super().onChanged(src, dst, inst)
24
+ self.events.append(("CHANGED", f"{src} -> {dst}"))
25
+
26
+ def entry(self, inst):
27
+ return "root"
28
+
29
+ @parallel("a", "b")
30
+ def root(self, inst):
31
+ return True
32
+
33
+ def a(self, inst):
34
+ return True
35
+
36
+ @sequence("c", "d")
37
+ def b(self, inst):
38
+ return True
39
+
40
+ def c(self, inst):
41
+ return True
42
+
43
+ def d(self, inst):
44
+ return True
45
+
46
+
47
+ def test_parallel_behavior():
48
+ """Test parallel behavior tree triggers CHANGED for children"""
49
+ print("="*60)
50
+ print("TEST: Parallel behavior - CHANGED events for children")
51
+ print("="*60)
52
+
53
+ stage = TestStage()
54
+ stage.events.clear() # Clear init events
55
+
56
+ print("\n--- Tick 1: entry -> root ---")
57
+ stage.handleStage(log=True)
58
+ print(f"Current state: {stage.current}")
59
+ print(f"Events: {stage.events}")
60
+
61
+ print("\n--- Tick 2: root parallel execution ---")
62
+ stage.events.clear()
63
+ stage.handleStage(log=True)
64
+ print(f"Current state: {stage.current}")
65
+ print(f"Events: {stage.events}")
66
+
67
+ # Check for child transitions
68
+ changed_events = [e for e in stage.events if e[0] == "CHANGED"]
69
+ child_transitions = [e for e in changed_events if "->" in e[1]]
70
+
71
+ print(f"\nChild transitions captured: {len(child_transitions)}")
72
+ for t in child_transitions:
73
+ print(f" {t[1]}")
74
+
75
+ # We expect to see transitions like:
76
+ # - root -> a
77
+ # - a -> b (or similar, depending on execution order)
78
+ success = len(child_transitions) > 0
79
+
80
+ # Check specific transitions
81
+ has_root_to_child = any("root ->" in e[1] for e in child_transitions)
82
+
83
+ if has_root_to_child:
84
+ print("PASS: Found CHANGED event from root to child")
85
+ else:
86
+ print("FAIL: No CHANGED event from root to child")
87
+
88
+ return success and has_root_to_child
89
+
90
+
91
+ def test_sequence_behavior():
92
+ """Test sequence behavior tree"""
93
+ print("\n" + "="*60)
94
+ print("TEST: Sequence behavior - step by step execution")
95
+ print("="*60)
96
+
97
+ class SeqStage(Stage):
98
+ NAME = "SeqStage"
99
+ LOG = True
100
+
101
+ def __init__(self):
102
+ super().__init__()
103
+ self.events = []
104
+
105
+ def onChanged(self, src, dst, inst):
106
+ self.events.append(("CHANGED", f"{src} -> {dst}"))
107
+
108
+ def entry(self, inst):
109
+ return "seq"
110
+
111
+ @sequence("s1", "s2", "s3")
112
+ def seq(self, inst):
113
+ return True
114
+
115
+ def s1(self, inst):
116
+ return True
117
+
118
+ def s2(self, inst):
119
+ return True
120
+
121
+ def s3(self, inst):
122
+ return True
123
+
124
+ stage = SeqStage()
125
+ stage.events.clear()
126
+
127
+ print("\n--- Tick 1: entry -> seq ---")
128
+ stage.handleStage(log=True)
129
+ print(f"Events: {stage.events}")
130
+
131
+ all_events = []
132
+ all_events.extend(stage.events)
133
+
134
+ print("\n--- Tick 2: seq starts ---")
135
+ stage.events.clear()
136
+ stage.handleStage(log=True)
137
+ print(f"Events: {stage.events}")
138
+ all_events.extend(stage.events)
139
+
140
+ # Sequence pauses after each child, so run more ticks
141
+ for i in range(5):
142
+ print(f"\n--- Tick {i+3} ---")
143
+ stage.events.clear()
144
+ stage.handleStage(log=True)
145
+ if stage.events:
146
+ print(f"Events: {stage.events}")
147
+ all_events.extend(stage.events)
148
+ if not stage.events:
149
+ break
150
+
151
+ print(f"\n=== All CHANGED events ===")
152
+ changed = [e for e in all_events if e[0] == "CHANGED"]
153
+ for c in changed:
154
+ print(f" {c[1]}")
155
+
156
+ # Check for child transitions in sequence
157
+ child_changes = [e for e in changed if "seq" in e[1] or "s1" in e[1] or "s2" in e[1] or "s3" in e[1]]
158
+
159
+ # We expect at least: entry -> seq, and some transitions involving s1, s2, s3
160
+ success = len(child_changes) >= 2
161
+
162
+ if success:
163
+ print(f"PASS: Found {len(child_changes)} relevant CHANGED events")
164
+ else:
165
+ print(f"FAIL: Only found {len(child_changes)} relevant CHANGED events")
166
+
167
+ return success
168
+
169
+
170
+ def test_selector_behavior():
171
+ """Test selector behavior tree"""
172
+ print("\n" + "="*60)
173
+ print("TEST: Selector behavior - first success stops")
174
+ print("="*60)
175
+
176
+ class SelStage(Stage):
177
+ NAME = "SelStage"
178
+ LOG = True
179
+
180
+ def __init__(self):
181
+ super().__init__()
182
+ self.events = []
183
+
184
+ def onChanged(self, src, dst, inst):
185
+ self.events.append(("CHANGED", f"{src} -> {dst}"))
186
+
187
+ def entry(self, inst):
188
+ return "sel"
189
+
190
+ @selector("f1", "f2", "f3")
191
+ def sel(self, inst):
192
+ return True
193
+
194
+ def f1(self, inst):
195
+ return False # Fail, try next
196
+
197
+ def f2(self, inst):
198
+ return True # Success, stop
199
+
200
+ def f3(self, inst):
201
+ return True # Won't execute
202
+
203
+ stage = SelStage()
204
+ stage.events.clear()
205
+
206
+ print("\n--- Tick 1: entry -> sel ---")
207
+ stage.handleStage(log=True)
208
+
209
+ all_events = list(stage.events)
210
+
211
+ print("\n--- Tick 2: selector execution ---")
212
+ stage.events.clear()
213
+ stage.handleStage(log=True)
214
+ all_events.extend(stage.events)
215
+
216
+ # Run more ticks
217
+ for i in range(3):
218
+ print(f"\n--- Tick {i+3} ---")
219
+ stage.events.clear()
220
+ stage.handleStage(log=True)
221
+ if stage.events:
222
+ print(f"Events: {stage.events}")
223
+ all_events.extend(stage.events)
224
+
225
+ print(f"\n=== All CHANGED events ===")
226
+ changed = [e for e in all_events if e[0] == "CHANGED"]
227
+ for c in changed:
228
+ print(f" {c[1]}")
229
+
230
+ # Selector should have transitioned to f1, then to f2 (when f1 fails)
231
+ has_child_transition = any("sel" in e[1] or "f1" in e[1] or "f2" in e[1] for e in changed)
232
+
233
+ if has_child_transition:
234
+ print("PASS: Selector has child transitions")
235
+ return True
236
+ else:
237
+ print("FAIL: No child transitions in selector")
238
+ return False
239
+
240
+
241
+ if __name__ == "__main__":
242
+ results = []
243
+
244
+ results.append(("Parallel", test_parallel_behavior()))
245
+ results.append(("Sequence", test_sequence_behavior()))
246
+ results.append(("Selector", test_selector_behavior()))
247
+
248
+ print("\n" + "="*60)
249
+ print("FINAL RESULTS")
250
+ print("="*60)
251
+ all_pass = True
252
+ for name, passed in results:
253
+ status = "PASS" if passed else "FAIL"
254
+ print(f" {name}: {status}")
255
+ if not passed:
256
+ all_pass = False
257
+
258
+ if all_pass:
259
+ print("\nALL TESTS PASSED!")
260
+ sys.exit(0)
261
+ else:
262
+ print("\nSOME TESTS FAILED")
263
+ sys.exit(1)
@@ -0,0 +1,80 @@
1
+ """Test nested behavior tree in parallel"""
2
+ from chartflow.fsm import Stage
3
+ from chartflow.fsm.decorators import sequence, parallel
4
+
5
+ # Use lowercase method names as states (Stage auto-discovers them)
6
+
7
+
8
+ class DemoLogic(Stage):
9
+ NAME = "DemoLogic"
10
+ LOG = True
11
+
12
+ def __init__(self):
13
+ super().__init__()
14
+ self.call_order = []
15
+
16
+ def entry(self, inst):
17
+ self.call_order.append("entry")
18
+ return "root"
19
+
20
+ @parallel("a", "b")
21
+ def root(self, inst):
22
+ self.call_order.append("root")
23
+ return True
24
+
25
+ def a(self, inst):
26
+ self.call_order.append("a")
27
+ return False # FAIL
28
+
29
+ @sequence("c", "d")
30
+ def b(self, inst):
31
+ self.call_order.append("b")
32
+ return True
33
+
34
+ def c(self, inst):
35
+ self.call_order.append("c")
36
+ return True
37
+
38
+ def d(self, inst):
39
+ self.call_order.append("d")
40
+ return True
41
+
42
+ def c(self, inst):
43
+ self.call_order.append("c")
44
+ return True
45
+
46
+ def d(self, inst):
47
+ self.call_order.append("d")
48
+ return True
49
+
50
+
51
+ def test_nested_parallel():
52
+ print("\n" + "="*60)
53
+ print("TEST: Nested sequence in parallel")
54
+ print("="*60)
55
+
56
+ stage = DemoLogic()
57
+
58
+ # Tick 1: entry -> root
59
+ print("\n--- Tick 1 ---")
60
+ stage.handleStage(log=True)
61
+
62
+ # Tick 2: parallel execution
63
+ print("\n--- Tick 2 ---")
64
+ stage.handleStage(log=True)
65
+
66
+ # Tick 3: continue parallel
67
+ print("\n--- Tick 3 ---")
68
+ stage.handleStage(log=True)
69
+
70
+ print(f"\nCall order: {stage.call_order}")
71
+ print(f"Current state: {stage.current_state}")
72
+
73
+ # Verify c and d were both called
74
+ assert "c" in stage.call_order, "c should be called"
75
+ assert "d" in stage.call_order, "d should be called"
76
+ print("PASS: Both c and d were called")
77
+
78
+
79
+ if __name__ == "__main__":
80
+ test_nested_parallel()