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.
@@ -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;">&lt;error: {e}&gt;</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;'>&lt;error: {e}&gt;</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())