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,448 @@
1
+ """
2
+ Stage Execute/Handle 模块规范测试
3
+ 验证所有需求文档中的行为
4
+ """
5
+
6
+ import pytest
7
+ from chartflow.fsm import Stage
8
+ from chartflow.fsm.decorators import sequence, selector, parallel, behavior
9
+
10
+
11
+ class TestBasicExecution:
12
+ """基础执行流程测试"""
13
+
14
+ def test_state_transition_cycle(self):
15
+ """测试: 所有节点转换必须通过 _execute_state -> _handle -> _transition_to 周期"""
16
+ events = []
17
+
18
+ class TestStage(Stage):
19
+ NAME = "Test"
20
+
21
+ def onChanged(self, src, dst, inst):
22
+ events.append(("onChanged", src, dst))
23
+
24
+ def a(self, inst):
25
+ return "b"
26
+
27
+ def b(self, inst):
28
+ return True
29
+
30
+ stage = TestStage()
31
+ stage.goto("a")
32
+ stage.handleStage(log=False)
33
+
34
+ assert ("onChanged", "a", "b") in events
35
+ assert stage.current_state == "b"
36
+
37
+ def test_changed_event_only_in_transition_to(self):
38
+ """测试: changed事件仅在 _transition_to 中触发"""
39
+ changed_events = []
40
+ enter_events = []
41
+
42
+ class TestStage(Stage):
43
+ NAME = "Test"
44
+
45
+ def setup(self):
46
+ self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
47
+ self.listenFor("enter", lambda e: enter_events.append(e["state"]))
48
+
49
+ def a(self, inst):
50
+ return "b"
51
+
52
+ def b(self, inst):
53
+ return True
54
+
55
+ stage = TestStage()
56
+ stage.setup()
57
+ stage.goto("a")
58
+
59
+ # Tick 1: a 执行并转换到 b —— changed 事件在 _transition_to 中触发
60
+ stage.handleStage(log=False)
61
+ assert ("a", "b") in changed_events
62
+
63
+ # Tick 2: b 被执行 —— enter 事件在 _execute_state 中触发。
64
+ # 引擎为每 tick 一个状态:a->b 转换后,b 在下一 tick 才执行并 enter。
65
+ stage.handleStage(log=False)
66
+ assert "b" in enter_events
67
+
68
+ def test_enter_event_only_in_execute_state(self):
69
+ """测试: enter事件仅在 _execute_state 中触发"""
70
+ enter_events = []
71
+
72
+ class TestStage(Stage):
73
+ NAME = "Test"
74
+
75
+ def setup(self):
76
+ self.listenFor("enter", lambda e: enter_events.append(e["state"]))
77
+
78
+ def a(self, inst):
79
+ return True
80
+
81
+ stage = TestStage()
82
+ stage.setup()
83
+ stage.goto("a")
84
+ stage.handleStage(log=False)
85
+
86
+ assert "a" in enter_events
87
+
88
+
89
+ class TestBehaviorTreeEvents:
90
+ """行为树事件测试"""
91
+
92
+ def test_sequence_child_changed_events(self):
93
+ """测试: Sequence子节点触发changed事件"""
94
+ changed_events = []
95
+
96
+ class SeqStage(Stage):
97
+ NAME = "Seq"
98
+
99
+ def setup(self):
100
+ self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
101
+
102
+ @sequence("s1", "s2", "s3")
103
+ def seq(self, inst):
104
+ return True
105
+
106
+ def s1(self, inst): return True
107
+ def s2(self, inst): return True
108
+ def s3(self, inst): return True
109
+
110
+ stage = SeqStage()
111
+ stage.setup()
112
+ stage.goto("seq")
113
+
114
+ # 执行多个tick完成序列
115
+ for _ in range(5):
116
+ stage.handleStage(log=False)
117
+
118
+ # 应该有 seq -> s1, s1 -> s2, s2 -> s3
119
+ assert ("seq", "s1") in changed_events
120
+ assert ("s1", "s2") in changed_events
121
+ assert ("s2", "s3") in changed_events
122
+
123
+ def test_selector_child_changed_events(self):
124
+ """测试: Selector子节点触发changed事件"""
125
+ changed_events = []
126
+
127
+ class SelStage(Stage):
128
+ NAME = "Sel"
129
+
130
+ def setup(self):
131
+ self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
132
+
133
+ @selector("f1", "f2")
134
+ def sel(self, inst):
135
+ return True
136
+
137
+ def f1(self, inst): return False
138
+ def f2(self, inst): return True
139
+
140
+ stage = SelStage()
141
+ stage.setup()
142
+ stage.goto("sel")
143
+
144
+ stage.handleStage(log=False)
145
+
146
+ # 应该有 sel -> f1, sel -> f2
147
+ assert ("sel", "f1") in changed_events
148
+ assert ("sel", "f2") in changed_events
149
+
150
+ def test_parallel_child_changed_events(self):
151
+ """测试: Parallel子节点触发changed事件"""
152
+ changed_events = []
153
+
154
+ class ParaStage(Stage):
155
+ NAME = "Para"
156
+
157
+ def setup(self):
158
+ self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
159
+
160
+ @parallel("a", "b")
161
+ def root(self, inst):
162
+ return True
163
+
164
+ def a(self, inst): return True
165
+ def b(self, inst): return True
166
+
167
+ stage = ParaStage()
168
+ stage.setup()
169
+ stage.goto("root")
170
+
171
+ stage.handleStage(log=False)
172
+
173
+ # 应该有 root -> a, root -> b
174
+ assert ("root", "a") in changed_events
175
+ assert ("root", "b") in changed_events
176
+
177
+ def test_break_no_duplicate_events(self):
178
+ """测试: BREAK不产生重复changed事件"""
179
+ changed_events = []
180
+
181
+ class TestStage(Stage):
182
+ NAME = "Test"
183
+
184
+ def setup(self):
185
+ self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
186
+
187
+ @sequence("a", "b")
188
+ def seq(self, inst):
189
+ return True
190
+
191
+ def a(self, inst):
192
+ return "external" # BREAK
193
+
194
+ def b(self, inst): return True
195
+
196
+ def external(self, inst):
197
+ return True
198
+
199
+ stage = TestStage()
200
+ stage.setup()
201
+ stage.goto("seq")
202
+
203
+ stage.handleStage(log=False)
204
+
205
+ # 应该只有 a -> external, 没有 seq -> external
206
+ assert ("a", "external") in changed_events
207
+ assert ("seq", "external") not in changed_events
208
+
209
+
210
+ class TestAsyncSemantics:
211
+ """异步语义测试(RUN/None)"""
212
+
213
+ def test_run_pauses_execution(self):
214
+ """测试: RUN (None) 暂停执行,下一tick恢复"""
215
+ call_count = {"a": 0}
216
+
217
+ class TestStage(Stage):
218
+ NAME = "Test"
219
+
220
+ @sequence("a", "b")
221
+ def seq(self, inst):
222
+ return True
223
+
224
+ def a(self, inst):
225
+ call_count["a"] += 1
226
+ return None # RUN
227
+
228
+ def b(self, inst):
229
+ return True
230
+
231
+ stage = TestStage()
232
+ stage.goto("seq")
233
+
234
+ # 第一个tick执行a,暂停
235
+ stage.handleStage(log=False)
236
+ assert call_count["a"] == 1
237
+ assert stage.current_state == "a"
238
+
239
+ # 第二个tick继续执行a,完成
240
+ stage.handleStage(log=False)
241
+ assert call_count["a"] == 2
242
+
243
+ def test_behavior_context_preserved_during_run(self):
244
+ """测试: RUN期间行为树上下文被保留"""
245
+ class TestStage(Stage):
246
+ NAME = "Test"
247
+
248
+ @sequence("a", "b")
249
+ def seq(self, inst):
250
+ return True
251
+
252
+ def a(self, inst):
253
+ return None # RUN
254
+
255
+ def b(self, inst):
256
+ return True
257
+
258
+ stage = TestStage()
259
+ stage.goto("seq")
260
+
261
+ stage.handleStage(log=False)
262
+
263
+ # _bhead 和 _bhcontext 应该被保留
264
+ assert stage._bhead == "seq"
265
+ assert "seq" in stage._bhcontext
266
+
267
+
268
+ class TestNestedBehaviors:
269
+ """嵌套行为树测试"""
270
+
271
+ def test_nested_sequence_events(self):
272
+ """测试: 嵌套序列正确触发所有事件"""
273
+ changed_events = []
274
+
275
+ class TestStage(Stage):
276
+ NAME = "Test"
277
+
278
+ def setup(self):
279
+ self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
280
+
281
+ @parallel("worker_a", "worker_b")
282
+ def root(self, inst):
283
+ return True
284
+
285
+ @sequence("s1", "s2")
286
+ def worker_a(self, inst):
287
+ return True
288
+
289
+ @sequence("p1", "p2")
290
+ def worker_b(self, inst):
291
+ return True
292
+
293
+ def s1(self, inst): return True
294
+ def s2(self, inst): return True
295
+ def p1(self, inst): return True
296
+ def p2(self, inst): return True
297
+
298
+ stage = TestStage()
299
+ stage.setup()
300
+ stage.goto("root")
301
+
302
+ # 执行多个tick完成
303
+ for _ in range(10):
304
+ stage.handleStage(log=False)
305
+
306
+ # 应该有 root -> worker_a, root -> worker_b, worker_a -> s1, worker_b -> p1, 等
307
+ assert ("root", "worker_a") in changed_events
308
+ assert ("root", "worker_b") in changed_events
309
+
310
+ def test_nested_break_propagation(self):
311
+ """测试: 嵌套BREAK正确传播,不触发父级事件"""
312
+ changed_events = []
313
+
314
+ class TestStage(Stage):
315
+ NAME = "Test"
316
+
317
+ def setup(self):
318
+ self.listenFor("changed", lambda e: changed_events.append((e["old"], e["new"])))
319
+
320
+ @parallel("inner_a", "inner_b")
321
+ def outer(self, inst):
322
+ return True
323
+
324
+ @sequence("x", "y")
325
+ def inner_a(self, inst):
326
+ return True
327
+
328
+ def inner_b(self, inst):
329
+ return True
330
+
331
+ def x(self, inst):
332
+ return "exit" # BREAK
333
+
334
+ def y(self, inst):
335
+ return True
336
+
337
+ def exit(self, inst):
338
+ return True
339
+
340
+ stage = TestStage()
341
+ stage.setup()
342
+ stage.goto("outer")
343
+
344
+ stage.handleStage(log=False)
345
+
346
+ # 应该只有 x -> exit, 没有 inner_a -> exit, outer -> exit
347
+ assert ("x", "exit") in changed_events
348
+ assert ("inner_a", "exit") not in changed_events
349
+ assert ("outer", "exit") not in changed_events
350
+
351
+
352
+ class TestOutputFormat:
353
+ """输出格式测试"""
354
+
355
+ def test_no_duplicate_arrow_on_resume(self, capsys):
356
+ """测试: 恢复时不重复打印 -> child"""
357
+ class TestStage(Stage):
358
+ NAME = "Test"
359
+ LOG = True
360
+
361
+ @sequence("a", "b")
362
+ def seq(self, inst):
363
+ return True
364
+
365
+ def a(self, inst):
366
+ return None # RUN
367
+
368
+ def b(self, inst):
369
+ return True
370
+
371
+ stage = TestStage()
372
+ stage.goto("seq")
373
+
374
+ # 第一个tick
375
+ stage.handleStage(log=True)
376
+ # 第二个tick(恢复)
377
+ stage.handleStage(log=True)
378
+
379
+ captured = capsys.readouterr()
380
+ # 应该只有一个 "-> a"
381
+ assert captured.out.count("-> a") == 1
382
+
383
+
384
+ class TestEdgeCases:
385
+ """边界情况测试"""
386
+
387
+ def test_sequence_restart_after_completion(self):
388
+ """测试: 序列完成后重新进入应该从头开始"""
389
+ call_order = []
390
+
391
+ class TestStage(Stage):
392
+ NAME = "Test"
393
+
394
+ @sequence("a", "b")
395
+ def seq(self, inst):
396
+ call_order.append("seq")
397
+ return True
398
+
399
+ def a(self, inst):
400
+ call_order.append("a")
401
+ return True
402
+
403
+ def b(self, inst):
404
+ call_order.append("b")
405
+ return True
406
+
407
+ stage = TestStage()
408
+ stage.goto("seq")
409
+
410
+ # 完成第一次执行
411
+ for _ in range(5):
412
+ stage.handleStage(log=False)
413
+
414
+ assert "a" in call_order
415
+ assert "b" in call_order
416
+
417
+ def test_empty_behavior(self):
418
+ """测试: 空行为树处理"""
419
+ class TestStage(Stage):
420
+ NAME = "Test"
421
+
422
+ @sequence()
423
+ def empty(self, inst):
424
+ return True
425
+
426
+ stage = TestStage()
427
+ stage.goto("empty")
428
+ result = stage.handleStage(log=False)
429
+
430
+ # 应该正常完成
431
+ assert result is not None
432
+
433
+ def test_unknown_state_in_behavior(self):
434
+ """测试: 行为树中未知状态在类定义时抛出异常"""
435
+ with pytest.raises(TypeError):
436
+ class TestStage(Stage):
437
+ NAME = "Test"
438
+
439
+ @sequence("unknown", "a")
440
+ def seq(self, inst):
441
+ return True
442
+
443
+ def a(self, inst):
444
+ return True
445
+
446
+
447
+ if __name__ == "__main__":
448
+ pytest.main([__file__, "-v"])
chartflow/fsm/utils.py ADDED
@@ -0,0 +1,34 @@
1
+ from efr import EventSystem as _EventSystem
2
+ from efr import EventFramework
3
+
4
+ __event_system__ = None
5
+
6
+
7
+ class EventSystem(_EventSystem):
8
+ """Extended EventSystem with source filtering support."""
9
+
10
+ def pushEvent(self, event, data, *targets, source=None):
11
+ """
12
+ Push event with optional source information.
13
+
14
+ Args:
15
+ event: Event name
16
+ data: Event data dict
17
+ *targets: Target names
18
+ source: Source identifier (stored in data)
19
+ """
20
+ if source is not None:
21
+ if not isinstance(data, dict):
22
+ data = {"_data": data}
23
+ data = dict(data)
24
+ data["source"] = source
25
+ super().pushEvent(event, data, *targets)
26
+
27
+
28
+ def GetES():
29
+ global __event_system__
30
+ if __event_system__ is None:
31
+ # Use standalone mode (synchronous) for predictable event handling
32
+ __event_system__ = EventSystem()
33
+ return __event_system__
34
+
@@ -0,0 +1,66 @@
1
+ """
2
+ qnode - A generic node graph canvas library based on PyQt6
3
+
4
+ This library provides a generic node graph canvas widget that can be used
5
+ for various node-based visual editing applications.
6
+
7
+ Usage:
8
+ from chartflow.node import NodeCanvas, NodeItem, ConnectionItem
9
+
10
+ canvas = NodeCanvas()
11
+ node1 = canvas.addNode("Start", QPointF(0, 0))
12
+ node2 = canvas.addNode("End", QPointF(200, 0))
13
+ canvas.addConnection(node1, node2)
14
+ """
15
+
16
+ from chartflow.node.enums import NodeState, NodeShape, ArrowState, ConnectionType
17
+ from chartflow.node.styles import NodeStyle, ConnectionStyle, ArrowStyle
18
+ from chartflow.node.node_item import QNodeItem
19
+ from chartflow.node.port_item import QNodePort, QNodePortSpec
20
+ from chartflow.node._port_interface import IQNodePort
21
+ from chartflow.node.connection_item import ConnectionItem
22
+ from chartflow.node.arrow_item import ArrowItem
23
+ from chartflow.node.selection_rect import SelectionRect
24
+ from chartflow.node.style_panel import StylePanel
25
+ from chartflow.node.node_canvas import QNodeCanvas
26
+ from chartflow.node.main_window import NodeCanvasWindow
27
+ from chartflow.node.qnode_editor import QNodeEditor
28
+ from chartflow.node.popmenu import (
29
+ ICanvasPopMenu, QCanvasPopMenu,
30
+ INodePopMenu, QNodePopMenu,
31
+ ILinePopMenu, QLinePopMenu,
32
+ )
33
+
34
+ __version__ = "1.0.0"
35
+ __author__ = "qnode"
36
+
37
+ __all__ = [
38
+ # Enums
39
+ "NodeState",
40
+ "NodeShape",
41
+ "ArrowState",
42
+ "ConnectionType",
43
+ # Styles
44
+ "NodeStyle",
45
+ "ConnectionStyle",
46
+ "ArrowStyle",
47
+ # Items
48
+ "QNodeItem",
49
+ "QNodePort",
50
+ "QNodePortSpec",
51
+ "IQNodePort",
52
+ "ConnectionItem",
53
+ "ArrowItem",
54
+ "SelectionRect",
55
+ # UI Components
56
+ "StylePanel",
57
+ # Canvas and Window
58
+ "QNodeCanvas",
59
+ "NodeCanvasWindow",
60
+ # Editor
61
+ "QNodeEditor",
62
+ # Pop Menus
63
+ "ICanvasPopMenu", "QCanvasPopMenu",
64
+ "INodePopMenu", "QNodePopMenu",
65
+ "ILinePopMenu", "QLinePopMenu",
66
+ ]