ripple-down-rules 0.6.0__py3-none-any.whl → 0.6.6__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/__init__.py +21 -1
- ripple_down_rules/datastructures/callable_expression.py +24 -7
- ripple_down_rules/datastructures/case.py +12 -11
- ripple_down_rules/datastructures/dataclasses.py +135 -14
- ripple_down_rules/datastructures/enums.py +29 -86
- ripple_down_rules/datastructures/field_info.py +177 -0
- ripple_down_rules/datastructures/tracked_object.py +208 -0
- ripple_down_rules/experts.py +141 -50
- ripple_down_rules/failures.py +4 -0
- ripple_down_rules/helpers.py +75 -8
- ripple_down_rules/predicates.py +97 -0
- ripple_down_rules/rdr.py +712 -96
- ripple_down_rules/rdr_decorators.py +164 -112
- ripple_down_rules/rules.py +351 -114
- ripple_down_rules/user_interface/gui.py +66 -41
- ripple_down_rules/user_interface/ipython_custom_shell.py +46 -9
- ripple_down_rules/user_interface/prompt.py +80 -60
- ripple_down_rules/user_interface/template_file_creator.py +13 -8
- ripple_down_rules/utils.py +537 -53
- {ripple_down_rules-0.6.0.dist-info → ripple_down_rules-0.6.6.dist-info}/METADATA +4 -1
- ripple_down_rules-0.6.6.dist-info/RECORD +28 -0
- ripple_down_rules-0.6.0.dist-info/RECORD +0 -24
- {ripple_down_rules-0.6.0.dist-info → ripple_down_rules-0.6.6.dist-info}/WHEEL +0 -0
- {ripple_down_rules-0.6.0.dist-info → ripple_down_rules-0.6.6.dist-info}/licenses/LICENSE +0 -0
- {ripple_down_rules-0.6.0.dist-info → ripple_down_rules-0.6.6.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,19 @@
|
|
1
|
-
from
|
1
|
+
from __future__ import annotations
|
2
2
|
|
3
3
|
import inspect
|
4
4
|
import logging
|
5
|
+
import os.path
|
5
6
|
from types import MethodType
|
6
7
|
|
8
|
+
from graphviz import Digraph, Source
|
9
|
+
|
7
10
|
try:
|
8
11
|
from PyQt6.QtCore import Qt
|
9
12
|
from PyQt6.QtGui import QPixmap, QPainter, QPalette
|
10
13
|
from PyQt6.QtWidgets import (
|
11
14
|
QWidget, QVBoxLayout, QLabel, QScrollArea,
|
12
|
-
QSizePolicy, QToolButton, QHBoxLayout, QPushButton, QMainWindow, QGraphicsView, QGraphicsScene,
|
15
|
+
QSizePolicy, QToolButton, QHBoxLayout, QPushButton, QMainWindow, QGraphicsView, QGraphicsScene,
|
16
|
+
QGraphicsPixmapItem
|
13
17
|
)
|
14
18
|
from qtconsole.inprocess import QtInProcessKernelManager
|
15
19
|
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
@@ -21,7 +25,7 @@ except ImportError as e:
|
|
21
25
|
from typing_extensions import Optional, Any, List, Dict, Callable
|
22
26
|
|
23
27
|
from ..datastructures.dataclasses import CaseQuery
|
24
|
-
from ..datastructures.enums import PromptFor
|
28
|
+
from ..datastructures.enums import PromptFor, ExitStatus
|
25
29
|
from .template_file_creator import TemplateFileCreator
|
26
30
|
from ..utils import is_iterable, contains_return_statement, encapsulate_code_lines_into_a_function
|
27
31
|
from .object_diagram import generate_object_graph
|
@@ -120,7 +124,7 @@ class BackgroundWidget(QWidget):
|
|
120
124
|
|
121
125
|
|
122
126
|
class CollapsibleBox(QWidget):
|
123
|
-
def __init__(self, title="", parent=None, viewer: Optional[RDRCaseViewer]=None, chain_name: Optional[str] = None,
|
127
|
+
def __init__(self, title="", parent=None, viewer: Optional[RDRCaseViewer] = None, chain_name: Optional[str] = None,
|
124
128
|
main_obj: Optional[Dict[str, Any]] = None):
|
125
129
|
super().__init__(parent)
|
126
130
|
|
@@ -259,12 +263,14 @@ def color_name_to_html(color_name):
|
|
259
263
|
'g': 'green',
|
260
264
|
'b': 'blue',
|
261
265
|
'o': 'orange',
|
266
|
+
'm': 'magenta',
|
262
267
|
}
|
263
268
|
color_map = {
|
264
269
|
'red': '#d6336c',
|
265
270
|
'green': '#2eb872',
|
266
271
|
'blue': '#007acc',
|
267
272
|
'orange': '#FFA07A',
|
273
|
+
'magenta': '#a22bcf',
|
268
274
|
}
|
269
275
|
if len(color_name) == 1:
|
270
276
|
color_name = single_char_to_name.get(color_name, color_name)
|
@@ -282,14 +288,13 @@ class RDRCaseViewer(QMainWindow):
|
|
282
288
|
user_input: Optional[str] = None
|
283
289
|
attributes_widget: Optional[QWidget] = None
|
284
290
|
save_function: Optional[Callable[str, str], None] = None
|
291
|
+
instances: List[RDRCaseViewer] = []
|
292
|
+
exit_status: ExitStatus = ExitStatus.CLOSE
|
285
293
|
|
286
|
-
def __init__(self, parent=None
|
287
|
-
save_dir: Optional[str] = None,
|
288
|
-
save_model_name: Optional[str] = None):
|
294
|
+
def __init__(self, parent=None):
|
289
295
|
super().__init__(parent)
|
290
|
-
self.
|
291
|
-
self.
|
292
|
-
|
296
|
+
self.instances.clear()
|
297
|
+
self.instances.append(self)
|
293
298
|
self.setWindowTitle("RDR Case Viewer")
|
294
299
|
|
295
300
|
self.setBaseSize(1600, 600) # or your preferred initial size
|
@@ -325,38 +330,31 @@ class RDRCaseViewer(QMainWindow):
|
|
325
330
|
|
326
331
|
# Add both to main layout
|
327
332
|
main_layout.addWidget(self.attributes_widget, stretch=1)
|
328
|
-
main_layout.addWidget(middle_widget, stretch=
|
333
|
+
main_layout.addWidget(middle_widget, stretch=2)
|
329
334
|
main_layout.addWidget(self.obj_diagram_viewer, stretch=2)
|
330
335
|
|
331
|
-
def set_save_function(self, save_function: Callable[[str, str], None]) -> None:
|
332
|
-
"""
|
333
|
-
Set the function to save the file.
|
334
|
-
|
335
|
-
:param save_function: The function to save the file.
|
336
|
-
"""
|
337
|
-
self.save_function = save_function
|
338
|
-
self.save_btn.clicked.connect(lambda: self.save_function(self.save_dir, self.save_model_name))
|
339
|
-
|
340
336
|
def print(self, msg):
|
341
337
|
"""
|
342
338
|
Print a message to the console.
|
343
339
|
"""
|
344
340
|
self.ipython_console._append_plain_text(msg + '\n', True)
|
345
341
|
|
346
|
-
def update_for_case_query(self, case_query: CaseQuery,
|
347
|
-
prompt_for: Optional[PromptFor] = None, code_to_modify: Optional[str] = None
|
342
|
+
def update_for_case_query(self, case_query: CaseQuery, prompt_str: Optional[str] = None,
|
343
|
+
prompt_for: Optional[PromptFor] = None, code_to_modify: Optional[str] = None,
|
344
|
+
title: Optional[str] = None):
|
348
345
|
self.case_query = case_query
|
349
346
|
self.prompt_for = prompt_for
|
350
347
|
self.code_to_modify = code_to_modify
|
351
|
-
title_text =
|
348
|
+
title_text = title or ""
|
352
349
|
case_attr_type = ', '.join([t.__name__ for t in case_query.core_attribute_type])
|
353
350
|
case_attr_type = style(f"{case_attr_type}", 'g', 28, 'bold')
|
354
351
|
case_name = style(f"{case_query.name}", 'b', 28, 'bold')
|
355
352
|
title_text = style(f"{title_text} {case_name} of type {case_attr_type}", 'o', 28, 'bold')
|
356
|
-
self.update_for_object(case_query.case, case_query.case_name, case_query.scope, title_text
|
353
|
+
self.update_for_object(case_query.case, case_query.case_name, scope=case_query.scope, title_text=title_text,
|
354
|
+
header=prompt_str)
|
357
355
|
|
358
356
|
def update_for_object(self, obj: Any, name: str, scope: Optional[dict] = None,
|
359
|
-
title_text: Optional[str] = None):
|
357
|
+
title_text: Optional[str] = None, header: Optional[str] = None):
|
360
358
|
self.update_main_obj(obj, name)
|
361
359
|
title_text = title_text or style(f"{name}", 'o', 28, 'bold')
|
362
360
|
scope = scope or {}
|
@@ -365,6 +363,9 @@ class RDRCaseViewer(QMainWindow):
|
|
365
363
|
self.update_attribute_layout(obj, name)
|
366
364
|
self.title_label.setText(title_text)
|
367
365
|
self.ipython_console.update_namespace(scope)
|
366
|
+
if header is not None and len(header) > 0:
|
367
|
+
self.ipython_console.print(header)
|
368
|
+
self.exit_status = ExitStatus.CLOSE
|
368
369
|
|
369
370
|
def update_main_obj(self, obj, name):
|
370
371
|
self.main_obj = {name: obj}
|
@@ -436,8 +437,6 @@ class RDRCaseViewer(QMainWindow):
|
|
436
437
|
if isinstance(item.widget(), CollapsibleBox):
|
437
438
|
self.expand_collapse_all(item.widget(), expand=True, curr_depth=curr_depth + 1, max_depth=max_depth)
|
438
439
|
|
439
|
-
|
440
|
-
|
441
440
|
def create_buttons_widget(self):
|
442
441
|
button_widget = QWidget()
|
443
442
|
button_widget_layout = QVBoxLayout(button_widget)
|
@@ -461,30 +460,36 @@ class RDRCaseViewer(QMainWindow):
|
|
461
460
|
load_btn.clicked.connect(self._load)
|
462
461
|
load_btn.setStyleSheet(f"background-color: {color_name_to_html('b')}; color: white;") # Blue button
|
463
462
|
|
464
|
-
|
465
|
-
|
463
|
+
current_value_btn = QPushButton("Current Value")
|
464
|
+
current_value_btn.clicked.connect(self._show_current_value)
|
465
|
+
current_value_btn.setStyleSheet(f"background-color: {color_name_to_html('m')}; color: white;")
|
466
|
+
|
467
|
+
rule_tree_btn = QPushButton("Rule Tree")
|
468
|
+
rule_tree_btn.clicked.connect(self._show_rule_tree) # Placeholder for rule tree functionality
|
469
|
+
rule_tree_btn.setStyleSheet(f"background-color: {color_name_to_html('r')}; color: white;")
|
466
470
|
|
467
471
|
row_1_button_widget_layout.addWidget(edit_btn)
|
468
472
|
row_1_button_widget_layout.addWidget(load_btn)
|
469
|
-
row_1_button_widget_layout.addWidget(
|
470
|
-
row_2_button_widget_layout.addWidget(
|
473
|
+
row_1_button_widget_layout.addWidget(current_value_btn)
|
474
|
+
row_2_button_widget_layout.addWidget(rule_tree_btn)
|
475
|
+
row_2_button_widget_layout.addWidget(accept_btn)
|
471
476
|
return button_widget
|
472
477
|
|
473
478
|
def _accept(self):
|
479
|
+
self.exit_status = ExitStatus.SUCCESS
|
474
480
|
# close the window
|
475
481
|
self.close()
|
476
482
|
|
477
483
|
def _edit(self):
|
478
|
-
self.template_file_creator =
|
479
|
-
self.print)
|
484
|
+
self.template_file_creator = self.create_template_file_creator()
|
480
485
|
self.template_file_creator.edit()
|
481
486
|
|
482
487
|
def _load(self):
|
483
488
|
if not self.template_file_creator:
|
484
489
|
return
|
485
490
|
self.code_lines, updates = self.template_file_creator.load(self.template_file_creator.temp_file_path,
|
486
|
-
|
487
|
-
|
491
|
+
self.template_file_creator.func_name,
|
492
|
+
self.template_file_creator.print_func)
|
488
493
|
self.ipython_console.kernel.shell.user_ns.update(updates)
|
489
494
|
if self.code_lines is not None:
|
490
495
|
self.user_input = encapsulate_code_lines_into_a_function(
|
@@ -492,7 +497,22 @@ class RDRCaseViewer(QMainWindow):
|
|
492
497
|
self.template_file_creator.function_signature,
|
493
498
|
self.template_file_creator.func_doc, self.case_query)
|
494
499
|
self.case_query.scope.update(updates)
|
495
|
-
|
500
|
+
|
501
|
+
def _show_current_value(self):
|
502
|
+
self.ipython_console.print(self.case_query.current_value_str)
|
503
|
+
|
504
|
+
def _show_rule_tree(self):
|
505
|
+
if self.case_query is None:
|
506
|
+
self.ipython_console.print("No case query provided.")
|
507
|
+
return
|
508
|
+
if not self.case_query.rdr:
|
509
|
+
self.ipython_console.print("No rule tree available for this case query.")
|
510
|
+
return
|
511
|
+
self.case_query.render_rule_tree(view=True)
|
512
|
+
|
513
|
+
def create_template_file_creator(self) -> TemplateFileCreator:
|
514
|
+
return TemplateFileCreator(self.case_query, self.prompt_for, self.code_to_modify,
|
515
|
+
self.print)
|
496
516
|
|
497
517
|
def update_attribute_layout(self, obj, name: str):
|
498
518
|
# Clear the existing layout
|
@@ -541,7 +561,8 @@ class RDRCaseViewer(QMainWindow):
|
|
541
561
|
attr = f"{attr}"
|
542
562
|
try:
|
543
563
|
if is_iterable(value) or hasattr(value, "__dict__") and not inspect.isfunction(value):
|
544
|
-
self.add_collapsible(attr, value, layout, current_depth + 1, max_depth,
|
564
|
+
self.add_collapsible(attr, value, layout, current_depth + 1, max_depth,
|
565
|
+
chain_name=f"{chain_name}.{attr}")
|
545
566
|
else:
|
546
567
|
self.add_non_collapsible(attr, value, layout)
|
547
568
|
except Exception as e:
|
@@ -553,7 +574,7 @@ class RDRCaseViewer(QMainWindow):
|
|
553
574
|
type_name = type(value) if not isinstance(value, type) else value
|
554
575
|
collapsible = CollapsibleBox(
|
555
576
|
f'<b><span style="color:#FFA07A;">{attr}</span></b> {python_colored_repr(type_name)}', viewer=self,
|
556
|
-
|
577
|
+
chain_name=chain_name, main_obj=self.main_obj)
|
557
578
|
self.add_attributes(value, attr, collapsible.content_layout, current_depth, max_depth, chain_name=chain_name)
|
558
579
|
layout.addWidget(collapsible)
|
559
580
|
|
@@ -581,7 +602,6 @@ class IPythonConsole(RichJupyterWidget):
|
|
581
602
|
|
582
603
|
# Monkey patch its run_cell method
|
583
604
|
def custom_run_cell(this, raw_cell, **kwargs):
|
584
|
-
print(raw_cell)
|
585
605
|
if contains_return_statement(raw_cell) and 'def ' not in raw_cell:
|
586
606
|
if self.parent.template_file_creator and self.parent.template_file_creator.func_name in raw_cell:
|
587
607
|
self.command_log = self.parent.code_lines
|
@@ -595,7 +615,6 @@ class IPythonConsole(RichJupyterWidget):
|
|
595
615
|
|
596
616
|
original_run_cell = self.kernel.shell.run_cell
|
597
617
|
self.kernel.shell.run_cell = MethodType(custom_run_cell, self.kernel.shell)
|
598
|
-
|
599
618
|
self.kernel_client = self.kernel_manager.client()
|
600
619
|
self.kernel_client.start_channels()
|
601
620
|
|
@@ -624,7 +643,7 @@ class IPythonConsole(RichJupyterWidget):
|
|
624
643
|
.in-prompt { font-weight: bold; color: %(in_prompt_color)s }
|
625
644
|
.out-prompt-number { font-weight: bold; color: %(out_prompt_number_color)s }
|
626
645
|
.out-prompt { font-weight: bold; color: %(out_prompt_color)s }
|
627
|
-
'''%dict(
|
646
|
+
''' % dict(
|
628
647
|
bgcolor='#0b0d0b', fgcolor='#47d9cc', select="#555",
|
629
648
|
in_prompt_number_color='lime', in_prompt_color='lime',
|
630
649
|
out_prompt_number_color='red', out_prompt_color='red'
|
@@ -642,6 +661,12 @@ class IPythonConsole(RichJupyterWidget):
|
|
642
661
|
"""
|
643
662
|
self.kernel.shell.user_ns.update(namespace)
|
644
663
|
|
664
|
+
def print(self, msg):
|
665
|
+
"""
|
666
|
+
Custom print function to append messages to the command log.
|
667
|
+
"""
|
668
|
+
self.execute(f"print(\"\\n\\n{msg}\")", hidden=True)
|
669
|
+
|
645
670
|
def execute(self, source=None, hidden=False, interactive=False):
|
646
671
|
# Log the command before execution
|
647
672
|
source = source if source is not None else self.input_buffer
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import logging
|
2
|
+
import os
|
2
3
|
from typing import Optional, List
|
3
4
|
|
4
5
|
from IPython.core.magic import magics_class, Magics, line_magic
|
@@ -6,9 +7,9 @@ from IPython.terminal.embed import InteractiveShellEmbed
|
|
6
7
|
from colorama import Fore, Style
|
7
8
|
from traitlets.config import Config
|
8
9
|
|
10
|
+
from .template_file_creator import TemplateFileCreator
|
9
11
|
from ..datastructures.dataclasses import CaseQuery
|
10
12
|
from ..datastructures.enums import PromptFor
|
11
|
-
from .template_file_creator import TemplateFileCreator
|
12
13
|
from ..utils import contains_return_statement, extract_dependencies, encapsulate_code_lines_into_a_function
|
13
14
|
|
14
15
|
|
@@ -20,12 +21,19 @@ class MyMagics(Magics):
|
|
20
21
|
prompt_for: Optional[PromptFor] = None,
|
21
22
|
case_query: Optional[CaseQuery] = None):
|
22
23
|
super().__init__(shell)
|
24
|
+
self.case_query: Optional[CaseQuery] = case_query
|
23
25
|
self.rule_editor = TemplateFileCreator(case_query, prompt_for=prompt_for, code_to_modify=code_to_modify)
|
24
26
|
self.all_code_lines: Optional[List[str]] = None
|
27
|
+
self.edited: bool = False
|
28
|
+
self.loaded: bool = False
|
25
29
|
|
26
30
|
@line_magic
|
27
31
|
def edit(self, line):
|
28
|
-
self.
|
32
|
+
if self.edited:
|
33
|
+
self.rule_editor.open_file_in_editor()
|
34
|
+
else:
|
35
|
+
self.rule_editor.edit()
|
36
|
+
self.edited = True
|
29
37
|
|
30
38
|
@line_magic
|
31
39
|
def load(self, line):
|
@@ -33,6 +41,31 @@ class MyMagics(Magics):
|
|
33
41
|
self.rule_editor.func_name,
|
34
42
|
self.rule_editor.print_func)
|
35
43
|
self.shell.user_ns.update(updates)
|
44
|
+
self.case_query.scope.update(updates)
|
45
|
+
self.loaded = True
|
46
|
+
|
47
|
+
@line_magic
|
48
|
+
def current_value(self, line):
|
49
|
+
"""
|
50
|
+
Display the current value of the attribute of the case.
|
51
|
+
"""
|
52
|
+
if self.case_query is None:
|
53
|
+
print(f"{Fore.RED}No case query provided.{Style.RESET_ALL}")
|
54
|
+
return
|
55
|
+
print(self.case_query.current_value_str)
|
56
|
+
|
57
|
+
@line_magic
|
58
|
+
def show_rule_tree(self, line):
|
59
|
+
"""
|
60
|
+
Display the rule tree for the current case query.
|
61
|
+
"""
|
62
|
+
if self.case_query is None:
|
63
|
+
print(f"{Fore.RED}No case query provided.{Style.RESET_ALL}")
|
64
|
+
return
|
65
|
+
if self.case_query.rdr is None:
|
66
|
+
print(f"{Fore.RED}No RDR available for the current case query.{Style.RESET_ALL}")
|
67
|
+
return
|
68
|
+
self.case_query.render_rule_tree(view=True)
|
36
69
|
|
37
70
|
@line_magic
|
38
71
|
def help(self, line):
|
@@ -42,12 +75,16 @@ class MyMagics(Magics):
|
|
42
75
|
help_text = f"""
|
43
76
|
Directly write python code in the shell, and then `{Fore.GREEN}return {Fore.RESET}output`. Or use
|
44
77
|
the magic commands to write the code in a temporary file and edit it in PyCharm:
|
45
|
-
{Fore.MAGENTA}
|
78
|
+
{Fore.MAGENTA}%edit{Style.RESET_ALL}
|
46
79
|
Opens a temporary file in PyCharm for editing a function (conclusion or conditions for case)
|
47
80
|
that will be executed on the case object.
|
48
|
-
{Fore.MAGENTA}
|
81
|
+
{Fore.MAGENTA}%load{Style.RESET_ALL}
|
49
82
|
Loads the function defined in the temporary file into the user namespace, that can then be used inside the
|
50
83
|
Ipython shell. You can then do `{Fore.GREEN}return {Fore.RESET}function_name(case)`.
|
84
|
+
{Fore.MAGENTA}%current_value{Style.RESET_ALL}
|
85
|
+
Shows the current value of the case attribute on which the rule are being fit.
|
86
|
+
{Fore.MAGENTA}%show_rule_tree{Style.RESET_ALL}
|
87
|
+
Displays the rule tree for the current case query.
|
51
88
|
"""
|
52
89
|
print(help_text)
|
53
90
|
|
@@ -136,7 +173,7 @@ class IPythonShell:
|
|
136
173
|
"""
|
137
174
|
if self.shell.all_lines[-1] in ['quit', 'exit']:
|
138
175
|
self.user_input = 'exit'
|
139
|
-
elif self.shell.all_lines[
|
176
|
+
elif self.shell.all_lines[-1].replace('return', '').strip() == '':
|
140
177
|
self.user_input = None
|
141
178
|
else:
|
142
179
|
self.all_code_lines = extract_dependencies(self.shell.all_lines)
|
@@ -144,7 +181,7 @@ class IPythonShell:
|
|
144
181
|
self.user_input = None
|
145
182
|
else:
|
146
183
|
self.user_input = encapsulate_code_lines_into_a_function(self.all_code_lines,
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
184
|
+
function_name=self.shell.my_magics.rule_editor.func_name,
|
185
|
+
function_signature=self.shell.my_magics.rule_editor.function_signature,
|
186
|
+
func_doc=self.shell.my_magics.rule_editor.func_doc,
|
187
|
+
case_query=self.case_query)
|
@@ -1,10 +1,13 @@
|
|
1
1
|
import ast
|
2
2
|
import logging
|
3
|
+
import sys
|
3
4
|
from _ast import AST
|
4
5
|
|
6
|
+
from .. import logger
|
7
|
+
|
5
8
|
try:
|
6
9
|
from PyQt6.QtWidgets import QApplication
|
7
|
-
from .gui import RDRCaseViewer
|
10
|
+
from .gui import RDRCaseViewer, style
|
8
11
|
except ImportError:
|
9
12
|
QApplication = None
|
10
13
|
RDRCaseViewer = None
|
@@ -17,27 +20,24 @@ from typing_extensions import Optional, Tuple
|
|
17
20
|
|
18
21
|
from ..datastructures.callable_expression import CallableExpression, parse_string_to_expression
|
19
22
|
from ..datastructures.dataclasses import CaseQuery
|
20
|
-
from ..datastructures.enums import PromptFor
|
23
|
+
from ..datastructures.enums import PromptFor, ExitStatus
|
21
24
|
from .ipython_custom_shell import IPythonShell
|
22
25
|
from ..utils import make_list
|
23
|
-
from threading import
|
26
|
+
from threading import Lock
|
24
27
|
|
25
28
|
|
26
29
|
class UserPrompt:
|
27
30
|
"""
|
28
31
|
A class to handle user prompts for the RDR.
|
29
32
|
"""
|
30
|
-
shell_lock:
|
33
|
+
shell_lock: Lock = Lock() # To ensure that only one thread can access the shell at a time
|
31
34
|
|
32
|
-
def __init__(self,
|
35
|
+
def __init__(self, prompt_user: bool = True):
|
33
36
|
"""
|
34
37
|
Initialize the UserPrompt class.
|
35
|
-
|
36
|
-
:param viewer: The RDRCaseViewer instance to use for prompting the user.
|
37
38
|
"""
|
38
|
-
self.viewer =
|
39
|
-
self.print_func = print if viewer
|
40
|
-
|
39
|
+
self.viewer = RDRCaseViewer.instances[0] if RDRCaseViewer and any(RDRCaseViewer.instances) else None
|
40
|
+
self.print_func = self.viewer.print if self.viewer else print
|
41
41
|
|
42
42
|
def prompt_user_for_expression(self, case_query: CaseQuery, prompt_for: PromptFor, prompt_str: Optional[str] = None) \
|
43
43
|
-> Tuple[Optional[str], Optional[CallableExpression]]:
|
@@ -49,39 +49,43 @@ class UserPrompt:
|
|
49
49
|
:param prompt_str: The prompt string to display to the user.
|
50
50
|
:return: A callable expression that takes a case and executes user expression on it.
|
51
51
|
"""
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
conclusion_type = bool if prompt_for == PromptFor.Conditions else case_query.attribute_type
|
70
|
-
callable_expression = CallableExpression(user_input, conclusion_type, expression_tree=expression_tree,
|
71
|
-
scope=case_query.scope,
|
72
|
-
mutually_exclusive=case_query.mutually_exclusive)
|
73
|
-
try:
|
74
|
-
result = callable_expression(case_query.case)
|
75
|
-
if len(make_list(result)) == 0:
|
76
|
-
self.print_func(f"{Fore.YELLOW}The given expression gave an empty result for case {case_query.name}."
|
77
|
-
f" Please modify!{Style.RESET_ALL}")
|
78
|
-
continue
|
79
|
-
break
|
80
|
-
except Exception as e:
|
81
|
-
logging.error(e)
|
82
|
-
self.print_func(f"{Fore.RED}{e}{Style.RESET_ALL}")
|
83
|
-
return user_input, callable_expression
|
52
|
+
with self.shell_lock:
|
53
|
+
prev_user_input: Optional[str] = None
|
54
|
+
user_input_to_modify: Optional[str] = None
|
55
|
+
callable_expression: Optional[CallableExpression] = None
|
56
|
+
while True:
|
57
|
+
user_input, expression_tree = self.prompt_user_about_case(case_query, prompt_for, prompt_str,
|
58
|
+
code_to_modify=prev_user_input)
|
59
|
+
if user_input is None:
|
60
|
+
if prompt_for == PromptFor.Conclusion:
|
61
|
+
self.print_func(f"\n{Fore.YELLOW}No conclusion provided. Exiting.{Style.RESET_ALL}")
|
62
|
+
return None, None
|
63
|
+
else:
|
64
|
+
self.print_func(f"\n{Fore.RED}Conditions must be provided. Please try again.{Style.RESET_ALL}")
|
65
|
+
continue
|
66
|
+
elif user_input in ["exit", 'quit']:
|
67
|
+
self.print_func(f"\n{Fore.YELLOW}Exiting.{Style.RESET_ALL}")
|
68
|
+
return user_input, None
|
84
69
|
|
70
|
+
prev_user_input = '\n'.join(user_input.split('\n')[2:-1])
|
71
|
+
conclusion_type = bool if prompt_for == PromptFor.Conditions else case_query.attribute_type
|
72
|
+
callable_expression = CallableExpression(user_input, conclusion_type, expression_tree=expression_tree,
|
73
|
+
scope=case_query.scope,
|
74
|
+
mutually_exclusive=case_query.mutually_exclusive)
|
75
|
+
try:
|
76
|
+
result = callable_expression(case_query.case)
|
77
|
+
if len(make_list(result)) == 0 and (user_input_to_modify is not None
|
78
|
+
and (prev_user_input != user_input_to_modify)):
|
79
|
+
user_input_to_modify = prev_user_input
|
80
|
+
self.print_func(
|
81
|
+
f"{Fore.YELLOW}The given expression gave an empty result for case {case_query.name}."
|
82
|
+
f" Please accept or modify!{Style.RESET_ALL}")
|
83
|
+
continue
|
84
|
+
break
|
85
|
+
except Exception as e:
|
86
|
+
logging.error(e)
|
87
|
+
self.print_func(f"{Fore.RED}{e}{Style.RESET_ALL}")
|
88
|
+
return user_input, callable_expression
|
85
89
|
|
86
90
|
def prompt_user_about_case(self, case_query: CaseQuery, prompt_for: PromptFor,
|
87
91
|
prompt_str: Optional[str] = None,
|
@@ -95,27 +99,40 @@ class UserPrompt:
|
|
95
99
|
:param code_to_modify: The code to modify. If given will be used as a start for user to modify.
|
96
100
|
:return: The user input, and the executable expression that was parsed from the user input.
|
97
101
|
"""
|
98
|
-
|
99
|
-
|
102
|
+
logger.debug("Entered shell")
|
103
|
+
initial_prompt_str = f"{prompt_str}\n" if prompt_str is not None else ''
|
100
104
|
if prompt_for == PromptFor.Conclusion:
|
101
|
-
|
105
|
+
prompt_for_str = f"Give possible value(s) for:"
|
102
106
|
else:
|
103
|
-
|
107
|
+
prompt_for_str = f"Give conditions for:"
|
104
108
|
case_query.scope.update({'case': case_query.case})
|
105
109
|
shell = None
|
110
|
+
|
106
111
|
if self.viewer is None:
|
112
|
+
prompt_for_str = prompt_for_str.replace(":", f" {case_query.name}:")
|
113
|
+
prompt_str = f"{Fore.WHITE}{initial_prompt_str}{Fore.MAGENTA}{prompt_for_str}"
|
107
114
|
prompt_str = self.construct_prompt_str_for_shell(case_query, prompt_for, prompt_str)
|
108
115
|
shell = IPythonShell(header=prompt_str, prompt_for=prompt_for, case_query=case_query,
|
109
|
-
|
116
|
+
code_to_modify=code_to_modify)
|
110
117
|
else:
|
111
|
-
|
112
|
-
self.viewer.update_for_case_query(case_query,
|
113
|
-
|
118
|
+
prompt_str = case_query.current_value_str
|
119
|
+
self.viewer.update_for_case_query(case_query, prompt_for=prompt_for, code_to_modify=code_to_modify,
|
120
|
+
title=prompt_for_str, prompt_str=prompt_str)
|
114
121
|
user_input, expression_tree = self.prompt_user_input_and_parse_to_expression(shell=shell)
|
115
|
-
|
122
|
+
logger.debug("Exited shell")
|
116
123
|
return user_input, expression_tree
|
117
124
|
|
118
|
-
|
125
|
+
def build_prompt_str_for_ai(self, case_query: CaseQuery, prompt_for: PromptFor,
|
126
|
+
initial_prompt_str: Optional[str] = None) -> str:
|
127
|
+
initial_prompt_str = f"{initial_prompt_str}\n" if initial_prompt_str is not None else ''
|
128
|
+
if prompt_for == PromptFor.Conclusion:
|
129
|
+
prompt_for_str = f"Give possible value(s) for:"
|
130
|
+
else:
|
131
|
+
prompt_for_str = f"Give conditions for:"
|
132
|
+
prompt_for_str = prompt_for_str.replace(":", f" {case_query.name}:")
|
133
|
+
prompt_str = f"{Fore.WHITE}{initial_prompt_str}{Fore.MAGENTA}{prompt_for_str}"
|
134
|
+
prompt_str += '\n' + case_query.current_value_str
|
135
|
+
return prompt_str
|
119
136
|
|
120
137
|
def construct_prompt_str_for_shell(self, case_query: CaseQuery, prompt_for: PromptFor,
|
121
138
|
prompt_str: Optional[str] = None) -> str:
|
@@ -126,8 +143,7 @@ class UserPrompt:
|
|
126
143
|
:param prompt_for: The type of information the user should provide for the given case.
|
127
144
|
:param prompt_str: The prompt string to display to the user.
|
128
145
|
"""
|
129
|
-
prompt_str +=
|
130
|
-
f"{Fore.CYAN}({', '.join(map(lambda x: x.__name__, case_query.core_attribute_type))}){Fore.MAGENTA}")
|
146
|
+
prompt_str += '\n' + case_query.current_value_str
|
131
147
|
if prompt_for == PromptFor.Conditions:
|
132
148
|
prompt_str += (f"\ne.g. `{Fore.GREEN}return {Fore.BLUE}len{Fore.RESET}(case.attribute) > {Fore.BLUE}0` "
|
133
149
|
f"{Fore.MAGENTA}\nOR `{Fore.GREEN}return {Fore.YELLOW}True`{Fore.MAGENTA} (If you want the"
|
@@ -137,7 +153,6 @@ class UserPrompt:
|
|
137
153
|
prompt_str = f"{Fore.MAGENTA}{prompt_str}{Fore.YELLOW}\n(Write %help for guide){Fore.RESET}\n"
|
138
154
|
return prompt_str
|
139
155
|
|
140
|
-
|
141
156
|
def prompt_user_input_and_parse_to_expression(self, shell: Optional[IPythonShell] = None,
|
142
157
|
user_input: Optional[str] = None) \
|
143
158
|
-> Tuple[Optional[str], Optional[ast.AST]]:
|
@@ -151,17 +166,18 @@ class UserPrompt:
|
|
151
166
|
while True:
|
152
167
|
if user_input is None:
|
153
168
|
user_input = self.start_shell_and_get_user_input(shell=shell)
|
154
|
-
if user_input is None or user_input
|
169
|
+
if user_input is None or user_input in ['exit', 'quit']:
|
155
170
|
return user_input, None
|
156
|
-
|
157
|
-
|
158
|
-
|
171
|
+
if logger.level <= logging.DEBUG:
|
172
|
+
self.print_func(f"\n{Fore.GREEN}Captured User input: {Style.RESET_ALL}")
|
173
|
+
highlighted_code = highlight(user_input, PythonLexer(), TerminalFormatter())
|
174
|
+
self.print_func(highlighted_code)
|
159
175
|
try:
|
160
176
|
return user_input, parse_string_to_expression(user_input)
|
161
177
|
except Exception as e:
|
162
178
|
msg = f"Error parsing expression: {e}"
|
163
179
|
logging.error(msg)
|
164
|
-
self.print_func(f"{Fore.RED}{msg}{Style.RESET_ALL}")
|
180
|
+
self.print_func(f"\n{Fore.RED}{msg}{Style.RESET_ALL}")
|
165
181
|
user_input = None
|
166
182
|
|
167
183
|
def start_shell_and_get_user_input(self, shell: Optional[IPythonShell] = None) -> Optional[str]:
|
@@ -173,6 +189,8 @@ class UserPrompt:
|
|
173
189
|
"""
|
174
190
|
if self.viewer is None:
|
175
191
|
shell = IPythonShell() if shell is None else shell
|
192
|
+
if not hasattr(shell.shell, "auto_match"):
|
193
|
+
shell.shell.auto_match = True # or True, depending on your preference
|
176
194
|
shell.run()
|
177
195
|
user_input = shell.user_input
|
178
196
|
else:
|
@@ -181,5 +199,7 @@ class UserPrompt:
|
|
181
199
|
raise RuntimeError("QApplication instance is None. Please run the application first.")
|
182
200
|
self.viewer.show()
|
183
201
|
app.exec()
|
202
|
+
if self.viewer.exit_status == ExitStatus.CLOSE:
|
203
|
+
sys.exit()
|
184
204
|
user_input = self.viewer.user_input
|
185
205
|
return user_input
|