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,336 @@
1
+ """
2
+ 测试状态方法的参数检查功能
3
+
4
+ 验证内容:
5
+ 1. 方法接受 inst 参数 - 应该传递 inst
6
+ 2. 方法不接受任何参数 - 不应该传递 inst
7
+ 3. 方法接受 *args - 应该传递 inst
8
+ 4. 方法接受 **kwargs - 应该传递 inst
9
+ 5. 缓存机制 - __runtime_paramable__ 应该被设置
10
+ 6. 集成测试 - Stage 子类的各种状态方法
11
+ """
12
+
13
+ import pytest
14
+ from chartflow.fsm.stage import (
15
+ Stage,
16
+ _checkMethodAcceptsParam,
17
+ _invokeStateMethod,
18
+ _RUNTIME_PARAMABLE_CACHE,
19
+ )
20
+
21
+
22
+ class TestCheckMethodAcceptsParam:
23
+ """测试 _checkMethodAcceptsParam 函数"""
24
+
25
+ def setup_method(self):
26
+ """每个测试前清空缓存"""
27
+ _RUNTIME_PARAMABLE_CACHE.clear()
28
+
29
+ def test_no_params(self):
30
+ """测试不接受参数的方法"""
31
+ def noParams(self):
32
+ return "ok"
33
+
34
+ result = _checkMethodAcceptsParam(noParams)
35
+ assert result is False
36
+ assert noParams.__runtime_paramable__ is False
37
+
38
+ def test_with_self_and_param(self):
39
+ """测试有self和一个参数的方法"""
40
+ class MyClass:
41
+ def method(self, inst):
42
+ return inst
43
+
44
+ # 测试未绑定方法
45
+ result = _checkMethodAcceptsParam(MyClass.method)
46
+ assert result is True
47
+ assert MyClass.method.__runtime_paramable__ is True
48
+
49
+ def test_with_self_no_other_params(self):
50
+ """测试只有self参数的方法"""
51
+ class MyClass:
52
+ def method(self):
53
+ return "ok"
54
+
55
+ # 测试未绑定方法
56
+ result = _checkMethodAcceptsParam(MyClass.method)
57
+ assert result is False
58
+ assert MyClass.method.__runtime_paramable__ is False
59
+
60
+ def test_with_args(self):
61
+ """测试接受 *args 的方法"""
62
+ def withArgs(self, *args):
63
+ return args
64
+
65
+ result = _checkMethodAcceptsParam(withArgs)
66
+ assert result is True
67
+ assert withArgs.__runtime_paramable__ is True
68
+
69
+ def test_with_kwargs(self):
70
+ """测试接受 **kwargs 的方法"""
71
+ def withKwargs(self, **kwargs):
72
+ return kwargs
73
+
74
+ result = _checkMethodAcceptsParam(withKwargs)
75
+ assert result is True
76
+ assert withKwargs.__runtime_paramable__ is True
77
+
78
+ def test_with_args_and_kwargs(self):
79
+ """测试接受 *args, **kwargs 的方法"""
80
+ def withBoth(self, *args, **kwargs):
81
+ return (args, kwargs)
82
+
83
+ result = _checkMethodAcceptsParam(withBoth)
84
+ assert result is True
85
+ assert withBoth.__runtime_paramable__ is True
86
+
87
+ def test_with_default_param(self):
88
+ """测试有默认参数的方法"""
89
+ def withDefault(self, inst=None):
90
+ return inst
91
+
92
+ result = _checkMethodAcceptsParam(withDefault)
93
+ assert result is True
94
+ assert withDefault.__runtime_paramable__ is True
95
+
96
+ def test_cache_is_used(self):
97
+ """测试缓存被使用"""
98
+ def testMethod(self, inst):
99
+ return inst
100
+
101
+ # 第一次调用
102
+ result1 = _checkMethodAcceptsParam(testMethod)
103
+ assert result1 is True
104
+ assert testMethod.__runtime_paramable__ is True
105
+
106
+ # 修改缓存值
107
+ testMethod.__runtime_paramable__ = False
108
+
109
+ # 第二次调用应该使用缓存
110
+ result2 = _checkMethodAcceptsParam(testMethod)
111
+ assert result2 is False # 使用缓存值
112
+
113
+
114
+ class TestInvokeStateMethod:
115
+ """测试 _invokeStateMethod 函数"""
116
+
117
+ def test_invoke_with_param(self):
118
+ """测试调用接受参数的方法"""
119
+ received = []
120
+
121
+ class TestClass:
122
+ def withParam(self, inst):
123
+ received.append(inst)
124
+ return "result"
125
+
126
+ obj = TestClass()
127
+ # 使用 bound method(self 已自动绑定)
128
+ result = _invokeStateMethod(obj.withParam, "my_inst")
129
+ assert result == "result"
130
+ assert received == ["my_inst"]
131
+
132
+ def test_invoke_without_param(self):
133
+ """测试调用不接受参数的方法"""
134
+ received = []
135
+
136
+ class TestClass:
137
+ def noParams(self):
138
+ received.append("called")
139
+ return "result"
140
+
141
+ obj = TestClass()
142
+ # 使用 bound method(self 已自动绑定)
143
+ result = _invokeStateMethod(obj.noParams, "my_inst")
144
+ assert result == "result"
145
+ assert received == ["called"]
146
+
147
+ def test_invoke_with_args(self):
148
+ """测试调用接受 *args 的方法"""
149
+ received = []
150
+
151
+ class TestClass:
152
+ def withArgs(self, *args):
153
+ received.extend(args)
154
+ return "result"
155
+
156
+ obj = TestClass()
157
+ # 使用 bound method(self 已自动绑定)
158
+ result = _invokeStateMethod(obj.withArgs, "my_inst")
159
+ assert result == "result"
160
+ assert "my_inst" in received
161
+
162
+
163
+ class TestStageIntegration:
164
+ """集成测试 - 测试 Stage 类的各种状态方法"""
165
+
166
+ def setup_method(self):
167
+ """每个测试前清空缓存"""
168
+ _RUNTIME_PARAMABLE_CACHE.clear()
169
+
170
+ def test_state_with_inst_param(self):
171
+ """测试接受 inst 参数的状态方法"""
172
+ received = []
173
+
174
+ class MyInst:
175
+ pass
176
+
177
+ class MyStage(Stage):
178
+ NAME = "MyStage"
179
+
180
+ def start(self, inst):
181
+ received.append(("start", inst))
182
+ return None
183
+
184
+ my_inst = MyInst()
185
+ stage = MyStage(my_inst)
186
+ stage.handleStage(log=False)
187
+
188
+ assert len(received) == 1
189
+ assert received[0] == ("start", my_inst)
190
+
191
+ def test_state_without_params(self):
192
+ """测试不接受参数的状态方法"""
193
+ received = []
194
+
195
+ class MyInst:
196
+ pass
197
+
198
+ class MyStage(Stage):
199
+ NAME = "MyStage"
200
+
201
+ def start(self):
202
+ received.append("start_called")
203
+ return None
204
+
205
+ my_inst = MyInst()
206
+ stage = MyStage(my_inst)
207
+ stage.handleStage(log=False)
208
+
209
+ assert len(received) == 1
210
+ assert received[0] == "start_called"
211
+
212
+ def test_state_with_args(self):
213
+ """测试接受 *args 的状态方法"""
214
+ received = []
215
+
216
+ class MyInst:
217
+ pass
218
+
219
+ class MyStage(Stage):
220
+ NAME = "MyStage"
221
+
222
+ def start(self, *args):
223
+ received.extend(args)
224
+ return None
225
+
226
+ my_inst = MyInst()
227
+ stage = MyStage(my_inst)
228
+ stage.handleStage(log=False)
229
+
230
+ assert my_inst in received
231
+
232
+ def test_mixed_states(self):
233
+ """测试混合参数类型的状态方法"""
234
+ received = []
235
+
236
+ class MyInst:
237
+ pass
238
+
239
+ class MyStage(Stage):
240
+ NAME = "MyStage"
241
+
242
+ def start(self, inst):
243
+ received.append(("start", inst))
244
+ return "process"
245
+
246
+ def process(self):
247
+ received.append("process_no_inst")
248
+ return "end"
249
+
250
+ def end(self, inst):
251
+ received.append(("end", inst))
252
+ return None
253
+
254
+ my_inst = MyInst()
255
+ stage = MyStage(my_inst)
256
+
257
+ # 执行多个 tick
258
+ stage.handleStage(log=False) # start -> process
259
+ stage.handleStage(log=False) # process -> end
260
+ stage.handleStage(log=False) # end
261
+
262
+ assert len(received) == 3
263
+ assert received[0] == ("start", my_inst)
264
+ assert received[1] == "process_no_inst"
265
+ assert received[2] == ("end", my_inst)
266
+
267
+ def test_runtime_paramable_cache_set_on_stage(self):
268
+ """测试 __runtime_paramable__ 缓存被设置在 Stage 方法上"""
269
+
270
+ class MyStage(Stage):
271
+ NAME = "MyStage"
272
+
273
+ def start(self, inst):
274
+ return "noInst" # 转移到 noInst
275
+
276
+ def noInst(self):
277
+ return None
278
+
279
+ # 创建实例前检查
280
+ assert not hasattr(MyStage.start, '__runtime_paramable__')
281
+ assert not hasattr(MyStage.noInst, '__runtime_paramable__')
282
+
283
+ # 创建并执行(使用 None 作为 inst 避免元类问题)
284
+ stage = MyStage(None)
285
+ stage.handleStage(log=False) # 执行 start,然后转移到 noInst
286
+ stage.handleStage(log=False) # 执行 noInst
287
+
288
+ # 执行后检查:bound method 应该有缓存属性
289
+ # 从实例上获取的 bound method 可以设置属性
290
+ start_method = stage.start # 获取 bound method
291
+ noInst_method = stage.noInst
292
+
293
+ # bound method 可能无法直接设置 __runtime_paramable__
294
+ # 但模块级缓存应该存在
295
+ # 缓存键是 (id(obj), id(func)) 对于 bound method
296
+ start_cache_key = (id(stage), id(start_method.__func__))
297
+ noInst_cache_key = (id(stage), id(noInst_method.__func__))
298
+
299
+ assert start_cache_key in _RUNTIME_PARAMABLE_CACHE or hasattr(start_method, '__runtime_paramable__')
300
+ assert noInst_cache_key in _RUNTIME_PARAMABLE_CACHE or hasattr(noInst_method, '__runtime_paramable__')
301
+
302
+ # 验证缓存值
303
+ start_result = _checkMethodAcceptsParam(start_method)
304
+ noInst_result = _checkMethodAcceptsParam(noInst_method)
305
+ assert start_result is True
306
+ assert noInst_result is False
307
+
308
+
309
+ class TestEdgeCases:
310
+ """测试边界情况"""
311
+
312
+ def setup_method(self):
313
+ """每个测试前清空缓存"""
314
+ _RUNTIME_PARAMABLE_CACHE.clear()
315
+
316
+ def test_method_with_multiple_params(self):
317
+ """测试接受多个参数的方法"""
318
+ def multiParam(self, a, b, c):
319
+ return (a, b, c)
320
+
321
+ result = _checkMethodAcceptsParam(multiParam)
322
+ assert result is True
323
+ assert multiParam.__runtime_paramable__ is True
324
+
325
+ def test_method_with_only_defaults(self):
326
+ """测试所有参数都有默认值的方法"""
327
+ def allDefaults(self, a=1, b=2):
328
+ return (a, b)
329
+
330
+ result = _checkMethodAcceptsParam(allDefaults)
331
+ assert result is True
332
+ assert allDefaults.__runtime_paramable__ is True
333
+
334
+
335
+ if __name__ == "__main__":
336
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,280 @@
1
+ """Tests for Scheduler"""
2
+
3
+ import pytest
4
+ from chartflow.fsm.scheduler import Scheduler
5
+ from chartflow.fsm.meta import StageMachineMeta, clear_prototypes
6
+
7
+
8
+ @pytest.fixture(autouse=True)
9
+ def reset_scheduler():
10
+ """Reset scheduler before each test"""
11
+ Scheduler._instance = None
12
+ clear_prototypes()
13
+ yield
14
+ Scheduler._instance = None
15
+
16
+
17
+ class TestSchedulerSingleton:
18
+ """Test scheduler singleton behavior"""
19
+
20
+ def test_singleton(self):
21
+ """Test that Scheduler is a singleton"""
22
+ s1 = Scheduler()
23
+ s2 = Scheduler()
24
+ assert s1 is s2
25
+
26
+ def test_same_instance_across_calls(self):
27
+ """Test same instance returned across multiple calls"""
28
+ instances = [Scheduler() for _ in range(5)]
29
+ assert all(i is instances[0] for i in instances)
30
+
31
+
32
+ class TestSchedulerLogin:
33
+ """Test class registration via login()"""
34
+
35
+ def test_login_registers_class(self):
36
+ """Test that login registers a class"""
37
+ scheduler = Scheduler()
38
+
39
+ class TestLogic(metaclass=StageMachineMeta):
40
+ NAME = "TestLogic"
41
+
42
+ def state_a(self, inst):
43
+ return None
44
+
45
+ scheduler.login(TestLogic)
46
+ assert "TestLogic" in scheduler.prototypes
47
+ assert scheduler.prototypes["TestLogic"] is TestLogic
48
+
49
+ def test_login_validates_name(self):
50
+ """Test that login validates NAME attribute"""
51
+ scheduler = Scheduler()
52
+
53
+ class BadLogic:
54
+ pass
55
+
56
+ with pytest.raises(ValueError):
57
+ scheduler.login(BadLogic)
58
+
59
+ def test_login_rejects_digit_suffix(self):
60
+ """Test that login rejects NAME ending with digit"""
61
+ scheduler = Scheduler()
62
+
63
+ class BadLogic:
64
+ NAME = "BadLogic1"
65
+
66
+ with pytest.raises(ValueError):
67
+ scheduler.login(BadLogic)
68
+
69
+
70
+ class MockLogic:
71
+ """Mock logic class for testing"""
72
+ NAME = "MockLogic"
73
+ PRIORITY = 0
74
+
75
+ def __init__(self):
76
+ self._alive = True
77
+ self._enabled = True
78
+ self._isEntered = False
79
+ self.check_called = False
80
+ self.enter_called = False
81
+ self.step_called = False
82
+ self.exit_called = False
83
+
84
+ @property
85
+ def alive(self):
86
+ return self._alive
87
+
88
+ @alive.setter
89
+ def alive(self, value):
90
+ self._alive = value
91
+
92
+ @property
93
+ def enabled(self):
94
+ return self._enabled
95
+
96
+ @enabled.setter
97
+ def enabled(self, value):
98
+ self._enabled = value
99
+
100
+ def _check(self, is_seek):
101
+ self.check_called = True
102
+ return True
103
+
104
+ def _enter(self):
105
+ self.enter_called = True
106
+ self._isEntered = True
107
+
108
+ def _step(self):
109
+ self.step_called = True
110
+
111
+ def _exit(self):
112
+ self.exit_called = True
113
+
114
+
115
+ class TestSchedulerAdd:
116
+ """Test instance registration via add()"""
117
+
118
+ def test_add_instance_directly(self):
119
+ """Test adding an instance directly"""
120
+ scheduler = Scheduler()
121
+ mock = MockLogic()
122
+
123
+ result = scheduler.add(mock)
124
+
125
+ assert result is mock
126
+ assert mock in scheduler.updates or mock in scheduler.waits
127
+
128
+ def test_add_by_name(self):
129
+ """Test adding by registered name"""
130
+ scheduler = Scheduler()
131
+
132
+ class TestLogic(metaclass=StageMachineMeta):
133
+ NAME = "TestLogic"
134
+ PRIORITY = 0
135
+
136
+ def __init__(self):
137
+ self._alive = True
138
+ self._enabled = True
139
+ self._isEntered = False
140
+
141
+ def _check(self, is_seek):
142
+ return True
143
+
144
+ def _enter(self):
145
+ self._isEntered = True
146
+
147
+ def _step(self):
148
+ pass
149
+
150
+ def _exit(self):
151
+ pass
152
+
153
+ scheduler.login(TestLogic)
154
+ instance = scheduler.add("TestLogic")
155
+
156
+ assert isinstance(instance, TestLogic)
157
+
158
+
159
+ class TestSchedulerHandle:
160
+ """Test the main handle() loop"""
161
+
162
+ def test_handle_calls_lifecycle(self):
163
+ """Test that handle calls all lifecycle methods"""
164
+ scheduler = Scheduler()
165
+ mock = MockLogic()
166
+ scheduler.add(mock)
167
+
168
+ scheduler.handle()
169
+
170
+ assert mock.check_called
171
+ assert mock.enter_called
172
+ assert mock.step_called
173
+
174
+ def test_handle_removes_dead_instances(self):
175
+ """Test that handle removes dead instances"""
176
+ scheduler = Scheduler()
177
+ mock = MockLogic()
178
+ scheduler.add(mock)
179
+
180
+ scheduler.handle()
181
+ assert mock.enter_called # First tick enters
182
+
183
+ mock._alive = False
184
+ scheduler.handle()
185
+
186
+ assert mock.exit_called
187
+ assert mock not in scheduler.updates
188
+
189
+ def test_handle_skips_disabled_instances(self):
190
+ """Test that handle skips disabled instances"""
191
+ scheduler = Scheduler()
192
+ mock = MockLogic()
193
+ mock._enabled = False
194
+ scheduler.add(mock)
195
+
196
+ scheduler.handle()
197
+
198
+ assert mock.check_called # Check is still called
199
+ assert not mock.enter_called # But not entered
200
+ assert not mock.step_called
201
+
202
+ def test_handle_priority_ordering(self):
203
+ """Test that instances are processed by priority"""
204
+ scheduler = Scheduler()
205
+
206
+ class HighPriority(MockLogic):
207
+ PRIORITY = 10
208
+
209
+ class LowPriority(MockLogic):
210
+ PRIORITY = 0
211
+
212
+ low = LowPriority()
213
+ high = HighPriority()
214
+
215
+ scheduler.add(low)
216
+ scheduler.add(high)
217
+
218
+ # Both should be in updates after handle
219
+ scheduler.handle()
220
+
221
+ assert high in scheduler.updates
222
+ assert low in scheduler.updates
223
+
224
+
225
+ class TestSchedulerEnableDisable:
226
+ """Test enable/disable functionality"""
227
+
228
+ def test_disable_prevents_processing(self):
229
+ """Test that global disable prevents processing"""
230
+ scheduler = Scheduler()
231
+ mock = MockLogic()
232
+ scheduler.add(mock)
233
+
234
+ Scheduler.Disable()
235
+ scheduler.handle()
236
+
237
+ # Global disable prevents all processing
238
+ assert not mock.check_called
239
+
240
+ # Re-enable for other tests
241
+ Scheduler.Enable()
242
+
243
+ def test_instance_enable_disable(self):
244
+ """Test instance-level enable/disable"""
245
+ scheduler = Scheduler()
246
+ mock = MockLogic()
247
+ scheduler.add(mock)
248
+
249
+ scheduler.disable()
250
+ # Global disable affects all new handles
251
+
252
+ scheduler.enable()
253
+
254
+
255
+ class TestSchedulerLenAndContains:
256
+ """Test __len__ and __contains__"""
257
+
258
+ def test_len_counts_instances(self):
259
+ """Test that __len__ returns total instance count"""
260
+ scheduler = Scheduler()
261
+
262
+ assert len(scheduler) == 0
263
+
264
+ mock1 = MockLogic()
265
+ mock2 = MockLogic()
266
+ scheduler.add(mock1)
267
+ scheduler.add(mock2)
268
+
269
+ assert len(scheduler) == 2
270
+
271
+ def test_contains_checks_instance(self):
272
+ """Test that __contains__ checks for instance"""
273
+ scheduler = Scheduler()
274
+ mock = MockLogic()
275
+
276
+ assert mock not in scheduler
277
+
278
+ scheduler.add(mock)
279
+
280
+ assert mock in scheduler