ripple-down-rules 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ripple_down_rules/datasets.py +5 -0
- ripple_down_rules/datastructures/enums.py +14 -0
- ripple_down_rules/experts.py +14 -7
- ripple_down_rules/rdr.py +6 -2
- ripple_down_rules/user_interface/__init__.py +0 -0
- ripple_down_rules/user_interface/gui.py +630 -0
- ripple_down_rules/user_interface/ipython_custom_shell.py +146 -0
- ripple_down_rules/user_interface/object_diagram.py +109 -0
- ripple_down_rules/user_interface/prompt.py +159 -0
- ripple_down_rules/user_interface/template_file_creator.py +293 -0
- ripple_down_rules/utils.py +2 -2
- {ripple_down_rules-0.3.0.dist-info → ripple_down_rules-0.4.0.dist-info}/METADATA +8 -1
- ripple_down_rules-0.4.0.dist-info/RECORD +25 -0
- ripple_down_rules/prompt.py +0 -510
- ripple_down_rules-0.3.0.dist-info/RECORD +0 -20
- {ripple_down_rules-0.3.0.dist-info → ripple_down_rules-0.4.0.dist-info}/WHEEL +0 -0
- {ripple_down_rules-0.3.0.dist-info → ripple_down_rules-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {ripple_down_rules-0.3.0.dist-info → ripple_down_rules-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,630 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import inspect
|
4
|
+
from copy import copy
|
5
|
+
from types import MethodType
|
6
|
+
|
7
|
+
from PyQt6.QtCore import Qt
|
8
|
+
from PyQt6.QtGui import QPixmap, QPainter, QPalette
|
9
|
+
from PyQt6.QtWidgets import (
|
10
|
+
QWidget, QVBoxLayout, QLabel, QScrollArea,
|
11
|
+
QSizePolicy, QToolButton, QHBoxLayout, QPushButton, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
|
12
|
+
)
|
13
|
+
from qtconsole.inprocess import QtInProcessKernelManager
|
14
|
+
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
15
|
+
from typing_extensions import Optional, Any, List, Dict
|
16
|
+
|
17
|
+
from ..datastructures.dataclasses import CaseQuery
|
18
|
+
from ..datastructures.enums import PromptFor
|
19
|
+
from .template_file_creator import TemplateFileCreator
|
20
|
+
from ..utils import is_iterable, contains_return_statement, encapsulate_user_input
|
21
|
+
from .object_diagram import generate_object_graph
|
22
|
+
|
23
|
+
|
24
|
+
class ImageViewer(QGraphicsView):
|
25
|
+
def __init__(self):
|
26
|
+
super().__init__()
|
27
|
+
self.setScene(QGraphicsScene(self))
|
28
|
+
self.setDragMode(QGraphicsView.ScrollHandDrag)
|
29
|
+
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
|
30
|
+
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
|
31
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
32
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
33
|
+
|
34
|
+
self.pixmap_item = None
|
35
|
+
|
36
|
+
self._zoom = 0
|
37
|
+
|
38
|
+
def update_image(self, image_path: str):
|
39
|
+
if self.pixmap_item is None:
|
40
|
+
self.pixmap_item = QGraphicsPixmapItem()
|
41
|
+
self.scene().addItem(self.pixmap_item)
|
42
|
+
pixmap = QPixmap(image_path)
|
43
|
+
self.pixmap_item.setPixmap(pixmap)
|
44
|
+
self.setSceneRect(self.pixmap_item.boundingRect())
|
45
|
+
|
46
|
+
def wheelEvent(self, event):
|
47
|
+
# Zoom in or out with Ctrl + mouse wheel
|
48
|
+
if event.modifiers() == Qt.ControlModifier:
|
49
|
+
angle = event.angleDelta().y()
|
50
|
+
factor = 1.2 if angle > 0 else 0.8
|
51
|
+
|
52
|
+
self._zoom += 1 if angle > 0 else -1
|
53
|
+
if self._zoom > 10: # max zoom in limit
|
54
|
+
self._zoom = 10
|
55
|
+
return
|
56
|
+
if self._zoom < -10: # max zoom out limit
|
57
|
+
self._zoom = -10
|
58
|
+
return
|
59
|
+
|
60
|
+
self.scale(factor, factor)
|
61
|
+
else:
|
62
|
+
super().wheelEvent(event)
|
63
|
+
|
64
|
+
|
65
|
+
class BackgroundWidget(QWidget):
|
66
|
+
def __init__(self, image_path, parent=None):
|
67
|
+
super().__init__(parent)
|
68
|
+
self.pixmap = QPixmap(image_path)
|
69
|
+
|
70
|
+
# Layout for buttons
|
71
|
+
self.layout = QVBoxLayout(self)
|
72
|
+
self.layout.setContentsMargins(20, 20, 20, 20)
|
73
|
+
self.layout.setSpacing(10)
|
74
|
+
|
75
|
+
accept_btn = QPushButton("Accept")
|
76
|
+
accept_btn.setStyleSheet("background-color: #4CAF50; color: white;") # Green button
|
77
|
+
edit_btn = QPushButton("Edit")
|
78
|
+
edit_btn.setStyleSheet("background-color: #2196F3; color: white;") # Blue button
|
79
|
+
|
80
|
+
self.layout.addWidget(accept_btn)
|
81
|
+
self.layout.addWidget(edit_btn)
|
82
|
+
self.layout.addStretch() # Push buttons to top
|
83
|
+
|
84
|
+
def paintEvent(self, event):
|
85
|
+
painter = QPainter(self)
|
86
|
+
if not self.pixmap.isNull():
|
87
|
+
# Calculate the vertical space used by buttons
|
88
|
+
button_area_height = 0
|
89
|
+
for i in range(self.layout.count()):
|
90
|
+
item = self.layout.itemAt(i)
|
91
|
+
if item.widget():
|
92
|
+
button_area_height += item.widget().height() + self.layout.spacing()
|
93
|
+
|
94
|
+
remaining_height = self.height() - button_area_height
|
95
|
+
if remaining_height <= 0:
|
96
|
+
return # No space to draw
|
97
|
+
|
98
|
+
# Scale image to the remaining area (width, height)
|
99
|
+
scaled = self.pixmap.scaled(
|
100
|
+
self.width(),
|
101
|
+
remaining_height,
|
102
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
103
|
+
Qt.TransformationMode.SmoothTransformation
|
104
|
+
)
|
105
|
+
|
106
|
+
x = (self.width() - scaled.width()) // 2
|
107
|
+
|
108
|
+
# Draw the image starting just below the buttons
|
109
|
+
painter.drawPixmap(x, button_area_height + 20, scaled)
|
110
|
+
|
111
|
+
def resizeEvent(self, event):
|
112
|
+
self.update() # Force repaint on resize
|
113
|
+
super().resizeEvent(event)
|
114
|
+
|
115
|
+
|
116
|
+
class CollapsibleBox(QWidget):
|
117
|
+
def __init__(self, title="", parent=None, viewer: Optional[RDRCaseViewer]=None, chain_name: Optional[str] = None,
|
118
|
+
main_obj: Optional[Dict[str, Any]] = None):
|
119
|
+
super().__init__(parent)
|
120
|
+
|
121
|
+
self.viewer = viewer
|
122
|
+
self.chain_name = chain_name
|
123
|
+
self.main_obj = main_obj
|
124
|
+
self.toggle_button = QToolButton(checkable=True, checked=False)
|
125
|
+
self.toggle_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
126
|
+
self.toggle_button.setArrowType(Qt.ArrowType.RightArrow)
|
127
|
+
self.toggle_button.clicked.connect(self.toggle)
|
128
|
+
self.toggle_button.setStyleSheet("""
|
129
|
+
QToolButton {
|
130
|
+
border: none;
|
131
|
+
font-weight: bold;
|
132
|
+
color: #FFA07A; /* Light orange */
|
133
|
+
}
|
134
|
+
""")
|
135
|
+
self.title_label = QLabel(title)
|
136
|
+
self.title_label.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering
|
137
|
+
self.title_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
138
|
+
self.title_label.setStyleSheet("QLabel { padding: 1px; color: #FFA07A; }")
|
139
|
+
|
140
|
+
self.content_area = QWidget()
|
141
|
+
self.content_area.setVisible(False)
|
142
|
+
self.content_layout = QVBoxLayout(self.content_area)
|
143
|
+
self.content_layout.setContentsMargins(15, 2, 0, 2)
|
144
|
+
self.content_layout.setSpacing(2)
|
145
|
+
|
146
|
+
layout = QVBoxLayout(self)
|
147
|
+
header_layout = QHBoxLayout()
|
148
|
+
header_layout.addWidget(self.toggle_button)
|
149
|
+
header_layout.addWidget(self.title_label)
|
150
|
+
layout.addLayout(header_layout)
|
151
|
+
layout.addWidget(self.content_area)
|
152
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
153
|
+
layout.setSpacing(2)
|
154
|
+
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
155
|
+
|
156
|
+
def toggle(self):
|
157
|
+
is_expanded = self.toggle_button.isChecked()
|
158
|
+
self.toggle_button.setArrowType(
|
159
|
+
Qt.ArrowType.DownArrow if is_expanded else Qt.ArrowType.RightArrow
|
160
|
+
)
|
161
|
+
self.content_area.setVisible(is_expanded)
|
162
|
+
if is_expanded and self.viewer is not None:
|
163
|
+
self.viewer.included_attrs.append(self.chain_name)
|
164
|
+
main_obj_name = self.chain_name.split('.')[0]
|
165
|
+
main_obj = self.main_obj.get(main_obj_name)
|
166
|
+
self.viewer.update_object_diagram(
|
167
|
+
main_obj, main_obj_name
|
168
|
+
)
|
169
|
+
elif self.viewer is not None and self.chain_name in self.viewer.included_attrs:
|
170
|
+
for name in self.viewer.included_attrs:
|
171
|
+
if name.startswith(self.chain_name):
|
172
|
+
self.viewer.included_attrs.remove(name)
|
173
|
+
main_obj_name = self.chain_name.split('.')[0]
|
174
|
+
main_obj = self.main_obj.get(main_obj_name)
|
175
|
+
self.viewer.update_object_diagram(
|
176
|
+
main_obj, main_obj_name
|
177
|
+
)
|
178
|
+
# toggle children
|
179
|
+
if not is_expanded:
|
180
|
+
for i in range(self.content_layout.count()):
|
181
|
+
item = self.content_layout.itemAt(i)
|
182
|
+
if isinstance(item.widget(), CollapsibleBox):
|
183
|
+
item.widget().toggle_button.setChecked(False)
|
184
|
+
item.widget().toggle()
|
185
|
+
|
186
|
+
def add_widget(self, widget):
|
187
|
+
self.content_layout.addWidget(widget)
|
188
|
+
|
189
|
+
def adjust_size_recursive(self):
|
190
|
+
# Trigger resize
|
191
|
+
self.adjustSize()
|
192
|
+
|
193
|
+
# Traverse upwards to main window and call adjustSize on it too
|
194
|
+
parent = self.parent()
|
195
|
+
while parent:
|
196
|
+
if isinstance(parent, QWidget):
|
197
|
+
parent.layout().activate() # Force layout refresh
|
198
|
+
parent.adjustSize()
|
199
|
+
elif isinstance(parent, QScrollArea):
|
200
|
+
parent.widget().adjustSize()
|
201
|
+
parent.viewport().update()
|
202
|
+
if isinstance(parent, BackgroundWidget):
|
203
|
+
parent.update()
|
204
|
+
parent.updateGeometry()
|
205
|
+
parent.repaint()
|
206
|
+
if parent.parent() is None:
|
207
|
+
top_window = parent.window() # The main top-level window
|
208
|
+
top_window.updateGeometry()
|
209
|
+
top_window.repaint()
|
210
|
+
parent = parent.parent()
|
211
|
+
|
212
|
+
|
213
|
+
def python_colored_repr(value):
|
214
|
+
if isinstance(value, str):
|
215
|
+
return f'<span style="color:#90EE90;">"{value}"</span>'
|
216
|
+
elif isinstance(value, (int, float)):
|
217
|
+
return f'<span style="color:#ADD8E6;">{value}</span>'
|
218
|
+
elif isinstance(value, bool) or value is None:
|
219
|
+
return f'<span style="color:darkorange;">{value}</span>'
|
220
|
+
elif isinstance(value, type):
|
221
|
+
return f'<span style="color:#C1BCBB;">{{{value.__name__}}}</span>'
|
222
|
+
elif callable(value):
|
223
|
+
return ''
|
224
|
+
else:
|
225
|
+
try:
|
226
|
+
return f'<span style="color:white;">{repr(value)}</span>'
|
227
|
+
except Exception as e:
|
228
|
+
return f'<span style="color:red;"><error: {e}></span>'
|
229
|
+
|
230
|
+
|
231
|
+
def style(text, color=None, font_size=None, font_weight=None):
|
232
|
+
s = '<span style="'
|
233
|
+
if color:
|
234
|
+
s += f'color:{color_name_to_html(color)};'
|
235
|
+
if font_size:
|
236
|
+
s += f'font-size:{font_size}px;'
|
237
|
+
if font_weight:
|
238
|
+
s += f'font-weight:{font_weight};'
|
239
|
+
s += '">'
|
240
|
+
s += text
|
241
|
+
s += '</span>'
|
242
|
+
return s
|
243
|
+
|
244
|
+
|
245
|
+
def color_name_to_html(color_name):
|
246
|
+
single_char_to_name = {
|
247
|
+
'r': 'red',
|
248
|
+
'g': 'green',
|
249
|
+
'b': 'blue',
|
250
|
+
'o': 'orange',
|
251
|
+
}
|
252
|
+
color_map = {
|
253
|
+
'red': '#d6336c',
|
254
|
+
'green': '#2eb872',
|
255
|
+
'blue': '#007acc',
|
256
|
+
'orange': '#FFA07A',
|
257
|
+
}
|
258
|
+
if len(color_name) == 1:
|
259
|
+
color_name = single_char_to_name.get(color_name, color_name)
|
260
|
+
return color_map.get(color_name.lower(), color_name) # Default to the name itself if not found
|
261
|
+
|
262
|
+
|
263
|
+
class RDRCaseViewer(QMainWindow):
|
264
|
+
case_query: Optional[CaseQuery] = None
|
265
|
+
prompt_for: Optional[PromptFor] = None
|
266
|
+
code_to_modify: Optional[str] = None
|
267
|
+
template_file_creator: Optional[TemplateFileCreator] = None
|
268
|
+
code_lines: Optional[List[str]] = None
|
269
|
+
included_attrs: Optional[List[str]] = None
|
270
|
+
main_obj: Optional[Dict[str, Any]] = None
|
271
|
+
user_input: Optional[str] = None
|
272
|
+
attributes_widget: Optional[QWidget] = None
|
273
|
+
|
274
|
+
def __init__(self, parent=None):
|
275
|
+
super().__init__(parent)
|
276
|
+
self.setWindowTitle("RDR Case Viewer")
|
277
|
+
|
278
|
+
self.setBaseSize(1600, 600) # or your preferred initial size
|
279
|
+
|
280
|
+
main_widget = QWidget()
|
281
|
+
self.setCentralWidget(main_widget)
|
282
|
+
self.setStyleSheet("background-color: #333333;")
|
283
|
+
main_widget.setStyleSheet("background-color: #333333;")
|
284
|
+
|
285
|
+
main_layout = QHBoxLayout(main_widget) # Horizontal layout to split window
|
286
|
+
|
287
|
+
# === Left: Attributes ===
|
288
|
+
self._create_attribute_widget()
|
289
|
+
|
290
|
+
# === Middle: Ipython & Action buttons ===
|
291
|
+
middle_widget = QWidget()
|
292
|
+
self.middle_widget_layout = QVBoxLayout(middle_widget)
|
293
|
+
|
294
|
+
self.title_label = self.create_label_widget(style(style(f"Welcome to {style('RDRViewer', 'b')} "
|
295
|
+
f"{style('App', 'g')}")
|
296
|
+
, 'o', 28, 'bold'))
|
297
|
+
|
298
|
+
self.buttons_widget = self.create_buttons_widget()
|
299
|
+
|
300
|
+
self.ipython_console = IPythonConsole(parent=self)
|
301
|
+
|
302
|
+
self.middle_widget_layout.addWidget(self.title_label)
|
303
|
+
self.middle_widget_layout.addWidget(self.ipython_console)
|
304
|
+
self.middle_widget_layout.addWidget(self.buttons_widget)
|
305
|
+
|
306
|
+
# === Right: Object Diagram ===
|
307
|
+
self.obj_diagram_viewer = ImageViewer() # put your image path here
|
308
|
+
|
309
|
+
# Add both to main layout
|
310
|
+
main_layout.addWidget(self.attributes_widget, stretch=1)
|
311
|
+
main_layout.addWidget(middle_widget, stretch=2)
|
312
|
+
main_layout.addWidget(self.obj_diagram_viewer, stretch=2)
|
313
|
+
|
314
|
+
def print(self, msg):
|
315
|
+
"""
|
316
|
+
Print a message to the console.
|
317
|
+
"""
|
318
|
+
self.ipython_console._append_plain_text(msg + '\n', True)
|
319
|
+
|
320
|
+
def update_for_case_query(self, case_query: CaseQuery, title_txt: Optional[str] = None,
|
321
|
+
prompt_for: Optional[PromptFor] = None, code_to_modify: Optional[str] = None):
|
322
|
+
self.case_query = case_query
|
323
|
+
self.prompt_for = prompt_for
|
324
|
+
self.code_to_modify = code_to_modify
|
325
|
+
title_text = title_txt or ""
|
326
|
+
case_attr_type = ', '.join([t.__name__ for t in case_query.core_attribute_type])
|
327
|
+
case_attr_type = style(f"{case_attr_type}", 'g', 28, 'bold')
|
328
|
+
case_name = style(f"{case_query.name}", 'b', 28, 'bold')
|
329
|
+
title_text = style(f"{title_text} {case_name} of type {case_attr_type}", 'o', 28, 'bold')
|
330
|
+
self.update_for_object(case_query.case, case_query.case_name, case_query.scope, title_text)
|
331
|
+
|
332
|
+
def update_for_object(self, obj: Any, name: str, scope: Optional[dict] = None,
|
333
|
+
title_text: Optional[str] = None):
|
334
|
+
self.update_main_obj(obj, name)
|
335
|
+
title_text = title_text or style(f"{name}", 'o', 28, 'bold')
|
336
|
+
scope = scope or {}
|
337
|
+
scope.update({name: obj})
|
338
|
+
self.update_object_diagram(obj, name)
|
339
|
+
self.update_attribute_layout(obj, name)
|
340
|
+
self.title_label.setText(title_text)
|
341
|
+
self.ipython_console.update_namespace(scope)
|
342
|
+
|
343
|
+
def update_main_obj(self, obj, name):
|
344
|
+
self.main_obj = {name: obj}
|
345
|
+
self.included_attrs = []
|
346
|
+
self.user_input = None
|
347
|
+
|
348
|
+
def update_object_diagram(self, obj: Any, name: str):
|
349
|
+
self.included_attrs = self.included_attrs or []
|
350
|
+
graph = generate_object_graph(obj, name, included_attrs=self.included_attrs)
|
351
|
+
graph.render("object_diagram", view=False, format='svg')
|
352
|
+
self.obj_diagram_viewer.update_image("object_diagram.svg")
|
353
|
+
|
354
|
+
def _create_attribute_widget(self):
|
355
|
+
main_widget = QWidget()
|
356
|
+
main_layout = QVBoxLayout(main_widget)
|
357
|
+
|
358
|
+
buttons_widget = QWidget()
|
359
|
+
buttons_layout = QHBoxLayout(buttons_widget)
|
360
|
+
|
361
|
+
expand_btn = QPushButton("Expand All")
|
362
|
+
expand_btn.clicked.connect(self.expand_all)
|
363
|
+
expand_btn.setStyleSheet(f"background-color: white; color: black;") # Green button
|
364
|
+
buttons_layout.addWidget(expand_btn)
|
365
|
+
|
366
|
+
collapse_btn = QPushButton("Collapse All")
|
367
|
+
collapse_btn.clicked.connect(self.collapse_all)
|
368
|
+
collapse_btn.setStyleSheet(f"background-color: white; color: black;") # Green button
|
369
|
+
buttons_layout.addWidget(collapse_btn)
|
370
|
+
|
371
|
+
main_layout.addWidget(buttons_widget)
|
372
|
+
|
373
|
+
scroll = QScrollArea()
|
374
|
+
scroll.setWidgetResizable(True)
|
375
|
+
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
376
|
+
|
377
|
+
attr_widget = QWidget()
|
378
|
+
self.attr_widget_layout = QVBoxLayout(attr_widget)
|
379
|
+
self.attr_widget_layout.setSpacing(2)
|
380
|
+
self.attr_widget_layout.setContentsMargins(6, 6, 6, 6)
|
381
|
+
attr_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
382
|
+
|
383
|
+
scroll.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
384
|
+
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
385
|
+
scroll.setWidget(attr_widget)
|
386
|
+
main_layout.addWidget(scroll)
|
387
|
+
self.attributes_widget = main_widget
|
388
|
+
|
389
|
+
def expand_all(self):
|
390
|
+
# Expand all collapsible boxes
|
391
|
+
for i in range(self.attr_widget_layout.count()):
|
392
|
+
item = self.attr_widget_layout.itemAt(i)
|
393
|
+
if isinstance(item.widget(), CollapsibleBox):
|
394
|
+
self.expand_collapse_all(item.widget(), expand=True)
|
395
|
+
|
396
|
+
def collapse_all(self):
|
397
|
+
# Collapse all collapsible boxes
|
398
|
+
for i in range(self.attr_widget_layout.count()):
|
399
|
+
item = self.attr_widget_layout.itemAt(i)
|
400
|
+
if isinstance(item.widget(), CollapsibleBox):
|
401
|
+
self.expand_collapse_all(item.widget(), expand=False)
|
402
|
+
|
403
|
+
def expand_collapse_all(self, widget, expand=True):
|
404
|
+
widget.toggle_button.setChecked(expand)
|
405
|
+
widget.toggle()
|
406
|
+
if expand:
|
407
|
+
# do it for recursive children
|
408
|
+
for i in range(widget.content_layout.count()):
|
409
|
+
item = widget.content_layout.itemAt(i)
|
410
|
+
if isinstance(item.widget(), CollapsibleBox):
|
411
|
+
self.expand_collapse_all(item.widget(), expand=True)
|
412
|
+
|
413
|
+
|
414
|
+
|
415
|
+
def create_buttons_widget(self):
|
416
|
+
button_widget = QWidget()
|
417
|
+
button_widget_layout = QHBoxLayout(button_widget)
|
418
|
+
|
419
|
+
accept_btn = QPushButton("Accept")
|
420
|
+
accept_btn.clicked.connect(self._accept)
|
421
|
+
accept_btn.setStyleSheet(f"background-color: {color_name_to_html('g')}; color: white;") # Green button
|
422
|
+
|
423
|
+
edit_btn = QPushButton("Edit")
|
424
|
+
edit_btn.clicked.connect(self._edit)
|
425
|
+
edit_btn.setStyleSheet(f"background-color: {color_name_to_html('o')}; color: white;") # Orange button
|
426
|
+
|
427
|
+
load_btn = QPushButton("Load")
|
428
|
+
load_btn.clicked.connect(self._load)
|
429
|
+
load_btn.setStyleSheet(f"background-color: {color_name_to_html('b')}; color: white;") # Blue button
|
430
|
+
|
431
|
+
button_widget_layout.addWidget(accept_btn)
|
432
|
+
button_widget_layout.addWidget(edit_btn)
|
433
|
+
button_widget_layout.addWidget(load_btn)
|
434
|
+
return button_widget
|
435
|
+
|
436
|
+
def _accept(self):
|
437
|
+
# close the window
|
438
|
+
self.close()
|
439
|
+
|
440
|
+
def _edit(self):
|
441
|
+
self.template_file_creator = TemplateFileCreator(self.ipython_console.kernel.shell,
|
442
|
+
self.case_query, self.prompt_for, self.code_to_modify,
|
443
|
+
self.print)
|
444
|
+
self.template_file_creator.edit()
|
445
|
+
|
446
|
+
def _load(self):
|
447
|
+
if not self.template_file_creator:
|
448
|
+
return
|
449
|
+
self.code_lines = self.template_file_creator.load()
|
450
|
+
if self.code_lines is not None:
|
451
|
+
self.user_input = encapsulate_code_lines_into_a_function(
|
452
|
+
self.code_lines, self.template_file_creator.func_name,
|
453
|
+
self.template_file_creator.function_signature,
|
454
|
+
self.template_file_creator.func_doc, self.case_query)
|
455
|
+
self.template_file_creator = None
|
456
|
+
|
457
|
+
def update_attribute_layout(self, obj, name: str):
|
458
|
+
# Clear the existing layout
|
459
|
+
clear_layout(self.attr_widget_layout)
|
460
|
+
|
461
|
+
# Re-add the collapsible box with the new object
|
462
|
+
self.add_collapsible(name, obj, self.attr_widget_layout, 0, 3, chain_name=name)
|
463
|
+
self.attr_widget_layout.addStretch()
|
464
|
+
|
465
|
+
def create_label_widget(self, text):
|
466
|
+
# Create a QLabel with rich text
|
467
|
+
label = QLabel()
|
468
|
+
label.setText(text)
|
469
|
+
label.setAlignment(Qt.AlignCenter) # Optional: center the text
|
470
|
+
label.setWordWrap(True) # Optional: allow wrapping if needed
|
471
|
+
return label
|
472
|
+
|
473
|
+
def add_attributes(self, obj, name, layout, current_depth=0, max_depth=3, chain_name=None):
|
474
|
+
if current_depth > max_depth:
|
475
|
+
return
|
476
|
+
if isinstance(obj, dict):
|
477
|
+
items = obj.items()
|
478
|
+
elif isinstance(obj, (list, tuple, set)):
|
479
|
+
items = enumerate(obj)
|
480
|
+
else:
|
481
|
+
methods = []
|
482
|
+
attributes = []
|
483
|
+
iterables = []
|
484
|
+
for attr in dir(obj):
|
485
|
+
if attr.startswith("_") or attr == "scope":
|
486
|
+
continue
|
487
|
+
try:
|
488
|
+
value = getattr(obj, attr)
|
489
|
+
if callable(value):
|
490
|
+
methods.append((attr, value))
|
491
|
+
continue
|
492
|
+
elif is_iterable(value):
|
493
|
+
iterables.append((attr, value))
|
494
|
+
continue
|
495
|
+
except Exception as e:
|
496
|
+
value = f"<error: {e}>"
|
497
|
+
attributes.append((attr, value))
|
498
|
+
items = attributes + iterables + methods
|
499
|
+
chain_name = chain_name if chain_name is not None else name
|
500
|
+
for attr, value in items:
|
501
|
+
attr = f"{attr}"
|
502
|
+
try:
|
503
|
+
if is_iterable(value) or hasattr(value, "__dict__") and not inspect.isfunction(value):
|
504
|
+
self.add_collapsible(attr, value, layout, current_depth + 1, max_depth, chain_name=f"{chain_name}.{attr}")
|
505
|
+
else:
|
506
|
+
self.add_non_collapsible(attr, value, layout)
|
507
|
+
except Exception as e:
|
508
|
+
err = QLabel(f"<b>{attr}</b>: <span style='color:red;'><error: {e}></span>")
|
509
|
+
err.setTextFormat(Qt.TextFormat.RichText)
|
510
|
+
layout.addWidget(err)
|
511
|
+
|
512
|
+
def add_collapsible(self, attr, value, layout, current_depth, max_depth, chain_name=None):
|
513
|
+
type_name = type(value) if not isinstance(value, type) else value
|
514
|
+
collapsible = CollapsibleBox(
|
515
|
+
f'<b><span style="color:#FFA07A;">{attr}</span></b> {python_colored_repr(type_name)}', viewer=self,
|
516
|
+
chain_name=chain_name, main_obj=self.main_obj)
|
517
|
+
self.add_attributes(value, attr, collapsible.content_layout, current_depth, max_depth, chain_name=chain_name)
|
518
|
+
layout.addWidget(collapsible)
|
519
|
+
|
520
|
+
def add_non_collapsible(self, attr, value, layout):
|
521
|
+
type_name = type(value) if not isinstance(value, type) else value
|
522
|
+
text = f'<b><span style="color:#FFA07A;">{attr}</span></b> {python_colored_repr(type_name)}: {python_colored_repr(value)}'
|
523
|
+
item_label = QLabel()
|
524
|
+
item_label.setTextFormat(Qt.TextFormat.RichText)
|
525
|
+
item_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
526
|
+
item_label.setStyleSheet("QLabel { padding: 1px; color: #FFA07A; }")
|
527
|
+
item_label.setText(text)
|
528
|
+
item_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
529
|
+
layout.addWidget(item_label)
|
530
|
+
|
531
|
+
|
532
|
+
def encapsulate_code_lines_into_a_function(code_lines: List[str], function_name: str, function_signature: str,
|
533
|
+
func_doc: str, case_query: CaseQuery) -> str:
|
534
|
+
"""
|
535
|
+
Encapsulate the given code lines into a function with the specified name, signature, and docstring.
|
536
|
+
|
537
|
+
:param code_lines: The lines of code to include in the user input.
|
538
|
+
:param function_name: The name of the function to include in the user input.
|
539
|
+
:param function_signature: The function signature to include in the user input.
|
540
|
+
:param func_doc: The function docstring to include in the user input.
|
541
|
+
:param case_query: The case query object.
|
542
|
+
"""
|
543
|
+
code = '\n'.join(code_lines)
|
544
|
+
code = encapsulate_user_input(code, function_signature, func_doc)
|
545
|
+
if case_query.is_function:
|
546
|
+
args = "**case"
|
547
|
+
else:
|
548
|
+
args = "case"
|
549
|
+
if f"return {function_name}({args})" not in code:
|
550
|
+
code = code.strip() + f"\nreturn {function_name}({args})"
|
551
|
+
return code
|
552
|
+
|
553
|
+
|
554
|
+
class IPythonConsole(RichJupyterWidget):
|
555
|
+
def __init__(self, namespace=None, parent=None):
|
556
|
+
super(IPythonConsole, self).__init__(parent)
|
557
|
+
|
558
|
+
self.kernel_manager = QtInProcessKernelManager()
|
559
|
+
self.kernel_manager.start_kernel()
|
560
|
+
self.kernel = self.kernel_manager.kernel
|
561
|
+
self.kernel.gui = 'qt'
|
562
|
+
self.command_log = []
|
563
|
+
|
564
|
+
# Monkey patch its run_cell method
|
565
|
+
def custom_run_cell(this, raw_cell, **kwargs):
|
566
|
+
print(raw_cell)
|
567
|
+
if contains_return_statement(raw_cell) and 'def ' not in raw_cell:
|
568
|
+
if self.parent.template_file_creator and self.parent.template_file_creator.func_name in raw_cell:
|
569
|
+
self.command_log = self.parent.code_lines
|
570
|
+
self.command_log.append(raw_cell)
|
571
|
+
this.history_manager.store_inputs(line_num=this.execution_count, source=raw_cell)
|
572
|
+
return None
|
573
|
+
result = original_run_cell(raw_cell, **kwargs)
|
574
|
+
if result.error_in_exec is None and result.error_before_exec is None:
|
575
|
+
self.command_log.append(raw_cell)
|
576
|
+
return result
|
577
|
+
|
578
|
+
original_run_cell = self.kernel.shell.run_cell
|
579
|
+
self.kernel.shell.run_cell = MethodType(custom_run_cell, self.kernel.shell)
|
580
|
+
|
581
|
+
self.kernel_client = self.kernel_manager.client()
|
582
|
+
self.kernel_client.start_channels()
|
583
|
+
|
584
|
+
# Update the user namespace with your custom variables
|
585
|
+
if namespace:
|
586
|
+
self.update_namespace(namespace)
|
587
|
+
|
588
|
+
# Set the underlying QTextEdit's palette
|
589
|
+
palette = QPalette()
|
590
|
+
self._control.setPalette(palette)
|
591
|
+
|
592
|
+
# Override the stylesheet to force background and text colors
|
593
|
+
# self._control.setStyleSheet("""
|
594
|
+
# background-color: #615f5f;
|
595
|
+
# color: #3ba8e7;
|
596
|
+
# selection-background-color: #006400;
|
597
|
+
# selection-color: white;
|
598
|
+
# """)
|
599
|
+
|
600
|
+
# Use a dark syntax style like monokai
|
601
|
+
# self.syntax_style = 'monokai'
|
602
|
+
self.set_default_style(colors='linux')
|
603
|
+
|
604
|
+
self.exit_requested.connect(self.stop)
|
605
|
+
|
606
|
+
def update_namespace(self, namespace):
|
607
|
+
"""
|
608
|
+
Update the user namespace with new variables.
|
609
|
+
"""
|
610
|
+
self.kernel.shell.user_ns.update(namespace)
|
611
|
+
|
612
|
+
def execute(self, source=None, hidden=False, interactive=False):
|
613
|
+
# Log the command before execution
|
614
|
+
source = source if source is not None else self.input_buffer
|
615
|
+
# self.command_log.append(source)
|
616
|
+
super().execute(source, hidden, interactive)
|
617
|
+
|
618
|
+
def stop(self):
|
619
|
+
self.kernel_client.stop_channels()
|
620
|
+
self.kernel_manager.shutdown_kernel()
|
621
|
+
|
622
|
+
|
623
|
+
def clear_layout(layout):
|
624
|
+
while layout.count():
|
625
|
+
item = layout.takeAt(0)
|
626
|
+
widget = item.widget()
|
627
|
+
if widget is not None:
|
628
|
+
widget.setParent(None)
|
629
|
+
elif item.layout() is not None:
|
630
|
+
clear_layout(item.layout())
|