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
chartflow/fsm/meta.py ADDED
@@ -0,0 +1,569 @@
1
+ from __future__ import annotations
2
+ """
3
+ qfsm - Finite State Machine Metaclass
4
+
5
+ This module provides the StageMachineMeta metaclass for automatic state discovery,
6
+ class registration, and inheritance handling for state machines.
7
+ """
8
+
9
+
10
+ import inspect
11
+ import weakref
12
+ from collections.abc import Callable
13
+ from typing import Any, ClassVar, Final, Self, TypeAlias
14
+
15
+ # Global prototype registry for all state machine classes
16
+ __protos__: dict[str, type] = {}
17
+
18
+ # Methods to ignore when discovering states
19
+ IGNORES: Final[frozenset[str]] = frozenset({
20
+ 'ref', 'refLast', 'refNext', 'refAdjacent', 'refEnd', 'refHead',
21
+ 'link', 'transform', 'listenFor', 'cancelListen', 'pushEvent',
22
+ 'schedule', 'cancelSchedule', 'handleStage', 'onStart', 'onStop',
23
+ 'onStep', 'onChanged', 'onKilled', 'onDraw', 'onLoading',
24
+ 'productQueue', 'showQueue', 'jumpQueue', 'accelerate',
25
+ 'goto', 'kill', 'enable', 'disable'
26
+ })
27
+
28
+ # Base classes that should not be registered
29
+ BASE_CLASSES: Final[frozenset[str]] = frozenset({'Stage', 'Logic'})
30
+
31
+ # Type aliases for class attribute structures
32
+ StateList: TypeAlias = list[str]
33
+ RecursiveSet: TypeAlias = set[str]
34
+ ListenDict: TypeAlias = dict[str, list[tuple[str, tuple[str, ...]]]]
35
+ BehaviorDict: TypeAlias = dict[str, dict[str, Any]]
36
+
37
+
38
+ class StageMachineMeta(type):
39
+ """
40
+ Metaclass for finite state machines with automatic state discovery.
41
+
42
+ Features:
43
+ - Automatic discovery of state methods (callable, non-private, non-UPPERCASE, not in IGNORES)
44
+ - Class registration to global __protos__ registry
45
+ - Inheritance of __states__, __recursive__, __listens__, __behaviors__
46
+ - Tracking of decorator-marked methods
47
+
48
+ Class Attributes Created:
49
+ __states__: List of discovered state method names
50
+ __recursive__: Set of recursive state names
51
+ __listens__: Dict of event listeners {event_name: [(func_name, sources), ...]}
52
+ __behaviors__: Dict of behavior tree nodes {state_name: {children: ..., composite: ...}}
53
+ """
54
+ __ID_PROTOS__ = {}
55
+ __INSTANCES__ = {}
56
+ IGNORES = IGNORES
57
+
58
+
59
+ @staticmethod
60
+ def _remove_ref(_ref):
61
+ for _name, _d in list(StageMachineMeta.__INSTANCES__.items()):
62
+ for _id, _r in list(_d.items()):
63
+ if _r is _ref:
64
+ _d.pop(_id, None)
65
+ if not _d:
66
+ StageMachineMeta.__INSTANCES__.pop(_name, None)
67
+ return
68
+
69
+
70
+ def __new__(
71
+ mcs,
72
+ name: str,
73
+ bases: tuple[type, ...],
74
+ namespace: dict[str, Any],
75
+ **kwargs: Any
76
+ ) -> Self:
77
+ """
78
+ Create a new state machine class with automatic state discovery.
79
+
80
+ Args:
81
+ name: Class name
82
+ bases: Base classes
83
+ namespace: Class namespace
84
+ **kwargs: Additional keyword arguments
85
+
86
+ Returns:
87
+ New class instance
88
+ """
89
+ # Create the class first
90
+ cls = super().__new__(mcs, name, bases, namespace)
91
+
92
+ # Discover states from current class namespace
93
+ _extra_ignores = set(namespace.get('IGNORES', []))
94
+ current_states = mcs._discover_states(namespace, _extra_ignores)
95
+
96
+ # Inherit and merge attributes from base classes
97
+ cls.__states__ = mcs._merge_states(bases, current_states)
98
+ cls.__recursive__ = mcs._merge_recursive(bases, namespace)
99
+ cls.__listens__ = mcs._merge_listens(bases, namespace)
100
+ cls.__behaviors__ = mcs._merge_behaviors(bases, namespace)
101
+
102
+ # Validate behavior children exist (must happen after states and behaviors are set)
103
+ mcs._validate_behaviors(cls, name, current_states)
104
+
105
+ # Calculate max state name length for aligned output
106
+ if cls.__states__:
107
+ cls.__max_state_len__ = max(len(s) for s in cls.__states__)
108
+ else:
109
+ cls.__max_state_len__ = 0
110
+
111
+ # Register class if it has a NAME and is not a base class
112
+ mcs._register_class(cls, name)
113
+
114
+ return cls
115
+
116
+
117
+ def __call__(cls, *args, **kwargs):
118
+ _skip_proto = getattr(cls, '__no_proto_call__', False)
119
+ if len(args) and isinstance(args[0], str) and not _skip_proto:
120
+ prototype = StageMachineMeta.proto(args[0])
121
+ if prototype is None:
122
+ raise TypeError(
123
+ f"[{cls.__name__}]: The given prototype name '{args[0]}' is not defined.\n\t* Please make sure import that file.")
124
+ return prototype(*args[1:], **kwargs)
125
+ instance = cls.__new__(cls, *args, **kwargs)
126
+ if isinstance(instance, cls):
127
+ instance.__init__(*args, **kwargs)
128
+ _name = getattr(instance, 'NAME', None)
129
+ if _name is not None:
130
+ _uid = getattr(instance, '_uid', id(instance))
131
+ ref = weakref.ref(instance, StageMachineMeta._remove_ref)
132
+ StageMachineMeta.__INSTANCES__.setdefault(_name, {})[_uid] = ref
133
+ return instance
134
+
135
+ @staticmethod
136
+ def _discover_states(namespace: dict[str, Any], extra_ignores: set[str] | None = None) -> StateList:
137
+ """
138
+ Discover state methods from class namespace.
139
+
140
+ A method is considered a state if:
141
+ - It's callable
142
+ - Doesn't start with '_' (not private)
143
+ - Is not ALL UPPERCASE constant (lambda assignment)
144
+ - Is not in the IGNORES list
145
+ - Is not marked with @listen decorator
146
+
147
+ Args:
148
+ namespace: Class namespace dictionary
149
+ extra_ignores: Additional method names to ignore (from class-level IGNORES)
150
+
151
+ Returns:
152
+ List of discovered state method names
153
+ """
154
+ _ignores = IGNORES | (extra_ignores or set())
155
+
156
+ def _is_method(name: str, obj: Any) -> bool:
157
+ """Check if obj is a method defined in this class (not lambda assignment)."""
158
+ if not callable(obj):
159
+ return False
160
+ if name.startswith('_'):
161
+ return False
162
+ if name in _ignores:
163
+ return False
164
+ if isinstance(obj, (staticmethod, classmethod)):
165
+ return False
166
+ if hasattr(obj, '_listen_event'):
167
+ return False
168
+ # Exclude ALL UPPERCASE method names (treat as constants)
169
+ if name.isupper():
170
+ return False
171
+ return True
172
+
173
+ return [
174
+ name for name, obj in namespace.items()
175
+ if _is_method(name, obj)
176
+ ]
177
+
178
+ @staticmethod
179
+ def _merge_states(bases: tuple[type, ...], current_states: StateList) -> StateList:
180
+ """
181
+ Merge states from base classes with current class states.
182
+
183
+ Base class states come first, then current class states.
184
+ Preserves order and removes duplicates.
185
+
186
+ Args:
187
+ bases: Base classes
188
+ current_states: States discovered in current class
189
+
190
+ Returns:
191
+ Merged list of state names
192
+ """
193
+ inherited: StateList = []
194
+
195
+ # Collect states from base classes
196
+ for base in bases:
197
+ if hasattr(base, '__states__'):
198
+ for state in base.__states__:
199
+ if state not in inherited:
200
+ inherited.append(state)
201
+
202
+ # Add current class states (avoiding duplicates)
203
+ for state in current_states:
204
+ if state not in inherited:
205
+ inherited.append(state)
206
+
207
+ return inherited
208
+
209
+ @staticmethod
210
+ def _merge_recursive(bases: tuple[type, ...], namespace: dict[str, Any]) -> RecursiveSet:
211
+ """
212
+ Merge recursive state markers from base classes and current class.
213
+
214
+ Args:
215
+ bases: Base classes
216
+ namespace: Class namespace
217
+
218
+ Returns:
219
+ Set of recursive state names
220
+ """
221
+ recursive: RecursiveSet = set()
222
+
223
+ # Inherit from base classes
224
+ for base in bases:
225
+ if hasattr(base, '__recursive__'):
226
+ recursive.update(base.__recursive__)
227
+
228
+ # Add from current class (methods marked with @recursive decorator)
229
+ for name, obj in namespace.items():
230
+ if name.startswith('_'):
231
+ continue
232
+ if callable(obj) and getattr(obj, '_recursive_marker', False):
233
+ recursive.add(name)
234
+
235
+ return recursive
236
+
237
+ @staticmethod
238
+ def _merge_listens(bases: tuple[type, ...], namespace: dict[str, Any]) -> ListenDict:
239
+ """
240
+ Merge event listeners from base classes and current class.
241
+
242
+ Args:
243
+ bases: Base classes
244
+ namespace: Class namespace
245
+
246
+ Returns:
247
+ Dict of event listeners
248
+ """
249
+ listens: ListenDict = {}
250
+
251
+ # Inherit from base classes
252
+ for base in bases:
253
+ if hasattr(base, '__listens__'):
254
+ for event, handlers in base.__listens__.items():
255
+ listens.setdefault(event, []).extend(handlers)
256
+
257
+ # Add from current class (methods marked with @listen decorator)
258
+ for name, obj in namespace.items():
259
+ if name.startswith('_'):
260
+ continue
261
+ if callable(obj):
262
+ listen_event = getattr(obj, '_listen_event', None)
263
+ if listen_event:
264
+ sources = getattr(obj, '_listen_sources', ())
265
+ listens.setdefault(listen_event, []).append((name, sources))
266
+
267
+ return listens
268
+
269
+ @staticmethod
270
+ def _merge_behaviors(bases: tuple[type, ...], namespace: dict[str, Any]) -> BehaviorDict:
271
+ """
272
+ Merge behavior tree nodes from base classes and current class.
273
+
274
+ Args:
275
+ bases: Base classes
276
+ namespace: Class namespace
277
+
278
+ Returns:
279
+ Dict of behavior tree nodes
280
+ """
281
+ behaviors: BehaviorDict = {}
282
+
283
+ # Inherit from base classes
284
+ for base in bases:
285
+ if hasattr(base, '__behaviors__'):
286
+ behaviors.update(base.__behaviors__)
287
+
288
+ # Add from current class (methods marked with behavior decorators)
289
+ for name, obj in namespace.items():
290
+ if name.startswith('_'):
291
+ continue
292
+ if callable(obj):
293
+ # Check for composite behaviors (@sequence, @selector, @parallel)
294
+ behavior_children = getattr(obj, '_behavior_children', None)
295
+ if behavior_children:
296
+ behavior_composite = getattr(obj, '_behavior_composite', None)
297
+ behaviors[name] = {
298
+ 'children': behavior_children,
299
+ 'composite': behavior_composite
300
+ }
301
+ else:
302
+ # Check for simple @behavior
303
+ behavior_target = getattr(obj, '_behavior_target', None)
304
+ if behavior_target:
305
+ behaviors[name] = {
306
+ 'children': behavior_target,
307
+ 'composite': None
308
+ }
309
+
310
+ return behaviors
311
+
312
+ @staticmethod
313
+ def _validate_behaviors(cls: type, class_name: str, current_states: list[str]) -> None:
314
+ """
315
+ Validate that all behavior children reference existing states.
316
+
317
+ This check happens at class definition time to catch errors early.
318
+ A behavior child must either be a regular state (in __states__) or
319
+ another behavior node (in __behaviors__).
320
+
321
+ Args:
322
+ cls: The class being created
323
+ class_name: The class name
324
+ current_states: States defined in the current class namespace
325
+
326
+ Raises:
327
+ TypeError: If a behavior references an unknown state
328
+ """
329
+ valid_states = set(cls.__states__)
330
+ valid_behaviors = set(cls.__behaviors__.keys())
331
+
332
+ for behavior_name, behavior_info in cls.__behaviors__.items():
333
+ children = behavior_info.get('children', [])
334
+ # Handle both list and single string children
335
+ if isinstance(children, str):
336
+ children = [children]
337
+ elif not isinstance(children, (list, tuple)):
338
+ continue
339
+
340
+ for child in children:
341
+ if child not in valid_states and child not in valid_behaviors:
342
+ raise TypeError(
343
+ f"[{cls.__name__}]: Behavior '{behavior_name}' "
344
+ f"references unknown state '{child}'. "
345
+ f"Available states: {sorted(valid_states)}"
346
+ )
347
+
348
+ @staticmethod
349
+ def _register_class(cls: type, name: str) -> None:
350
+ """
351
+ Register class to global prototype registry.
352
+
353
+ Registration rules:
354
+ - Class must have a 'NAME' attribute
355
+ - Class name must not be in BASE_CLASSES
356
+ - NAME must not end with a digit
357
+
358
+ Args:
359
+ cls: Class to register
360
+ name: Class name
361
+ """
362
+ # Skip base classes
363
+ if name in BASE_CLASSES:
364
+ return
365
+
366
+ # Check for NAME attribute
367
+ class_name = getattr(cls, 'NAME', None)
368
+ if class_name is None:
369
+ return
370
+
371
+ # Validate NAME doesn't end with digit
372
+ if class_name and class_name[-1].isdigit():
373
+ raise ValueError(
374
+ f"Class '{name}' has invalid NAME '{class_name}': "
375
+ "NAME must not end with a digit"
376
+ )
377
+
378
+ # Register to global prototype registry
379
+ __protos__[class_name] = cls
380
+
381
+ @staticmethod
382
+ def proto(name:str):
383
+ return __protos__.get(name)
384
+
385
+ @staticmethod
386
+ def instances(name: str | type) -> list[Any]:
387
+ """
388
+ Get all living instances with the given NAME.
389
+
390
+ Args:
391
+ name: Instance NAME (class NAME attribute) or class type
392
+
393
+ Returns:
394
+ List of living instances
395
+ """
396
+ _n = name.NAME if isinstance(name, type) else name
397
+ _refs = StageMachineMeta.__INSTANCES__.get(_n, {})
398
+ _ret = []
399
+ for _ref in _refs.values():
400
+ _inst = _ref()
401
+ if _inst is not None:
402
+ _ret.append(_inst)
403
+ return _ret
404
+
405
+ @classmethod
406
+ def requireId(cls, name:str):
407
+ if not name:
408
+ raise ValueError("NAME cannot be empty")
409
+ ret = cls.__ID_PROTOS__[name] = cls.__ID_PROTOS__.get(name, -1) + 1
410
+ return ret
411
+
412
+ @staticmethod
413
+ def removeInstance(name: str | type, uid: int) -> bool:
414
+ """从 __INSTANCES__ 中移除指定 uid 的实例。
415
+
416
+ Args:
417
+ name: Instance NAME (class NAME attribute) or class type
418
+ uid: 实例 uid (通常为 id(instance))
419
+
420
+ Returns:
421
+ True if removed, False if not found
422
+ """
423
+ _n = name.NAME if isinstance(name, type) else name
424
+ _d = StageMachineMeta.__INSTANCES__.get(_n)
425
+ if _d is None:
426
+ return False
427
+ _removed = _d.pop(uid, None) is not None
428
+ if not _d:
429
+ StageMachineMeta.__INSTANCES__.pop(_n, None)
430
+ return _removed
431
+
432
+
433
+ # Decorator functions for marking methods
434
+ def recursive(func: Callable) -> Callable:
435
+ """
436
+ Decorator to mark a state method as recursive.
437
+
438
+ Recursive states can transition to themselves.
439
+
440
+ Args:
441
+ func: Method to mark
442
+
443
+ Returns:
444
+ Marked method
445
+ """
446
+ func._is_recursive = True # type: ignore[attr-defined]
447
+ return func
448
+
449
+
450
+ def listen(event: str, *sources: str) -> Callable[[Callable], Callable]:
451
+ """
452
+ Decorator to mark a method as an event listener.
453
+
454
+ Args:
455
+ event: Event name to listen for
456
+ *sources: Optional source filters
457
+
458
+ Returns:
459
+ Decorator function
460
+
461
+ Example:
462
+ @listen('onComplete', 'loader')
463
+ def handle_complete(self, event): ...
464
+ """
465
+ def decorator(func: Callable) -> Callable:
466
+ func._listen_info = (event, sources) # type: ignore[attr-defined]
467
+ return func
468
+ return decorator
469
+
470
+
471
+ def behavior(*, children: list[str] | None = None, composite: str | None = None) -> Callable[[Callable], Callable]:
472
+ """
473
+ Decorator to mark a method as a behavior tree node.
474
+
475
+ Args:
476
+ children: List of child state names
477
+ composite: Composite type (e.g., 'selector', 'sequence')
478
+
479
+ Returns:
480
+ Decorator function
481
+
482
+ Example:
483
+ @behavior(children=['idle', 'walk', 'run'], composite='selector')
484
+ def movement(self): ...
485
+ """
486
+ def decorator(func: Callable) -> Callable:
487
+ func._behavior_info = { # type: ignore[attr-defined]
488
+ 'children': children or [],
489
+ 'composite': composite
490
+ }
491
+ return func
492
+ return decorator
493
+
494
+
495
+ def state(func: Callable) -> Callable:
496
+ """
497
+ Decorator to explicitly mark a method as a state.
498
+
499
+ This is optional since states are auto-discovered, but can be used
500
+ for clarity or to include methods that might otherwise be excluded.
501
+
502
+ Args:
503
+ func: Method to mark
504
+
505
+ Returns:
506
+ Marked method
507
+ """
508
+ func._is_state = True # type: ignore[attr-defined]
509
+ return func
510
+
511
+
512
+ # Utility functions for prototype management
513
+ def GetPrototype(name: str) -> type | None:
514
+ """
515
+ Get a registered prototype by name.
516
+
517
+ Args:
518
+ name: Prototype name (class NAME attribute)
519
+
520
+ Returns:
521
+ The prototype class or None if not found
522
+ """
523
+ return __protos__.get(name)
524
+
525
+
526
+ # 小写别名,保持向后兼容
527
+ get_prototype = GetPrototype
528
+
529
+
530
+ def ListPrototypes() -> list[str]:
531
+ """
532
+ List all registered prototype names.
533
+
534
+ Returns:
535
+ List of registered prototype names
536
+ """
537
+ return list(__protos__.keys())
538
+
539
+
540
+ # 小写别名,保持向后兼容
541
+ list_prototypes = ListPrototypes
542
+
543
+
544
+ def ClearPrototypes() -> None:
545
+ """Clear all registered prototypes. Mainly for testing."""
546
+ __protos__.clear()
547
+
548
+
549
+ # 小写别名,保持向后兼容
550
+ clear_prototypes = ClearPrototypes
551
+
552
+
553
+ # Export all public symbols
554
+ __all__ = [
555
+ 'StageMachineMeta',
556
+ 'recursive',
557
+ 'listen',
558
+ 'behavior',
559
+ 'state',
560
+ 'GetPrototype',
561
+ 'get_prototype',
562
+ 'ListPrototypes',
563
+ 'list_prototypes',
564
+ 'ClearPrototypes',
565
+ 'clear_prototypes',
566
+ '__protos__',
567
+ 'IGNORES',
568
+ 'BASE_CLASSES',
569
+ ]