PyDecisionGraph 0.3.2.post1__cp315-cp315t-win_amd64.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 (148) hide show
  1. decision_graph/__infra__.pxd +1 -0
  2. decision_graph/__init__.pxd +1 -0
  3. decision_graph/__init__.py +39 -0
  4. decision_graph/decision_tree/__init__.pxd +2 -0
  5. decision_graph/decision_tree/__init__.py +85 -0
  6. decision_graph/decision_tree/bake/__infra__.pxd +502 -0
  7. decision_graph/decision_tree/bake/__init__.pxd +502 -0
  8. decision_graph/decision_tree/bake/__init__.py +29 -0
  9. decision_graph/decision_tree/bake/c_action.cp315t-win_amd64.pyd +0 -0
  10. decision_graph/decision_tree/bake/c_action.h +324 -0
  11. decision_graph/decision_tree/bake/c_action.pxd +54 -0
  12. decision_graph/decision_tree/bake/c_action.pyi +126 -0
  13. decision_graph/decision_tree/bake/c_action.pyx +65 -0
  14. decision_graph/decision_tree/bake/c_allocator_protocol.cp315t-win_amd64.pyd +0 -0
  15. decision_graph/decision_tree/bake/c_allocator_protocol.pxd +24 -0
  16. decision_graph/decision_tree/bake/c_allocator_protocol.pyi +61 -0
  17. decision_graph/decision_tree/bake/c_allocator_protocol.pyx +109 -0
  18. decision_graph/decision_tree/bake/c_bake.cp315t-win_amd64.pyd +0 -0
  19. decision_graph/decision_tree/bake/c_bake.h +629 -0
  20. decision_graph/decision_tree/bake/c_bake.pxd +52 -0
  21. decision_graph/decision_tree/bake/c_bake.pyi +151 -0
  22. decision_graph/decision_tree/bake/c_bake.pyx +142 -0
  23. decision_graph/decision_tree/bake/c_collections.cp315t-win_amd64.pyd +0 -0
  24. decision_graph/decision_tree/bake/c_collections.h +692 -0
  25. decision_graph/decision_tree/bake/c_collections.pxd +52 -0
  26. decision_graph/decision_tree/bake/c_collections.pyi +193 -0
  27. decision_graph/decision_tree/bake/c_collections.pyx +161 -0
  28. decision_graph/decision_tree/bake/c_const.cp315t-win_amd64.pyd +0 -0
  29. decision_graph/decision_tree/bake/c_const.h +395 -0
  30. decision_graph/decision_tree/bake/c_const.pxd +56 -0
  31. decision_graph/decision_tree/bake/c_const.pyi +305 -0
  32. decision_graph/decision_tree/bake/c_const.pyx +278 -0
  33. decision_graph/decision_tree/bake/c_edge.cp315t-win_amd64.pyd +0 -0
  34. decision_graph/decision_tree/bake/c_edge.h +511 -0
  35. decision_graph/decision_tree/bake/c_edge.pxd +117 -0
  36. decision_graph/decision_tree/bake/c_edge.pyi +192 -0
  37. decision_graph/decision_tree/bake/c_edge.pyx +270 -0
  38. decision_graph/decision_tree/bake/c_eval.h +848 -0
  39. decision_graph/decision_tree/bake/c_eval.pxd +40 -0
  40. decision_graph/decision_tree/bake/c_expr.cp315t-win_amd64.pyd +0 -0
  41. decision_graph/decision_tree/bake/c_expr.h +1900 -0
  42. decision_graph/decision_tree/bake/c_expr.pxd +129 -0
  43. decision_graph/decision_tree/bake/c_expr.pyi +280 -0
  44. decision_graph/decision_tree/bake/c_expr.pyx +272 -0
  45. decision_graph/decision_tree/bake/c_hierarchy.cp315t-win_amd64.pyd +0 -0
  46. decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
  47. decision_graph/decision_tree/bake/c_hierarchy.pxd +71 -0
  48. decision_graph/decision_tree/bake/c_hierarchy.pyi +409 -0
  49. decision_graph/decision_tree/bake/c_hierarchy.pyx +277 -0
  50. decision_graph/decision_tree/bake/c_logic_group.cp315t-win_amd64.pyd +0 -0
  51. decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
  52. decision_graph/decision_tree/bake/c_logic_group.pxd +162 -0
  53. decision_graph/decision_tree/bake/c_logic_group.pyi +264 -0
  54. decision_graph/decision_tree/bake/c_logic_group.pyx +308 -0
  55. decision_graph/decision_tree/bake/c_node.cp315t-win_amd64.pyd +0 -0
  56. decision_graph/decision_tree/bake/c_node.h +2660 -0
  57. decision_graph/decision_tree/bake/c_node.pxd +381 -0
  58. decision_graph/decision_tree/bake/c_node.pyi +438 -0
  59. decision_graph/decision_tree/bake/c_node.pyx +639 -0
  60. decision_graph/decision_tree/bake/c_reconstruct.cp315t-win_amd64.pyd +0 -0
  61. decision_graph/decision_tree/bake/c_reconstruct.pxd +63 -0
  62. decision_graph/decision_tree/bake/c_reconstruct.pyi +60 -0
  63. decision_graph/decision_tree/bake/c_reconstruct.pyx +252 -0
  64. decision_graph/decision_tree/bake/c_var.cp315t-win_amd64.pyd +0 -0
  65. decision_graph/decision_tree/bake/c_var.h +2054 -0
  66. decision_graph/decision_tree/bake/c_var.pxd +205 -0
  67. decision_graph/decision_tree/bake/c_var.pyi +271 -0
  68. decision_graph/decision_tree/bake/c_var.pyx +228 -0
  69. decision_graph/decision_tree/capi/__infra__.pxd +33 -0
  70. decision_graph/decision_tree/capi/__init__.pxd +33 -0
  71. decision_graph/decision_tree/capi/__init__.py +57 -0
  72. decision_graph/decision_tree/capi/c_abc.cp315t-win_amd64.pyd +0 -0
  73. decision_graph/decision_tree/capi/c_abc.pxd +246 -0
  74. decision_graph/decision_tree/capi/c_abc.pyi +770 -0
  75. decision_graph/decision_tree/capi/c_abc.pyx +1532 -0
  76. decision_graph/decision_tree/capi/c_collection.cp315t-win_amd64.pyd +0 -0
  77. decision_graph/decision_tree/capi/c_collection.pxd +22 -0
  78. decision_graph/decision_tree/capi/c_collection.pyi +245 -0
  79. decision_graph/decision_tree/capi/c_collection.pyx +138 -0
  80. decision_graph/decision_tree/capi/c_node.cp315t-win_amd64.pyd +0 -0
  81. decision_graph/decision_tree/capi/c_node.pxd +95 -0
  82. decision_graph/decision_tree/capi/c_node.pyi +492 -0
  83. decision_graph/decision_tree/capi/c_node.pyx +551 -0
  84. decision_graph/decision_tree/exc.py +189 -0
  85. decision_graph/decision_tree/native/__init__.py +57 -0
  86. decision_graph/decision_tree/native/abc.py +1167 -0
  87. decision_graph/decision_tree/native/collection.py +134 -0
  88. decision_graph/decision_tree/native/node.py +521 -0
  89. decision_graph/includes/decision_graph/decision_tree/bake/c_action.c +16320 -0
  90. decision_graph/includes/decision_graph/decision_tree/bake/c_action.h +324 -0
  91. decision_graph/includes/decision_graph/decision_tree/bake/c_allocator_protocol.c +12473 -0
  92. decision_graph/includes/decision_graph/decision_tree/bake/c_bake.c +16485 -0
  93. decision_graph/includes/decision_graph/decision_tree/bake/c_bake.h +629 -0
  94. decision_graph/includes/decision_graph/decision_tree/bake/c_collections.c +17381 -0
  95. decision_graph/includes/decision_graph/decision_tree/bake/c_collections.h +692 -0
  96. decision_graph/includes/decision_graph/decision_tree/bake/c_const.c +20815 -0
  97. decision_graph/includes/decision_graph/decision_tree/bake/c_const.h +395 -0
  98. decision_graph/includes/decision_graph/decision_tree/bake/c_edge.c +20696 -0
  99. decision_graph/includes/decision_graph/decision_tree/bake/c_edge.h +511 -0
  100. decision_graph/includes/decision_graph/decision_tree/bake/c_eval.h +848 -0
  101. decision_graph/includes/decision_graph/decision_tree/bake/c_expr.c +22065 -0
  102. decision_graph/includes/decision_graph/decision_tree/bake/c_expr.h +1900 -0
  103. decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.c +23267 -0
  104. decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
  105. decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.c +22434 -0
  106. decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
  107. decision_graph/includes/decision_graph/decision_tree/bake/c_node.c +28385 -0
  108. decision_graph/includes/decision_graph/decision_tree/bake/c_node.h +2660 -0
  109. decision_graph/includes/decision_graph/decision_tree/bake/c_reconstruct.c +15955 -0
  110. decision_graph/includes/decision_graph/decision_tree/bake/c_var.c +16881 -0
  111. decision_graph/includes/decision_graph/decision_tree/bake/c_var.h +2054 -0
  112. decision_graph/includes/decision_graph/decision_tree/capi/c_abc.c +59196 -0
  113. decision_graph/includes/decision_graph/decision_tree/capi/c_collection.c +19967 -0
  114. decision_graph/includes/decision_graph/decision_tree/capi/c_node.c +39229 -0
  115. decision_graph/logic_group/__init__.py +25 -0
  116. decision_graph/logic_group/base.py +100 -0
  117. decision_graph/logic_group/pending_request.py +384 -0
  118. decision_graph/webui/__init__.py +24 -0
  119. decision_graph/webui/bake/__init__.py +65 -0
  120. decision_graph/webui/bake/app.py +488 -0
  121. decision_graph/webui/bake/static/d3.v7.min.js +2 -0
  122. decision_graph/webui/bake/static/script.js +1440 -0
  123. decision_graph/webui/bake/static/style.css +920 -0
  124. decision_graph/webui/bake/templates/index.html +183 -0
  125. decision_graph/webui/bake/templates/offline.html +184 -0
  126. decision_graph/webui/capi/__init__.py +54 -0
  127. decision_graph/webui/capi/app.py +366 -0
  128. decision_graph/webui/capi/main.py +53 -0
  129. decision_graph/webui/capi/static/d3.v7.min.js +2 -0
  130. decision_graph/webui/capi/static/script.js +1026 -0
  131. decision_graph/webui/capi/static/style.css +656 -0
  132. decision_graph/webui/capi/templates/index.html +93 -0
  133. decision_graph/webui/capi/templates/offline.html +94 -0
  134. decision_graph/webui/native/__init__.py +54 -0
  135. decision_graph/webui/native/app.py +366 -0
  136. decision_graph/webui/native/main.py +53 -0
  137. decision_graph/webui/native/static/d3.v7.min.js +2 -0
  138. decision_graph/webui/native/static/script.js +1026 -0
  139. decision_graph/webui/native/static/style.css +656 -0
  140. decision_graph/webui/native/templates/index.html +93 -0
  141. decision_graph/webui/native/templates/offline.html +94 -0
  142. pydecisiongraph-0.3.2.post1.dist-info/METADATA +136 -0
  143. pydecisiongraph-0.3.2.post1.dist-info/RECORD +148 -0
  144. pydecisiongraph-0.3.2.post1.dist-info/WHEEL +5 -0
  145. pydecisiongraph-0.3.2.post1.dist-info/licenses/LICENSE +373 -0
  146. pydecisiongraph-0.3.2.post1.dist-info/top_level.txt +2 -0
  147. tests/bake_c_evaluator.cp315t-win_amd64.pyd +0 -0
  148. tests/bake_eval_probe.cp315t-win_amd64.pyd +0 -0
@@ -0,0 +1,1167 @@
1
+ from __future__ import annotations
2
+
3
+ import linecache
4
+ import operator
5
+ import sys
6
+ import uuid
7
+ from collections.abc import Callable
8
+ from typing import Any, Self, final
9
+
10
+ from . import LOGGER
11
+ from ..exc import *
12
+
13
+ LOGGER = LOGGER.getChild('abc')
14
+
15
+ __all__ = ['Singleton',
16
+ 'NodeEdgeCondition', 'ConditionElse', 'ConditionAny', 'ConditionAuto', 'BinaryCondition', 'ConditionTrue', 'ConditionFalse',
17
+ 'NO_CONDITION', 'ELSE_CONDITION', 'AUTO_CONDITION', 'TRUE_CONDITION', 'FALSE_CONDITION',
18
+ 'SkipContextsBlock', 'LogicExpression', 'LogicNode',
19
+ 'LogicGroupManager', 'LGM', 'LogicGroup',
20
+ 'ActionNode', 'BreakpointNode', 'PlaceholderNode',
21
+ 'NoAction', 'LongAction', 'ShortAction', 'CancelAction']
22
+
23
+
24
+ class Singleton(type):
25
+ _instances = {}
26
+
27
+ def __call__(cls, *args, **kwargs):
28
+ if cls not in cls._instances:
29
+ cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
30
+ return cls._instances[cls]
31
+
32
+
33
+ class NodeEdgeCondition(metaclass=Singleton):
34
+ def __init__(self, value=None):
35
+ self._value = value
36
+
37
+ def __hash__(self):
38
+ return hash(self._value)
39
+
40
+ def __repr__(self):
41
+ return f'<{self.__class__.__name__} {id(self):#0x}>'
42
+
43
+ @property
44
+ def value(self):
45
+ if self._value is None:
46
+ raise ValueError("Condition has no value assigned.")
47
+ return self._value
48
+
49
+ @value.setter
50
+ def value(self, value):
51
+ self._value = value
52
+
53
+
54
+ class ConditionElse(NodeEdgeCondition):
55
+ def __init__(self, *args, **kwargs):
56
+ super().__init__()
57
+ self._value = None
58
+
59
+ def __hash__(self):
60
+ return id(self)
61
+
62
+ def __repr__(self):
63
+ return f'<CONDITION Internal {id(self):#0x}>(Else)'
64
+
65
+ def __str__(self):
66
+ return 'Else'
67
+
68
+ @NodeEdgeCondition.value.setter
69
+ def value(self, value):
70
+ raise NotImplementedError()
71
+
72
+
73
+ class ConditionAny(NodeEdgeCondition):
74
+ def __init__(self, *args, **kwargs):
75
+ super().__init__()
76
+ self._value = None
77
+
78
+ def __hash__(self):
79
+ return id(self)
80
+
81
+ def __repr__(self):
82
+ return f'<CONDITION {id(self):#0x}>(Unconditional)'
83
+
84
+ def __str__(self):
85
+ return 'Unconditional'
86
+
87
+ @NodeEdgeCondition.value.setter
88
+ def value(self, value):
89
+ raise NotImplementedError()
90
+
91
+
92
+ class ConditionAuto(NodeEdgeCondition):
93
+ def __init__(self, *args, **kwargs):
94
+ super().__init__()
95
+ self._value = None
96
+
97
+ def __hash__(self):
98
+ raise NotImplementedError()
99
+
100
+ def __repr__(self):
101
+ return f'<CONDITION Internal {id(self):#0x}>(AutoInfer)'
102
+
103
+ def __str__(self):
104
+ return 'AutoInfer'
105
+
106
+
107
+ class BinaryCondition(NodeEdgeCondition):
108
+ def __init__(self, *args, **kwargs):
109
+ super().__init__()
110
+ self._value = None
111
+
112
+ def __hash__(self):
113
+ return id(self)
114
+
115
+ def __invert__(self):
116
+ raise NotImplementedError()
117
+
118
+ @NodeEdgeCondition.value.setter
119
+ def value(self, value):
120
+ raise NotImplementedError()
121
+
122
+
123
+ class ConditionTrue(BinaryCondition):
124
+ def __repr__(self):
125
+ return f'<CONDITION {id(self):#0x}>(True)'
126
+
127
+ def __str__(self):
128
+ return 'True'
129
+
130
+ def __bool__(self):
131
+ return True
132
+
133
+ def __int__(self):
134
+ return 1
135
+
136
+ def __neg__(self):
137
+ return FALSE_CONDITION
138
+
139
+ def __invert__(self):
140
+ return FALSE_CONDITION
141
+
142
+ @property
143
+ def value(self):
144
+ return True
145
+
146
+
147
+ class ConditionFalse(BinaryCondition):
148
+ def __repr__(self):
149
+ return f'<CONDITION {id(self):#0x}>(False)'
150
+
151
+ def __str__(self):
152
+ return 'False'
153
+
154
+ def __bool__(self):
155
+ return False
156
+
157
+ def __int__(self):
158
+ return 0
159
+
160
+ def __neg__(self):
161
+ return TRUE_CONDITION
162
+
163
+ def __invert__(self):
164
+ return TRUE_CONDITION
165
+
166
+ @property
167
+ def value(self):
168
+ return False
169
+
170
+
171
+ NO_CONDITION = ConditionAny()
172
+ ELSE_CONDITION = ConditionElse()
173
+ AUTO_CONDITION = ConditionAuto()
174
+ TRUE_CONDITION = ConditionTrue()
175
+ FALSE_CONDITION = ConditionFalse()
176
+
177
+
178
+ class SkipContextsBlock(object):
179
+ def __init__(self):
180
+ self.skip_exception = type(f"{self.__class__.__name__}SkipException", (EmptyBlock,), {"owner": self})
181
+ self.tracer_override = False
182
+ self.default_entry_check = True
183
+ self.__cframe = None
184
+ self.__original_trace = None
185
+ self.__enter_line = None
186
+
187
+ def _entry_check(self) -> Any:
188
+ return self.default_entry_check
189
+
190
+ @final
191
+ def __enter__(self):
192
+ if self._entry_check(): # Check if the expression evaluates to True
193
+ self._on_enter()
194
+ return self
195
+
196
+ self.__cframe = sys._getframe().f_back
197
+ self.__original_trace = self.get_trace()
198
+ self.__enter_line = (self.__cframe.f_code.co_filename, self.__cframe.f_lineno)
199
+ self.__cframe.f_trace = self.__tracer_skipper
200
+ sys.settrace(self.__tracer_skipper)
201
+ self.tracer_override = True
202
+
203
+ @final
204
+ def __exit__(self, exc_type, exc_value, exc_traceback):
205
+ sys.settrace(None)
206
+ self.__restore_trace()
207
+
208
+ if exc_type is None:
209
+ self._on_exit()
210
+ return
211
+
212
+ if issubclass(exc_type, self.skip_exception):
213
+ return True
214
+
215
+ self._on_exit()
216
+ # Propagate any other exception raised in the block
217
+ return False
218
+
219
+ def _on_enter(self):
220
+ pass
221
+
222
+ def _on_exit(self):
223
+ pass
224
+
225
+ @staticmethod
226
+ def get_trace():
227
+ try:
228
+ # Check if PyDev debugger is active
229
+ # noinspection PyUnresolvedReferences
230
+ import pydevd
231
+ debugger = pydevd.GetGlobalDebugger()
232
+ if debugger is not None:
233
+ return debugger.trace_dispatch # Use PyDev's trace function
234
+ except ImportError:
235
+ pass # PyDev debugger is not installed or active
236
+
237
+ # Fall back to the standard trace function
238
+ return sys.gettrace()
239
+
240
+ def __restore_trace(self):
241
+ if self.tracer_override:
242
+ # print('[restore_trace]', f'storing tracer to {self.__original_trace}.')
243
+ self.__cframe.f_trace = self.__original_trace
244
+ sys.settrace(self.__original_trace) # Restore the original trace
245
+
246
+ def __tracer_skipper(self, frame, event, arg):
247
+ line = linecache.getline(frame.f_code.co_filename, frame.f_lineno).strip()
248
+ # print(f'[SkipContextsBlock]', line, frame, event, arg, (frame.f_code.co_filename, frame.f_lineno), self.__enter_line)
249
+ if line.startswith(('pass', '...')):
250
+ return self.__tracer_skipper
251
+ elif self.__enter_line == (frame.f_code.co_filename, frame.f_lineno):
252
+ # print(f'[SkipContextsBlock]', 'Restoring trace...')
253
+ self.__restore_trace()
254
+ return self.__tracer_skipper
255
+ elif self.tracer_override:
256
+ raise self.skip_exception("Expression evaluated to be False, cannot enter the block.")
257
+
258
+
259
+ class LogicExpression(SkipContextsBlock):
260
+ def __init__(self, *, expression: float | int | bool | Exception | Callable[[], Any], dtype: type = None, repr: str = None, uid: uuid.UUID = None):
261
+ super().__init__()
262
+ self.expression = expression
263
+ self.dtype = dtype
264
+ self.repr = repr if repr is not None else str(expression)
265
+ self.uid = uuid.uuid4() if uid is None else uid
266
+
267
+ def _entry_check(self) -> Any:
268
+ return bool(self._eval(False))
269
+
270
+ def eval(self, enforce_dtype: bool = False) -> Any:
271
+ return self._eval(enforce_dtype)
272
+
273
+ def _eval(self, enforce_dtype: bool) -> Any:
274
+ if isinstance(self.expression, (float, int, bool, str)):
275
+ value = self.expression
276
+ elif callable(self.expression):
277
+ value = self.expression()
278
+ elif isinstance(self.expression, Exception):
279
+ raise self.expression
280
+ else:
281
+ raise TypeError(f"Unsupported expression type: {type(self.expression)}.")
282
+
283
+ if self.dtype is Any or self.dtype is None:
284
+ pass # No type enforcement
285
+ elif enforce_dtype:
286
+ value = self.dtype(value)
287
+ elif not isinstance(value, self.dtype):
288
+ LOGGER.warning(f"Evaluated value {value} does not match dtype {self.dtype.__name__}.")
289
+
290
+ return value
291
+
292
+ @classmethod
293
+ def cast(cls, value: int | float | bool | Exception | Self, dtype: type = None) -> Self:
294
+ if isinstance(value, LogicExpression):
295
+ return value
296
+ if isinstance(value, (int, float, bool)):
297
+ return LogicExpression(
298
+ expression=value,
299
+ dtype=dtype or type(value),
300
+ repr=str(value)
301
+ )
302
+ if callable(value):
303
+ return LogicExpression(
304
+ expression=value,
305
+ dtype=dtype or Any,
306
+ repr=f"Eval({value})"
307
+ )
308
+ if isinstance(value, Exception):
309
+ return LogicExpression(
310
+ expression=value,
311
+ dtype=dtype or Any,
312
+ repr=f"Raises({type(value).__name__}: {value})"
313
+ )
314
+ raise TypeError(f"Unsupported type for LogicExpression conversion: {type(value)}.")
315
+
316
+ def __bool__(self) -> bool:
317
+ value = self.eval()
318
+ if isinstance(value, LogicExpression):
319
+ return True
320
+ return bool(value)
321
+
322
+ def __and__(self, other: Self | bool) -> Self:
323
+ other_expr = self.cast(value=other, dtype=bool)
324
+ new_expr = LogicExpression(
325
+ expression=lambda: self.eval() and other_expr.eval(),
326
+ dtype=bool,
327
+ repr=f"({self.repr} and {other_expr.repr})"
328
+ )
329
+ return new_expr
330
+
331
+ def __eq__(self, other: int | float | bool | str | Self) -> bool:
332
+ """
333
+ This behavior must differ from the CAPI version.
334
+ Python interface of __eq__ must return a boolean value.
335
+ Otherwise it interferes with the __contains__ operation.
336
+ Comparing reference is the most safe way.
337
+ """
338
+ return id(self) == id(other)
339
+
340
+ def __or__(self, other: Self | bool) -> Self:
341
+ other_expr = self.cast(value=other, dtype=bool)
342
+ new_expr = LogicExpression(
343
+ expression=lambda: self.eval() or other_expr.eval(),
344
+ dtype=bool,
345
+ repr=f"({self.repr} or {other_expr.repr})"
346
+ )
347
+ return new_expr
348
+
349
+ # Math operators
350
+ @staticmethod
351
+ def _math_op(self: LogicExpression, other: int | float | LogicExpression, op: Callable, operator_str: str, dtype: type = None) -> LogicExpression:
352
+ other_expr = LogicExpression.cast(other)
353
+
354
+ if dtype is None:
355
+ dtype = self.dtype
356
+
357
+ new_expr = LogicExpression(
358
+ expression=lambda: op(self.eval(), other_expr.eval()),
359
+ dtype=dtype,
360
+ repr=f"({self.repr} {operator_str} {other_expr.repr})",
361
+ )
362
+ return new_expr
363
+
364
+ def __add__(self, other: int | float | bool | Self) -> Self:
365
+ return LogicExpression._math_op(self, other, operator.add, "+", None)
366
+
367
+ def __sub__(self, other: int | float | bool | Self) -> Self:
368
+ return LogicExpression._math_op(self, other, operator.sub, "-", None)
369
+
370
+ def __mul__(self, other: int | float | bool | Self) -> Self:
371
+ return LogicExpression._math_op(self, other, operator.mul, "*", None)
372
+
373
+ def __truediv__(self, other: int | float | bool | Self) -> Self:
374
+ return LogicExpression._math_op(self, other, operator.truediv, "/", None)
375
+
376
+ def __floordiv__(self, other: int | float | bool | Self) -> Self:
377
+ return LogicExpression._math_op(self, other, operator.floordiv, "//", None)
378
+
379
+ def __pow__(self, other: int | float | bool | Self) -> Self:
380
+ return LogicExpression._math_op(self, other, operator.pow, "**", None)
381
+
382
+ # Comparison operators, note that __eq__, __ne__ is special and should not implement as math operator
383
+ def __lt__(self, other: int | float | bool | Self) -> Self:
384
+ return LogicExpression._math_op(self, other, operator.lt, "<", bool)
385
+
386
+ def __le__(self, other: int | float | bool | Self) -> Self:
387
+ return LogicExpression._math_op(self, other, operator.le, "<=", bool)
388
+
389
+ def __gt__(self, other: int | float | bool | Self) -> Self:
390
+ return LogicExpression._math_op(self, other, operator.gt, ">", bool)
391
+
392
+ def __ge__(self, other: int | float | bool | Self) -> Self:
393
+ return LogicExpression._math_op(self, other, operator.ge, ">=", bool)
394
+
395
+ def __repr__(self) -> str:
396
+ return f"<{self.__class__.__name__}>(dtype={'Any' if self.dtype is None else self.dtype.__name__}, repr={self.repr})"
397
+
398
+
399
+ class LogicGroupManager(metaclass=Singleton):
400
+ def __init__(self):
401
+ # Dictionary to store cached LogicGroup instances
402
+ self._cache = {}
403
+
404
+ # Stack cursors: top of stack is at index 0
405
+ self._active_groups: list[LogicGroup] = []
406
+ self._active_nodes: list[LogicNode] = []
407
+ self._breakpoint_nodes: list[BreakpointNode] = []
408
+ self._shelved_state: list[dict] = []
409
+
410
+ self.inspection_mode = False
411
+ self.vigilant_mode = False
412
+
413
+ def __call__(self, name: str, cls: type[LogicGroup], **kwargs) -> LogicGroup:
414
+ reg_key = (cls.__module__, cls.__qualname__)
415
+ registry = self._cache.get(reg_key)
416
+ if registry is None:
417
+ registry = self._cache[reg_key] = {}
418
+
419
+ if name in registry:
420
+ return registry[name]
421
+
422
+ logic_group = cls(name=name, **kwargs)
423
+ registry[name] = logic_group
424
+ return logic_group
425
+
426
+ def __contains__(self, instance: LogicGroup) -> bool:
427
+ cls = instance.__class__
428
+ name = instance.name
429
+ reg_key = (cls.__module__, cls.__qualname__)
430
+ registry = self._cache.get(reg_key)
431
+ if registry is None:
432
+ return False
433
+ return name in registry
434
+
435
+ def _lg_enter(self, logic_group: LogicGroup):
436
+ # Set parent if there's an active group
437
+ if self._active_groups:
438
+ logic_group.parent = self._active_groups[0]
439
+ self._active_groups.insert(0, logic_group)
440
+
441
+ def _lg_exit(self, logic_group: LogicGroup = None):
442
+ if not self._active_groups:
443
+ raise RuntimeError("No active LogicGroup to exit.")
444
+
445
+ current = self._active_groups[0]
446
+ if logic_group is not None and current is not logic_group:
447
+ raise AssertionError("The LogicGroup is not currently active.")
448
+ # If logic_group is None, we exit the top one (current)
449
+
450
+ # Activate pending breakpoints tied to this group
451
+ for node in self._breakpoint_nodes:
452
+ if node.break_from is current:
453
+ node.await_connection = True
454
+
455
+ self._active_groups.pop(0)
456
+
457
+ def _ln_enter(self, logic_node: LogicNode):
458
+ if isinstance(logic_node, ActionNode):
459
+ LOGGER.error('Enter the with code block of an ActionNode rejected. Check if this is intentional?')
460
+ return
461
+
462
+ # Connect and remove all awaiting breakpoint nodes
463
+ for breakpoint_node in self._breakpoint_nodes[:]:
464
+ if breakpoint_node.await_connection:
465
+ breakpoint_node._connect(logic_node)
466
+ self._breakpoint_nodes.remove(breakpoint_node)
467
+
468
+ # If no active node, push directly
469
+ if not self._active_nodes:
470
+ self._active_nodes.insert(0, logic_node)
471
+ return
472
+
473
+ # Otherwise, get current active node (top = index 0)
474
+ active_node = self._active_nodes[0]
475
+ placeholder = active_node._get_placeholder()
476
+ active_node._replace(placeholder, logic_node)
477
+ self._active_nodes.insert(0, logic_node)
478
+
479
+ def _ln_exit(self, logic_node: LogicNode):
480
+ if not self._active_nodes or self._active_nodes[0] is not logic_node:
481
+ raise AssertionError("The LogicNode is not currently active.")
482
+ self._active_nodes.pop(0)
483
+
484
+ def shelve(self):
485
+ shelved = {
486
+ 'active_groups': self._active_groups.copy(),
487
+ 'active_nodes': self._active_nodes.copy(),
488
+ 'breakpoint_nodes': self._breakpoint_nodes.copy(),
489
+ 'inspection_mode': self.inspection_mode,
490
+ 'vigilant_mode': self.vigilant_mode,
491
+ }
492
+ self._shelved_state.insert(0, shelved)
493
+
494
+ # Reset to clean state
495
+ self._active_groups = []
496
+ self._active_nodes = []
497
+ self._breakpoint_nodes = []
498
+
499
+ return shelved
500
+
501
+ def unshelve(self):
502
+ if not self._shelved_state:
503
+ raise RuntimeError("No shelved state to unshelve.")
504
+
505
+ state = self._shelved_state.pop(0)
506
+
507
+ self._active_groups = state['active_groups']
508
+ self._active_nodes = state['active_nodes']
509
+ self._breakpoint_nodes = state['breakpoint_nodes']
510
+ self.inspection_mode = state['inspection_mode']
511
+ self.vigilant_mode = state['vigilant_mode']
512
+
513
+ def clear(self):
514
+ self._cache.clear()
515
+ self._active_groups.clear()
516
+ self._active_nodes.clear()
517
+ self._breakpoint_nodes.clear()
518
+
519
+ @property
520
+ def active_group(self) -> LogicGroup | None:
521
+ return self._active_groups[0] if self._active_groups else None
522
+
523
+ @property
524
+ def active_node(self) -> LogicNode | None:
525
+ return self._active_nodes[0] if self._active_nodes else None
526
+
527
+
528
+ LGM = LogicGroupManager()
529
+
530
+
531
+ class LogicGroup(object):
532
+ def __init__(self, *, name: str = None, parent: LogicGroup = None, contexts: dict | None = None, **kwargs):
533
+ self.name = f"{self.__class__.__name__}.{uuid.uuid4()}" if name is None else name
534
+
535
+ if self in LGM:
536
+ raise RuntimeError(f"LogicGroup {name} of type {self.__class__.__name__} already exists!")
537
+
538
+ self.parent = parent
539
+ self.Break = type(f"{self.__class__.__name__}Break", (BreakBlock,), {})
540
+ self.contexts = {} if contexts is None else contexts
541
+
542
+ def _break_inspection(self) -> None:
543
+ # Case 1: No active node, breaks affect nothing
544
+ if not LGM._active_nodes:
545
+ return
546
+
547
+ active_node = LGM._active_nodes[0] # top of stack
548
+
549
+ # Step 2.1: Locate placeholder and replace with breakpoint
550
+ placeholder = active_node._get_placeholder()
551
+ breakpoint_node = BreakpointNode()
552
+ breakpoint_node.break_from = self
553
+ active_node._replace(placeholder, breakpoint_node)
554
+
555
+ # Step 2.2: Push to global breakpoint stack
556
+ LGM._breakpoint_nodes.insert(0, breakpoint_node)
557
+
558
+ def _break_active(self) -> None:
559
+ if not LGM._active_groups:
560
+ raise RuntimeError("No active LogicGroup to break from.")
561
+ active_group = LGM._active_groups[0]
562
+ if active_group is not self:
563
+ raise IndexError('Not breaking from the top active LogicGroup.')
564
+ raise self.Break()
565
+
566
+ def _break_runtime(self) -> None:
567
+ if not LGM._active_nodes:
568
+ raise RuntimeError("No active node context to break from.")
569
+
570
+ # Step 1: Validate that this group is in the active group stack
571
+ if self not in LGM._active_groups:
572
+ raise ValueError(f"Break scope {self} not in active LogicGroup stack.")
573
+
574
+ # Step 2: Unwind the group stack from top until we hit `self`
575
+ # We iterate from the top (end of list) downward
576
+ for group in LGM._active_groups[:]:
577
+ group._break_active()
578
+ if group is self:
579
+ break
580
+
581
+ # === Python Interfaces ===
582
+
583
+ def __repr__(self) -> str:
584
+ return f'<{self.__class__.__name__}>({self.name!r})'
585
+
586
+ def __enter__(self):
587
+ LGM._lg_enter(self)
588
+ return self
589
+
590
+ def __exit__(self, exc_type: type | None, exc_value: BaseException | None, exc_traceback: Any) -> bool | None:
591
+ LGM._lg_exit(self)
592
+
593
+ if exc_type is None:
594
+ return None
595
+
596
+ # Suppress only the dynamically created Break exception for this group
597
+ if issubclass(exc_type, self.Break):
598
+ return True
599
+ return False
600
+
601
+ @classmethod
602
+ def break_(cls, scope: LogicGroup = None) -> None:
603
+ if scope is None:
604
+ scope = LGM.active_group
605
+
606
+ if scope is None:
607
+ raise RuntimeError("No active LogicGroup to break from.")
608
+
609
+ if LGM.inspection_mode:
610
+ scope._break_inspection()
611
+ else:
612
+ scope._break_runtime()
613
+
614
+ def break_active(self) -> None:
615
+ self._break_active()
616
+
617
+ def break_inspection(self) -> None:
618
+ self._break_inspection()
619
+
620
+ def break_runtime(self) -> None:
621
+ self._break_runtime()
622
+
623
+
624
+ class LogicNode(LogicExpression):
625
+ def __init__(self, *, expression: float | int | bool | Exception | Callable[[], Any], dtype: type = None, repr: str = None, uid: uuid.UUID = None):
626
+ super().__init__(expression=expression, dtype=dtype, repr=repr, uid=uid)
627
+
628
+ self.subordinates = []
629
+ self.condition_to_parent = NO_CONDITION
630
+ self.parent = None
631
+ self.children = {}
632
+ self.labels = [_.name for _ in LGM._active_groups]
633
+ self.autogen = False
634
+
635
+ def _infer_condition(self, child: LogicNode) -> NodeEdgeCondition:
636
+ size = len(self.subordinates)
637
+
638
+ # Case 1: No child node registered, the first child is always TRUE unless specified
639
+ if size == 0:
640
+ return TRUE_CONDITION
641
+
642
+ last_node = self.subordinates[0]
643
+ last_condition = last_node.condition_to_parent
644
+
645
+ # Case 1: Only 1 child node registered, the second child is always opposite of the first
646
+ if size == 1 and isinstance(last_condition, BinaryCondition):
647
+ if last_condition is TRUE_CONDITION:
648
+ return FALSE_CONDITION
649
+ else:
650
+ return TRUE_CONDITION
651
+
652
+ # Case 2: Only 1 child node registered, and the first condition is not specified.
653
+ if size == 1 and isinstance(last_condition, ConditionElse):
654
+ # Case 2.1: If the child is auto generated, we can assume this child should be TRUE
655
+ if last_node.autogen:
656
+ return TRUE_CONDITION
657
+ # Case 2.2: Otherwise, we cannot infer the condition
658
+
659
+ if size == 1:
660
+ raise EdgeValueError('Cannot auto-infer condition from single existing non-binary condition.')
661
+
662
+ second_node = self.subordinates[1]
663
+ second_condition = second_node.condition_to_parent
664
+ # Case 3: check if any 2 node is from autogeneration
665
+ # Returning the condition of the auto-generated node, so that it can be replaced later.
666
+ if last_node.autogen:
667
+ return last_condition
668
+ elif second_node.autogen:
669
+ return second_condition
670
+
671
+ if size > 2:
672
+ raise TooManyChildren(size)
673
+ # Case 4: both conditions are non-binary, cannot infer
674
+ raise EdgeValueError('Cannot auto-infer condition for non-binary existing condition.')
675
+
676
+ def _get_placeholder(self) -> PlaceholderNode:
677
+ if not self.subordinates:
678
+ placeholder = PlaceholderNode(auto_connect=False)
679
+ self._append(placeholder, TRUE_CONDITION)
680
+ return placeholder
681
+
682
+ # Case 2: Traverse the stack to find existing placeholder
683
+ for node in self.subordinates:
684
+ if isinstance(node, PlaceholderNode):
685
+ return node
686
+
687
+ # Case 3: No existing placeholder, create a new one with AUTO_CONDITION.
688
+ placeholder = PlaceholderNode(auto_connect=False)
689
+ # c_append will validate and infer the condition, and raise error as we intended.
690
+ self._append(placeholder, AUTO_CONDITION)
691
+ return placeholder
692
+
693
+ def _append(self, child: LogicNode, condition: NodeEdgeCondition) -> None:
694
+ if condition is None:
695
+ raise ValueError("LogicNode must have an valid edge condition.")
696
+
697
+ if condition is AUTO_CONDITION:
698
+ condition = self._infer_condition(child)
699
+
700
+ if condition in self.children:
701
+ raise KeyError(f"Edge {condition} already registered.")
702
+
703
+ self.children[condition] = child
704
+ self.subordinates.insert(0, child)
705
+ child.parent = self
706
+ child.condition_to_parent = condition
707
+
708
+ def _overwrite(self, new_node: LogicNode, condition: NodeEdgeCondition) -> None:
709
+ if condition is None:
710
+ raise ValueError("LogicNode must have an valid edge condition.")
711
+
712
+ if condition is AUTO_CONDITION:
713
+ condition = self._infer_condition(new_node)
714
+
715
+ if condition not in self.children:
716
+ raise KeyError(f"Edge {condition} not registered, cannot overwrite.")
717
+
718
+ original_node = self.children[condition]
719
+ self.children[condition] = new_node
720
+ new_node.parent = self
721
+ new_node.condition_to_parent = condition
722
+
723
+ self.subordinates[self.subordinates.index(original_node)] = new_node
724
+ original_node.parent = None
725
+ original_node.condition_to_parent = NO_CONDITION
726
+
727
+ def _replace(self, original_node: LogicNode, new_node: LogicNode) -> None:
728
+ # The __eq__ of LogicExpression is overloaded, so we must check identity here.
729
+ for node in LGM._active_nodes:
730
+ if node is original_node:
731
+ raise RuntimeError('Must not replace active node. Existing first required.')
732
+
733
+ self.subordinates[self.subordinates.index(original_node)] = new_node
734
+
735
+ self.children[original_node.condition_to_parent] = new_node
736
+ new_node.parent = self
737
+ new_node.condition_to_parent = original_node.condition_to_parent
738
+
739
+ original_node.parent = None
740
+ original_node.condition_to_parent = NO_CONDITION
741
+
742
+ def _validate(self):
743
+ if len(self.subordinates) != len(self.children):
744
+ raise NodeValueError('Subordinate stack size does not match registered children.')
745
+
746
+ for condition, child in self.children.items():
747
+ if child.condition_to_parent is not condition:
748
+ raise EdgeValueError('Child node condition does not match registered condition.')
749
+ if child not in self.subordinates:
750
+ raise ValueError(f"LogicNode {child} not found in stack")
751
+
752
+ def _eval_recursively(self, path: list | None = None, default: Any = NO_DEFAULT) -> tuple[Any, list]:
753
+ if path is None:
754
+ path = [self]
755
+ else:
756
+ path.append(self)
757
+
758
+ value = self._eval(False)
759
+ if self.is_leaf:
760
+ return value, path
761
+
762
+ else_branch = None
763
+ for child in self.subordinates:
764
+ condition = child.condition_to_parent
765
+ if condition is ELSE_CONDITION:
766
+ else_branch = child
767
+ elif condition is NO_CONDITION or value == condition.value:
768
+ return child._eval_recursively(path, default)
769
+
770
+ if else_branch is not None:
771
+ return else_branch._eval_recursively(path, default)
772
+
773
+ if default is NO_DEFAULT:
774
+ raise ValueError(f"No matching condition found for value {value} at '{self.repr}'.")
775
+
776
+ LOGGER.warning(f"No matching condition found for value {value} at '{self.repr}', using default {default}.")
777
+ return default, path
778
+
779
+ def _auto_fill(self) -> None:
780
+ size = len(self.children)
781
+ no_action = NoAction(auto_connect=False, autogen=True)
782
+
783
+ # Case 1: No child node registered
784
+ if size == 0:
785
+ LOGGER.warning(f"{self} having no [True] branch. Check the <with> statement code block to see if this is intended.")
786
+ self._append(no_action, NO_CONDITION)
787
+ return
788
+
789
+ node = self.subordinates[0]
790
+ condition = node.condition_to_parent
791
+
792
+ # Case 2: Single child node
793
+ if size == 1:
794
+ # Case 2.1: single child with no condition
795
+ if condition is NO_CONDITION:
796
+ return
797
+ # Case 2.2: single child deliberately set to ELSE
798
+ if condition is ELSE_CONDITION:
799
+ LOGGER.warning(f"{self} having no [True] branch. Check the <with> statement code block to see if this is intended.")
800
+ self._overwrite(no_action, TRUE_CONDITION)
801
+ return
802
+ # Case 2.4: single child with binary condition
803
+ if isinstance(condition, BinaryCondition):
804
+ self._append(no_action, ~condition)
805
+ return
806
+
807
+ # Case 3: Double Node
808
+ second_node = self.subordinates[1]
809
+ second_condition = second_node.condition_to_parent
810
+
811
+ if size == 2:
812
+ # Case 3.1: unconditioned branch not allowed with two branches
813
+ if condition is NO_CONDITION or second_condition is NO_CONDITION:
814
+ raise TooManyChildren('Cannot have unconditioned branch when there are two branches.')
815
+ # Case 3.2: one branch is ELSE — valid
816
+ if condition is ELSE_CONDITION or second_condition is ELSE_CONDITION:
817
+ return
818
+ # Case 3.3: both binary — valid
819
+ if isinstance(condition, BinaryCondition) and isinstance(second_condition, BinaryCondition):
820
+ return
821
+ # Case 3.4: both non-binary — add protective else
822
+ if not isinstance(condition, BinaryCondition) and not isinstance(second_condition, BinaryCondition):
823
+ self._append(no_action, ELSE_CONDITION)
824
+ return
825
+ raise EdgeValueError(f'Conflicting conditions detected, {condition} and {second_condition}.')
826
+
827
+ # Case 4: Multiple nodes (>2)
828
+ else_detected = False
829
+ for cond in self.children:
830
+ if cond is NO_CONDITION:
831
+ raise TooManyChildren('Cannot have unconditioned branch when there are multiple branches.')
832
+ if cond is ELSE_CONDITION:
833
+ else_detected = True
834
+ if isinstance(cond, BinaryCondition):
835
+ raise TooManyChildren('Cannot have binary branch when there are more than 2 branches.')
836
+ if not else_detected:
837
+ self._append(no_action, ELSE_CONDITION)
838
+
839
+ def _consolidate_placeholder(self) -> int:
840
+ placeholder_count = 0
841
+ i = 0
842
+ while i < len(self.subordinates):
843
+ node = self.subordinates[i]
844
+ if isinstance(node, PlaceholderNode):
845
+ self._replace(node, NoAction(auto_connect=False, autogen=True))
846
+ placeholder_count += 1
847
+ # no increment: replacement maintains list length; continue checking same index
848
+ else:
849
+ i += 1
850
+ return placeholder_count
851
+
852
+ def _entry_check(self) -> bool:
853
+ if LGM.inspection_mode:
854
+ return True
855
+ return bool(self._eval(False))
856
+
857
+ def _on_enter(self) -> None:
858
+ # Placeholders must be in reversed order, so that locating placeholder works correctly.
859
+ self._append(PlaceholderNode(auto_connect=False), FALSE_CONDITION)
860
+ self._append(PlaceholderNode(auto_connect=False), TRUE_CONDITION)
861
+ LGM._ln_enter(self)
862
+
863
+ def _on_exit(self) -> None:
864
+ self._validate()
865
+ self._auto_fill()
866
+ self._consolidate_placeholder()
867
+ LGM._ln_exit(self)
868
+
869
+ # === Python Interfaces ===
870
+
871
+ def __rshift__(self, other: LogicNode) -> LogicNode:
872
+ self._append(other, AUTO_CONDITION)
873
+ return other # Allow chaining
874
+
875
+ def __call__(self, default: Any = None) -> Any:
876
+ if default is None:
877
+ default = NoAction(auto_connect=False, autogen=True)
878
+ inspection_mode = LGM.inspection_mode
879
+ if inspection_mode:
880
+ LOGGER.info('LGM inspection mode temporarily disabled to evaluate correctly.')
881
+ LGM.inspection_mode = False
882
+ try:
883
+ path = []
884
+ value, _ = self._eval_recursively(path, default)
885
+ return value
886
+ finally:
887
+ LGM.inspection_mode = inspection_mode
888
+
889
+ def __repr__(self) -> str:
890
+ return f'<{self.__class__.__name__}>({self.repr!r})'
891
+
892
+ def __hash__(self) -> int:
893
+ return id(self)
894
+
895
+ def append(self, child: LogicNode, condition: NodeEdgeCondition = AUTO_CONDITION) -> None:
896
+ self._append(child, condition)
897
+
898
+ def overwrite(self, new_node: LogicNode, condition: NodeEdgeCondition) -> None:
899
+ self._overwrite(new_node, condition)
900
+
901
+ def replace(self, original_node: LogicNode, new_node: LogicNode) -> None:
902
+ self._replace(original_node, new_node)
903
+
904
+ def eval_recursively(self, path: list | None = None, default: Any = NO_DEFAULT) -> tuple[Any, list]:
905
+ return self._eval_recursively(path, default)
906
+
907
+ def list_labels(self) -> dict[str, list[LogicNode]]:
908
+ labels = {}
909
+
910
+ def traverse(node: LogicNode):
911
+ for group in node.labels:
912
+ labels.setdefault(group, []).append(node)
913
+ for child in node.children.values():
914
+ traverse(child)
915
+
916
+ traverse(self)
917
+ return labels
918
+
919
+ @property
920
+ def leaves(self):
921
+ if not self.subordinates:
922
+ yield self
923
+ else:
924
+ for child in self.subordinates:
925
+ yield from child.leaves
926
+
927
+ @property
928
+ def is_leaf(self) -> bool:
929
+ return not self.children
930
+
931
+ @property
932
+ def child_stack(self):
933
+ yield from self.subordinates
934
+
935
+ @property
936
+ def descendants(self):
937
+ for child in self.subordinates:
938
+ yield child
939
+ yield from child.descendants
940
+ frame = frame.prev
941
+
942
+
943
+ class BreakpointNode(LogicNode):
944
+ def __init__(self, *, break_from: LogicGroup = None, expression: float | int | bool | Exception | Callable[[], Any] = None, dtype: type = None, repr: str = None, uid: uuid.UUID = None):
945
+ super().__init__(
946
+ expression=NoAction(auto_connect=False, autogen=True) if expression is None else expression,
947
+ dtype=dtype,
948
+ repr=repr,
949
+ uid=uid
950
+ )
951
+
952
+ self.break_from = break_from
953
+ self.autogen = True
954
+ self.await_connection = False
955
+
956
+ def _connect(self, child: LogicNode) -> None:
957
+ if self.subordinates:
958
+ raise TooManyChildren(f'{self.__class__.__name__} must not have more than one child node.')
959
+ self._append(child, NO_CONDITION)
960
+ self.await_connection = False
961
+
962
+ def _on_enter(self) -> None:
963
+ if self.subordinates:
964
+ raise TooManyChildren(f'{self.__class__.__name__} must not have more than one child node.')
965
+ self.await_connection = False
966
+ try:
967
+ LGM._breakpoint_nodes.remove(self)
968
+ except ValueError: # list.remove, not the C stack's NodeNotFountError
969
+ pass
970
+ self._append(PlaceholderNode(auto_connect=False), NO_CONDITION)
971
+ LGM._active_nodes.insert(0, self)
972
+
973
+ def _on_exit(self) -> None:
974
+ LGM._ln_exit(self)
975
+
976
+ def _eval(self, enforce_dtype: bool) -> Any:
977
+ if not self.subordinates:
978
+ if LGM.vigilant_mode:
979
+ raise NodeValueError(f'{self} not connected.')
980
+ return self.expression
981
+
982
+ linked_to = self.subordinates[0]
983
+ return linked_to._eval(enforce_dtype)
984
+
985
+ def _eval_recursively(self, path: list | None = None, default: Any = NO_DEFAULT) -> tuple[Any, list]:
986
+ if path is None:
987
+ path = []
988
+ path.append(self)
989
+
990
+ if not self.subordinates:
991
+ if LGM.vigilant_mode:
992
+ raise NodeValueError(f'{self} not connected.')
993
+ return self.expression, path
994
+
995
+ linked_to = self.subordinates[0]
996
+ return linked_to._eval_recursively(path, default)
997
+
998
+ def __repr__(self) -> str:
999
+ if self.subordinates:
1000
+ return f'<{self.__class__.__name__} connected>(break_from={self.break_from})'
1001
+ elif self.await_connection:
1002
+ return f'<{self.__class__.__name__} active>(break_from={self.break_from})'
1003
+ elif self.parent:
1004
+ return f'<{self.__class__.__name__} idle>(break_from={self.break_from})'
1005
+ else:
1006
+ return f'<{self.__class__.__name__} dangling>(break_from={self.break_from})'
1007
+
1008
+ @classmethod
1009
+ def break_(cls, break_from: LogicGroup, **kwargs):
1010
+ breakpoint_node = BreakpointNode(break_from=break_from, **kwargs)
1011
+ active_node = LGM.active_node
1012
+ placeholder = active_node._get_placeholder()
1013
+ active_node._replace(placeholder, breakpoint_node)
1014
+ return breakpoint_node
1015
+
1016
+ def connect(self, child: LogicNode) -> None:
1017
+ self._connect(child)
1018
+
1019
+ @property
1020
+ def linked_to(self) -> LogicNode | None:
1021
+ return self.subordinates[0] if self.subordinates else None
1022
+
1023
+
1024
+ class ActionNode(LogicNode):
1025
+ def __init__(
1026
+ self,
1027
+ *,
1028
+ action: Callable[[], Any] | None = None,
1029
+ expression: Any = None,
1030
+ dtype: type = None,
1031
+ repr: str | None = None,
1032
+ uid: uuid.UUID = None,
1033
+ auto_connect: bool = True,
1034
+ **kwargs
1035
+ ):
1036
+ # Do not capture logic group labels — action nodes are leaves
1037
+ super().__init__(expression=expression, dtype=dtype, repr=repr, uid=uid)
1038
+
1039
+ self.action = action
1040
+
1041
+ if auto_connect:
1042
+ self._auto_connect()
1043
+
1044
+ def _auto_connect(self) -> None:
1045
+ if not LGM._active_nodes:
1046
+ if LGM.vigilant_mode:
1047
+ raise NodeValueError(f'Cannot set ActionNode {self} as root node.')
1048
+ return
1049
+
1050
+ active_node = LGM._active_nodes[0] # top of stack
1051
+ placeholder = active_node._get_placeholder()
1052
+ active_node._replace(placeholder, self)
1053
+
1054
+ def _post_eval(self) -> None:
1055
+ if self.action is not None:
1056
+ self.action()
1057
+
1058
+ def _append(self, child: LogicNode, condition: NodeEdgeCondition) -> None:
1059
+ raise TooManyChildren('Action node must not have any child node.')
1060
+
1061
+ def _on_enter(self) -> None:
1062
+ raise NodeContextError('ActionNode does not support context management with <with> statement.')
1063
+
1064
+ def _on_exit(self) -> None:
1065
+ pass
1066
+
1067
+ def _eval_recursively(self, path: list | None = None, default: Any = NO_DEFAULT) -> tuple[Any, list]:
1068
+ if path is None:
1069
+ path = []
1070
+ path.append(self)
1071
+
1072
+ value = self._eval(False)
1073
+ self._post_eval()
1074
+
1075
+ if not self.is_leaf:
1076
+ raise TooManyChildren('Action node must not have any child node.')
1077
+
1078
+ return value, path
1079
+
1080
+
1081
+ class PlaceholderNode(ActionNode):
1082
+ def __init__(self, **kwargs):
1083
+ super().__init__(**kwargs)
1084
+
1085
+ self.autogen = True
1086
+ self.action = NoAction(auto_connect=False, autogen=True)
1087
+
1088
+ def _eval(self, enforce_dtype: bool) -> Any:
1089
+ if LGM.vigilant_mode:
1090
+ return self
1091
+ return self.action
1092
+
1093
+
1094
+ class NoAction(ActionNode):
1095
+ def __init__(self, sig: int = 0, repr='NoAction', autogen: bool = False, **kwargs):
1096
+ super().__init__(repr=repr, **kwargs)
1097
+ self.sig = sig
1098
+ self.autogen = autogen
1099
+
1100
+ def _eval(self, enforce_dtype: bool) -> Any:
1101
+ return self
1102
+
1103
+ def __repr__(self) -> str:
1104
+ return f'<{self.__class__.__name__}>(sig={self.sig})'
1105
+
1106
+ def __int__(self):
1107
+ return self.sig
1108
+
1109
+
1110
+ class LongAction(ActionNode):
1111
+ def __init__(self, *, sig: int = 1, repr='LongAction', **kwargs):
1112
+ super().__init__(repr=repr, **kwargs)
1113
+ self.sig = sig
1114
+
1115
+ def _eval(self, enforce_dtype: bool) -> Any:
1116
+ return self
1117
+
1118
+ def __repr__(self) -> str:
1119
+ return f'<{self.__class__.__name__}>(sig={self.sig})'
1120
+
1121
+ def __int__(self):
1122
+ return self.sig
1123
+
1124
+
1125
+ class ShortAction(ActionNode):
1126
+ def __init__(self, *, sig: int = -1, repr='ShortAction', **kwargs):
1127
+ super().__init__(repr=repr, **kwargs)
1128
+ self.sig = sig
1129
+
1130
+ def _eval(self, enforce_dtype: bool) -> Any:
1131
+ return self
1132
+
1133
+ def __repr__(self) -> str:
1134
+ return f'<{self.__class__.__name__}>(sig={self.sig})'
1135
+
1136
+ def __int__(self):
1137
+ return self.sig
1138
+
1139
+
1140
+ class CancelAction(ActionNode):
1141
+ def __init__(self, sig: int = 0, repr='CancelAction', **kwargs):
1142
+ super().__init__(repr=repr, **kwargs)
1143
+ self.sig = sig
1144
+
1145
+ def _eval(self, enforce_dtype: bool) -> Any:
1146
+ return self
1147
+
1148
+ def __repr__(self) -> str:
1149
+ return f'<{self.__class__.__name__}>(sig={self.sig})'
1150
+
1151
+ def __int__(self):
1152
+ return self.sig
1153
+
1154
+
1155
+ class ClearAction(ActionNode):
1156
+ def __init__(self, sig: int = 0, repr='ClearAction', **kwargs):
1157
+ super().__init__(repr=repr, **kwargs)
1158
+ self.sig = sig
1159
+
1160
+ def _eval(self, enforce_dtype: bool) -> Any:
1161
+ return self
1162
+
1163
+ def __repr__(self) -> str:
1164
+ return f'<{self.__class__.__name__}>(sig={self.sig})'
1165
+
1166
+ def __int__(self):
1167
+ return self.sig