PyDecisionGraph 0.3.2.post1__cp315-cp315-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.
- decision_graph/__infra__.pxd +1 -0
- decision_graph/__init__.pxd +1 -0
- decision_graph/__init__.py +39 -0
- decision_graph/decision_tree/__init__.pxd +2 -0
- decision_graph/decision_tree/__init__.py +85 -0
- decision_graph/decision_tree/bake/__infra__.pxd +502 -0
- decision_graph/decision_tree/bake/__init__.pxd +502 -0
- decision_graph/decision_tree/bake/__init__.py +29 -0
- decision_graph/decision_tree/bake/c_action.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_action.h +324 -0
- decision_graph/decision_tree/bake/c_action.pxd +54 -0
- decision_graph/decision_tree/bake/c_action.pyi +126 -0
- decision_graph/decision_tree/bake/c_action.pyx +65 -0
- decision_graph/decision_tree/bake/c_allocator_protocol.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_allocator_protocol.pxd +24 -0
- decision_graph/decision_tree/bake/c_allocator_protocol.pyi +61 -0
- decision_graph/decision_tree/bake/c_allocator_protocol.pyx +109 -0
- decision_graph/decision_tree/bake/c_bake.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_bake.h +629 -0
- decision_graph/decision_tree/bake/c_bake.pxd +52 -0
- decision_graph/decision_tree/bake/c_bake.pyi +151 -0
- decision_graph/decision_tree/bake/c_bake.pyx +142 -0
- decision_graph/decision_tree/bake/c_collections.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_collections.h +692 -0
- decision_graph/decision_tree/bake/c_collections.pxd +52 -0
- decision_graph/decision_tree/bake/c_collections.pyi +193 -0
- decision_graph/decision_tree/bake/c_collections.pyx +161 -0
- decision_graph/decision_tree/bake/c_const.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_const.h +395 -0
- decision_graph/decision_tree/bake/c_const.pxd +56 -0
- decision_graph/decision_tree/bake/c_const.pyi +305 -0
- decision_graph/decision_tree/bake/c_const.pyx +278 -0
- decision_graph/decision_tree/bake/c_edge.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_edge.h +511 -0
- decision_graph/decision_tree/bake/c_edge.pxd +117 -0
- decision_graph/decision_tree/bake/c_edge.pyi +192 -0
- decision_graph/decision_tree/bake/c_edge.pyx +270 -0
- decision_graph/decision_tree/bake/c_eval.h +848 -0
- decision_graph/decision_tree/bake/c_eval.pxd +40 -0
- decision_graph/decision_tree/bake/c_expr.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_expr.h +1900 -0
- decision_graph/decision_tree/bake/c_expr.pxd +129 -0
- decision_graph/decision_tree/bake/c_expr.pyi +280 -0
- decision_graph/decision_tree/bake/c_expr.pyx +272 -0
- decision_graph/decision_tree/bake/c_hierarchy.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
- decision_graph/decision_tree/bake/c_hierarchy.pxd +71 -0
- decision_graph/decision_tree/bake/c_hierarchy.pyi +409 -0
- decision_graph/decision_tree/bake/c_hierarchy.pyx +277 -0
- decision_graph/decision_tree/bake/c_logic_group.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
- decision_graph/decision_tree/bake/c_logic_group.pxd +162 -0
- decision_graph/decision_tree/bake/c_logic_group.pyi +264 -0
- decision_graph/decision_tree/bake/c_logic_group.pyx +308 -0
- decision_graph/decision_tree/bake/c_node.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_node.h +2660 -0
- decision_graph/decision_tree/bake/c_node.pxd +381 -0
- decision_graph/decision_tree/bake/c_node.pyi +438 -0
- decision_graph/decision_tree/bake/c_node.pyx +639 -0
- decision_graph/decision_tree/bake/c_reconstruct.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_reconstruct.pxd +63 -0
- decision_graph/decision_tree/bake/c_reconstruct.pyi +60 -0
- decision_graph/decision_tree/bake/c_reconstruct.pyx +252 -0
- decision_graph/decision_tree/bake/c_var.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/bake/c_var.h +2054 -0
- decision_graph/decision_tree/bake/c_var.pxd +205 -0
- decision_graph/decision_tree/bake/c_var.pyi +271 -0
- decision_graph/decision_tree/bake/c_var.pyx +228 -0
- decision_graph/decision_tree/capi/__infra__.pxd +33 -0
- decision_graph/decision_tree/capi/__init__.pxd +33 -0
- decision_graph/decision_tree/capi/__init__.py +57 -0
- decision_graph/decision_tree/capi/c_abc.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/capi/c_abc.pxd +246 -0
- decision_graph/decision_tree/capi/c_abc.pyi +770 -0
- decision_graph/decision_tree/capi/c_abc.pyx +1532 -0
- decision_graph/decision_tree/capi/c_collection.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/capi/c_collection.pxd +22 -0
- decision_graph/decision_tree/capi/c_collection.pyi +245 -0
- decision_graph/decision_tree/capi/c_collection.pyx +138 -0
- decision_graph/decision_tree/capi/c_node.cp315-win_amd64.pyd +0 -0
- decision_graph/decision_tree/capi/c_node.pxd +95 -0
- decision_graph/decision_tree/capi/c_node.pyi +492 -0
- decision_graph/decision_tree/capi/c_node.pyx +551 -0
- decision_graph/decision_tree/exc.py +189 -0
- decision_graph/decision_tree/native/__init__.py +57 -0
- decision_graph/decision_tree/native/abc.py +1167 -0
- decision_graph/decision_tree/native/collection.py +134 -0
- decision_graph/decision_tree/native/node.py +521 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_action.c +16320 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_action.h +324 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_allocator_protocol.c +12473 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_bake.c +16485 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_bake.h +629 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_collections.c +17381 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_collections.h +692 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_const.c +20815 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_const.h +395 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_edge.c +20696 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_edge.h +511 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_eval.h +848 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_expr.c +22065 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_expr.h +1900 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.c +23267 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.c +22434 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_node.c +28385 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_node.h +2660 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_reconstruct.c +15955 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_var.c +16881 -0
- decision_graph/includes/decision_graph/decision_tree/bake/c_var.h +2054 -0
- decision_graph/includes/decision_graph/decision_tree/capi/c_abc.c +59196 -0
- decision_graph/includes/decision_graph/decision_tree/capi/c_collection.c +19967 -0
- decision_graph/includes/decision_graph/decision_tree/capi/c_node.c +39229 -0
- decision_graph/logic_group/__init__.py +25 -0
- decision_graph/logic_group/base.py +100 -0
- decision_graph/logic_group/pending_request.py +384 -0
- decision_graph/webui/__init__.py +24 -0
- decision_graph/webui/bake/__init__.py +65 -0
- decision_graph/webui/bake/app.py +488 -0
- decision_graph/webui/bake/static/d3.v7.min.js +2 -0
- decision_graph/webui/bake/static/script.js +1440 -0
- decision_graph/webui/bake/static/style.css +920 -0
- decision_graph/webui/bake/templates/index.html +183 -0
- decision_graph/webui/bake/templates/offline.html +184 -0
- decision_graph/webui/capi/__init__.py +54 -0
- decision_graph/webui/capi/app.py +366 -0
- decision_graph/webui/capi/main.py +53 -0
- decision_graph/webui/capi/static/d3.v7.min.js +2 -0
- decision_graph/webui/capi/static/script.js +1026 -0
- decision_graph/webui/capi/static/style.css +656 -0
- decision_graph/webui/capi/templates/index.html +93 -0
- decision_graph/webui/capi/templates/offline.html +94 -0
- decision_graph/webui/native/__init__.py +54 -0
- decision_graph/webui/native/app.py +366 -0
- decision_graph/webui/native/main.py +53 -0
- decision_graph/webui/native/static/d3.v7.min.js +2 -0
- decision_graph/webui/native/static/script.js +1026 -0
- decision_graph/webui/native/static/style.css +656 -0
- decision_graph/webui/native/templates/index.html +93 -0
- decision_graph/webui/native/templates/offline.html +94 -0
- pydecisiongraph-0.3.2.post1.dist-info/METADATA +136 -0
- pydecisiongraph-0.3.2.post1.dist-info/RECORD +148 -0
- pydecisiongraph-0.3.2.post1.dist-info/WHEEL +5 -0
- pydecisiongraph-0.3.2.post1.dist-info/licenses/LICENSE +373 -0
- pydecisiongraph-0.3.2.post1.dist-info/top_level.txt +2 -0
- tests/bake_c_evaluator.cp315-win_amd64.pyd +0 -0
- tests/bake_eval_probe.cp315-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
|