ripple-down-rules 0.6.28__py3-none-any.whl → 0.6.30__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 +1 -1
- ripple_down_rules/datastructures/callable_expression.py +4 -2
- ripple_down_rules/datastructures/case.py +12 -11
- ripple_down_rules/datastructures/dataclasses.py +87 -9
- ripple_down_rules/experts.py +137 -44
- ripple_down_rules/rdr.py +44 -29
- ripple_down_rules/rules.py +15 -1
- ripple_down_rules/user_interface/gui.py +59 -40
- ripple_down_rules/user_interface/ipython_custom_shell.py +36 -7
- ripple_down_rules/user_interface/prompt.py +41 -26
- ripple_down_rules/user_interface/template_file_creator.py +10 -8
- ripple_down_rules/utils.py +77 -15
- {ripple_down_rules-0.6.28.dist-info → ripple_down_rules-0.6.30.dist-info}/METADATA +1 -1
- ripple_down_rules-0.6.30.dist-info/RECORD +24 -0
- ripple_down_rules-0.6.28.dist-info/RECORD +0 -24
- {ripple_down_rules-0.6.28.dist-info → ripple_down_rules-0.6.30.dist-info}/WHEEL +0 -0
- {ripple_down_rules-0.6.28.dist-info → ripple_down_rules-0.6.30.dist-info}/licenses/LICENSE +0 -0
- {ripple_down_rules-0.6.28.dist-info → ripple_down_rules-0.6.30.dist-info}/top_level.txt +0 -0
ripple_down_rules/rdr.py
CHANGED
@@ -13,7 +13,6 @@ from . import logger
|
|
13
13
|
|
14
14
|
try:
|
15
15
|
from matplotlib import pyplot as plt
|
16
|
-
|
17
16
|
Figure = plt.Figure
|
18
17
|
except ImportError as e:
|
19
18
|
logger.debug(f"{e}: matplotlib is not installed")
|
@@ -90,23 +89,13 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
|
|
90
89
|
:param save_dir: The directory to save the classifier to.
|
91
90
|
"""
|
92
91
|
self.model_name: Optional[str] = model_name
|
93
|
-
self.save_dir = save_dir
|
94
|
-
self.start_rule = start_rule
|
92
|
+
self.save_dir: Optional[str] = save_dir
|
93
|
+
self.start_rule: Optional[Rule] = start_rule
|
95
94
|
self.fig: Optional[Figure] = None
|
96
95
|
self.viewer: Optional[RDRCaseViewer] = RDRCaseViewer.instances[0]\
|
97
96
|
if RDRCaseViewer and any(RDRCaseViewer.instances) else None
|
98
97
|
self.input_node: Optional[Rule] = None
|
99
98
|
|
100
|
-
@property
|
101
|
-
def viewer(self):
|
102
|
-
return self._viewer
|
103
|
-
|
104
|
-
@viewer.setter
|
105
|
-
def viewer(self, viewer):
|
106
|
-
self._viewer = viewer
|
107
|
-
if viewer:
|
108
|
-
viewer.set_save_function(self.save)
|
109
|
-
|
110
99
|
def render_evaluated_rule_tree(self, filename: str, show_full_tree: bool = False) -> None:
|
111
100
|
if show_full_tree:
|
112
101
|
start_rule = self.start_rule if self.input_node is None else self.input_node
|
@@ -117,6 +106,26 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
|
|
117
106
|
render_tree(evaluated_rules[0], use_dot_exporter=True, filename=filename,
|
118
107
|
only_nodes=evaluated_rules)
|
119
108
|
|
109
|
+
def get_contributing_rules(self) -> Optional[List[Rule]]:
|
110
|
+
"""
|
111
|
+
Get the contributing rules of the classifier.
|
112
|
+
|
113
|
+
:return: The contributing rules.
|
114
|
+
"""
|
115
|
+
if self.start_rule is None:
|
116
|
+
return None
|
117
|
+
return [r for r in self.get_fired_rule_tree() if r.contributed]
|
118
|
+
|
119
|
+
def get_fired_rule_tree(self) -> Optional[List[Rule]]:
|
120
|
+
"""
|
121
|
+
Get the fired rule tree of the classifier.
|
122
|
+
|
123
|
+
:return: The fired rule tree.
|
124
|
+
"""
|
125
|
+
if self.start_rule is None:
|
126
|
+
return None
|
127
|
+
return [r for r in self.get_evaluated_rule_tree() if r.fired]
|
128
|
+
|
120
129
|
def get_evaluated_rule_tree(self) -> Optional[List[Rule]]:
|
121
130
|
"""
|
122
131
|
Get the evaluated rule tree of the classifier.
|
@@ -196,16 +205,6 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
|
|
196
205
|
"""
|
197
206
|
pass
|
198
207
|
|
199
|
-
def set_viewer(self, viewer: RDRCaseViewer):
|
200
|
-
"""
|
201
|
-
Set the viewer for the classifier.
|
202
|
-
|
203
|
-
:param viewer: The viewer to set.
|
204
|
-
"""
|
205
|
-
self.viewer = viewer
|
206
|
-
if self.viewer is not None:
|
207
|
-
self.viewer.set_save_function(self.save)
|
208
|
-
|
209
208
|
def fit(self, case_queries: List[CaseQuery],
|
210
209
|
expert: Optional[Expert] = None,
|
211
210
|
n_iter: int = None,
|
@@ -230,7 +229,8 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
|
|
230
229
|
num_rules: int = 0
|
231
230
|
while not stop_iterating:
|
232
231
|
for case_query in case_queries:
|
233
|
-
pred_cat = self.fit_case(case_query, expert=expert,
|
232
|
+
pred_cat = self.fit_case(case_query, expert=expert, clear_expert_answers=False,
|
233
|
+
**kwargs_for_fit_case)
|
234
234
|
if case_query.target is None:
|
235
235
|
continue
|
236
236
|
target = {case_query.attribute_name: case_query.target(case_query.case)}
|
@@ -272,8 +272,7 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
|
|
272
272
|
"""
|
273
273
|
if self.start_rule is not None:
|
274
274
|
for rule in [self.start_rule] + list(self.start_rule.descendants):
|
275
|
-
rule.
|
276
|
-
rule.fired = False
|
275
|
+
rule.reset()
|
277
276
|
if self.start_rule is not None and self.start_rule.parent is None:
|
278
277
|
if self.input_node is None:
|
279
278
|
self.input_node = type(self.start_rule)(parent=None, uid='0')
|
@@ -308,6 +307,7 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
|
|
308
307
|
update_existing_rules: bool = True,
|
309
308
|
scenario: Optional[Callable] = None,
|
310
309
|
ask_now: Callable = lambda _: True,
|
310
|
+
clear_expert_answers: bool = True,
|
311
311
|
**kwargs) \
|
312
312
|
-> Union[CallableExpression, Dict[str, CallableExpression]]:
|
313
313
|
"""
|
@@ -319,7 +319,8 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
|
|
319
319
|
:param update_existing_rules: Whether to update the existing same conclusion type rules that already gave
|
320
320
|
some conclusions with the type required by the case query.
|
321
321
|
:param scenario: The scenario at which the case was created, this is used to recreate the case if needed.
|
322
|
-
:ask_now: Whether to ask the expert for refinements or alternatives.
|
322
|
+
:param ask_now: Whether to ask the expert for refinements or alternatives.
|
323
|
+
:param clear_expert_answers: Whether to clear expert answers after saving the new rule.
|
323
324
|
:return: The category that the case belongs to.
|
324
325
|
"""
|
325
326
|
if case_query is None:
|
@@ -329,6 +330,7 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
|
|
329
330
|
self.case_type = case_query.case_type if self.case_type is None else self.case_type
|
330
331
|
self.case_name = case_query.case_name if self.case_name is None else self.case_name
|
331
332
|
case_query.scenario = scenario if case_query.scenario is None else case_query.scenario
|
333
|
+
case_query.rdr = self
|
332
334
|
|
333
335
|
expert = expert or Human(answers_save_path=self.save_dir + '/expert_answers'
|
334
336
|
if self.save_dir else None)
|
@@ -348,7 +350,8 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
|
|
348
350
|
|
349
351
|
if self.save_dir is not None:
|
350
352
|
self.save()
|
351
|
-
|
353
|
+
if clear_expert_answers:
|
354
|
+
expert.clear_answers()
|
352
355
|
|
353
356
|
return fit_case_result
|
354
357
|
|
@@ -767,6 +770,11 @@ class SingleClassRDR(RDRWithCodeWriter):
|
|
767
770
|
"""
|
768
771
|
pred = self.evaluate(case)
|
769
772
|
conclusion = pred.conclusion(case) if pred is not None and pred.fired else self.default_conclusion
|
773
|
+
if pred is not None and pred.fired:
|
774
|
+
pred.contributed = True
|
775
|
+
pred.last_conclusion = conclusion
|
776
|
+
if case_query is not None:
|
777
|
+
pred.contributed_to_case_query = True
|
770
778
|
if pred is not None and pred.fired and case_query is not None:
|
771
779
|
if pred.corner_case_metadata is None and conclusion is not None \
|
772
780
|
and type(conclusion) in case_query.core_attribute_type:
|
@@ -884,6 +892,13 @@ class MultiClassRDR(RDRWithCodeWriter):
|
|
884
892
|
and any(
|
885
893
|
ct in case_query.core_attribute_type for ct in map(type, make_list(rule_conclusion))):
|
886
894
|
evaluated_rule.corner_case_metadata = CaseFactoryMetaData.from_case_query(case_query)
|
895
|
+
if rule_conclusion is not None and any(make_list(rule_conclusion)):
|
896
|
+
evaluated_rule.contributed = True
|
897
|
+
evaluated_rule.last_conclusion = rule_conclusion
|
898
|
+
if case_query is not None:
|
899
|
+
rule_conclusion_types = set(map(type, make_list(rule_conclusion)))
|
900
|
+
if any(rule_conclusion_types.intersection(set(case_query.core_attribute_type))):
|
901
|
+
evaluated_rule.contributed_to_case_query = True
|
887
902
|
self.add_conclusion(rule_conclusion)
|
888
903
|
evaluated_rule = next_rule
|
889
904
|
return make_set(self.conclusions)
|
@@ -978,7 +993,7 @@ class MultiClassRDR(RDRWithCodeWriter):
|
|
978
993
|
"""
|
979
994
|
if not self.start_rule:
|
980
995
|
conditions = expert.ask_for_conditions(case_query)
|
981
|
-
self.start_rule = MultiClassTopRule.from_case_query(case_query)
|
996
|
+
self.start_rule: MultiClassTopRule = MultiClassTopRule.from_case_query(case_query)
|
982
997
|
|
983
998
|
@property
|
984
999
|
def last_top_rule(self) -> Optional[MultiClassTopRule]:
|
ripple_down_rules/rules.py
CHANGED
@@ -63,6 +63,9 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
|
63
63
|
self.uid: str = uid if uid else str(uuid4().int)
|
64
64
|
self.evaluated: bool = False
|
65
65
|
self._user_defined_name: Optional[str] = None
|
66
|
+
self.last_conclusion: Optional[Any] = None
|
67
|
+
self.contributed: bool = False
|
68
|
+
self.contributed_to_case_query: bool = False
|
66
69
|
|
67
70
|
def get_an_updated_case_copy(self, case: Case) -> Case:
|
68
71
|
"""
|
@@ -72,11 +75,22 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
|
72
75
|
return get_an_updated_case_copy(case, self.conclusion, self.conclusion_name, self.conclusion.conclusion_type,
|
73
76
|
self.mutually_exclusive)
|
74
77
|
|
78
|
+
def reset(self):
|
79
|
+
self.evaluated = False
|
80
|
+
self.fired = False
|
81
|
+
self.contributed = False
|
82
|
+
self.contributed_to_case_query = False
|
83
|
+
self.last_conclusion = None
|
84
|
+
|
75
85
|
@property
|
76
86
|
def color(self) -> str:
|
77
87
|
if self.evaluated:
|
78
|
-
if self.
|
88
|
+
if self.contributed_to_case_query:
|
79
89
|
return "green"
|
90
|
+
elif self.contributed:
|
91
|
+
return "yellow"
|
92
|
+
elif self.fired:
|
93
|
+
return "orange"
|
80
94
|
else:
|
81
95
|
return "red"
|
82
96
|
else:
|
@@ -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
|
@@ -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)
|
@@ -285,16 +291,10 @@ class RDRCaseViewer(QMainWindow):
|
|
285
291
|
instances: List[RDRCaseViewer] = []
|
286
292
|
exit_status: ExitStatus = ExitStatus.CLOSE
|
287
293
|
|
288
|
-
def __init__(self, parent=None
|
289
|
-
save_dir: Optional[str] = None,
|
290
|
-
save_model_name: Optional[str] = None):
|
294
|
+
def __init__(self, parent=None):
|
291
295
|
super().__init__(parent)
|
292
|
-
self.exit_status = ExitStatus.CLOSE
|
293
296
|
self.instances.clear()
|
294
297
|
self.instances.append(self)
|
295
|
-
self.save_dir = save_dir
|
296
|
-
self.save_model_name = save_model_name
|
297
|
-
|
298
298
|
self.setWindowTitle("RDR Case Viewer")
|
299
299
|
|
300
300
|
self.setBaseSize(1600, 600) # or your preferred initial size
|
@@ -333,35 +333,28 @@ class RDRCaseViewer(QMainWindow):
|
|
333
333
|
main_layout.addWidget(middle_widget, stretch=2)
|
334
334
|
main_layout.addWidget(self.obj_diagram_viewer, stretch=2)
|
335
335
|
|
336
|
-
def set_save_function(self, save_function: Callable[[str, str], None]) -> None:
|
337
|
-
"""
|
338
|
-
Set the function to save the file.
|
339
|
-
|
340
|
-
:param save_function: The function to save the file.
|
341
|
-
"""
|
342
|
-
self.save_function = save_function
|
343
|
-
self.save_btn.clicked.connect(lambda: self.save_function(self.save_dir, self.save_model_name))
|
344
|
-
|
345
336
|
def print(self, msg):
|
346
337
|
"""
|
347
338
|
Print a message to the console.
|
348
339
|
"""
|
349
340
|
self.ipython_console._append_plain_text(msg + '\n', True)
|
350
341
|
|
351
|
-
def update_for_case_query(self, case_query: CaseQuery,
|
352
|
-
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):
|
353
345
|
self.case_query = case_query
|
354
346
|
self.prompt_for = prompt_for
|
355
347
|
self.code_to_modify = code_to_modify
|
356
|
-
title_text =
|
348
|
+
title_text = title or ""
|
357
349
|
case_attr_type = ', '.join([t.__name__ for t in case_query.core_attribute_type])
|
358
350
|
case_attr_type = style(f"{case_attr_type}", 'g', 28, 'bold')
|
359
351
|
case_name = style(f"{case_query.name}", 'b', 28, 'bold')
|
360
352
|
title_text = style(f"{title_text} {case_name} of type {case_attr_type}", 'o', 28, 'bold')
|
361
|
-
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)
|
362
355
|
|
363
356
|
def update_for_object(self, obj: Any, name: str, scope: Optional[dict] = None,
|
364
|
-
title_text: Optional[str] = None):
|
357
|
+
title_text: Optional[str] = None, header: Optional[str] = None):
|
365
358
|
self.update_main_obj(obj, name)
|
366
359
|
title_text = title_text or style(f"{name}", 'o', 28, 'bold')
|
367
360
|
scope = scope or {}
|
@@ -370,6 +363,9 @@ class RDRCaseViewer(QMainWindow):
|
|
370
363
|
self.update_attribute_layout(obj, name)
|
371
364
|
self.title_label.setText(title_text)
|
372
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
|
373
369
|
|
374
370
|
def update_main_obj(self, obj, name):
|
375
371
|
self.main_obj = {name: obj}
|
@@ -441,8 +437,6 @@ class RDRCaseViewer(QMainWindow):
|
|
441
437
|
if isinstance(item.widget(), CollapsibleBox):
|
442
438
|
self.expand_collapse_all(item.widget(), expand=True, curr_depth=curr_depth + 1, max_depth=max_depth)
|
443
439
|
|
444
|
-
|
445
|
-
|
446
440
|
def create_buttons_widget(self):
|
447
441
|
button_widget = QWidget()
|
448
442
|
button_widget_layout = QVBoxLayout(button_widget)
|
@@ -466,13 +460,19 @@ class RDRCaseViewer(QMainWindow):
|
|
466
460
|
load_btn.clicked.connect(self._load)
|
467
461
|
load_btn.setStyleSheet(f"background-color: {color_name_to_html('b')}; color: white;") # Blue button
|
468
462
|
|
469
|
-
|
470
|
-
|
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;")
|
471
470
|
|
472
471
|
row_1_button_widget_layout.addWidget(edit_btn)
|
473
472
|
row_1_button_widget_layout.addWidget(load_btn)
|
474
|
-
row_1_button_widget_layout.addWidget(
|
475
|
-
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)
|
476
476
|
return button_widget
|
477
477
|
|
478
478
|
def _accept(self):
|
@@ -481,16 +481,15 @@ class RDRCaseViewer(QMainWindow):
|
|
481
481
|
self.close()
|
482
482
|
|
483
483
|
def _edit(self):
|
484
|
-
self.template_file_creator =
|
485
|
-
self.print)
|
484
|
+
self.template_file_creator = self.create_template_file_creator()
|
486
485
|
self.template_file_creator.edit()
|
487
486
|
|
488
487
|
def _load(self):
|
489
488
|
if not self.template_file_creator:
|
490
489
|
return
|
491
490
|
self.code_lines, updates = self.template_file_creator.load(self.template_file_creator.temp_file_path,
|
492
|
-
|
493
|
-
|
491
|
+
self.template_file_creator.func_name,
|
492
|
+
self.template_file_creator.print_func)
|
494
493
|
self.ipython_console.kernel.shell.user_ns.update(updates)
|
495
494
|
if self.code_lines is not None:
|
496
495
|
self.user_input = encapsulate_code_lines_into_a_function(
|
@@ -498,7 +497,22 @@ class RDRCaseViewer(QMainWindow):
|
|
498
497
|
self.template_file_creator.function_signature,
|
499
498
|
self.template_file_creator.func_doc, self.case_query)
|
500
499
|
self.case_query.scope.update(updates)
|
501
|
-
|
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)
|
502
516
|
|
503
517
|
def update_attribute_layout(self, obj, name: str):
|
504
518
|
# Clear the existing layout
|
@@ -547,7 +561,8 @@ class RDRCaseViewer(QMainWindow):
|
|
547
561
|
attr = f"{attr}"
|
548
562
|
try:
|
549
563
|
if is_iterable(value) or hasattr(value, "__dict__") and not inspect.isfunction(value):
|
550
|
-
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}")
|
551
566
|
else:
|
552
567
|
self.add_non_collapsible(attr, value, layout)
|
553
568
|
except Exception as e:
|
@@ -559,7 +574,7 @@ class RDRCaseViewer(QMainWindow):
|
|
559
574
|
type_name = type(value) if not isinstance(value, type) else value
|
560
575
|
collapsible = CollapsibleBox(
|
561
576
|
f'<b><span style="color:#FFA07A;">{attr}</span></b> {python_colored_repr(type_name)}', viewer=self,
|
562
|
-
|
577
|
+
chain_name=chain_name, main_obj=self.main_obj)
|
563
578
|
self.add_attributes(value, attr, collapsible.content_layout, current_depth, max_depth, chain_name=chain_name)
|
564
579
|
layout.addWidget(collapsible)
|
565
580
|
|
@@ -587,7 +602,6 @@ class IPythonConsole(RichJupyterWidget):
|
|
587
602
|
|
588
603
|
# Monkey patch its run_cell method
|
589
604
|
def custom_run_cell(this, raw_cell, **kwargs):
|
590
|
-
print(raw_cell)
|
591
605
|
if contains_return_statement(raw_cell) and 'def ' not in raw_cell:
|
592
606
|
if self.parent.template_file_creator and self.parent.template_file_creator.func_name in raw_cell:
|
593
607
|
self.command_log = self.parent.code_lines
|
@@ -601,7 +615,6 @@ class IPythonConsole(RichJupyterWidget):
|
|
601
615
|
|
602
616
|
original_run_cell = self.kernel.shell.run_cell
|
603
617
|
self.kernel.shell.run_cell = MethodType(custom_run_cell, self.kernel.shell)
|
604
|
-
|
605
618
|
self.kernel_client = self.kernel_manager.client()
|
606
619
|
self.kernel_client.start_channels()
|
607
620
|
|
@@ -630,7 +643,7 @@ class IPythonConsole(RichJupyterWidget):
|
|
630
643
|
.in-prompt { font-weight: bold; color: %(in_prompt_color)s }
|
631
644
|
.out-prompt-number { font-weight: bold; color: %(out_prompt_number_color)s }
|
632
645
|
.out-prompt { font-weight: bold; color: %(out_prompt_color)s }
|
633
|
-
'''%dict(
|
646
|
+
''' % dict(
|
634
647
|
bgcolor='#0b0d0b', fgcolor='#47d9cc', select="#555",
|
635
648
|
in_prompt_number_color='lime', in_prompt_color='lime',
|
636
649
|
out_prompt_number_color='red', out_prompt_color='red'
|
@@ -648,6 +661,12 @@ class IPythonConsole(RichJupyterWidget):
|
|
648
661
|
"""
|
649
662
|
self.kernel.shell.user_ns.update(namespace)
|
650
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
|
+
|
651
670
|
def execute(self, source=None, hidden=False, interactive=False):
|
652
671
|
# Log the command before execution
|
653
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,6 +21,7 @@ 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
|
25
27
|
|
@@ -34,6 +36,29 @@ class MyMagics(Magics):
|
|
34
36
|
self.rule_editor.print_func)
|
35
37
|
self.shell.user_ns.update(updates)
|
36
38
|
|
39
|
+
@line_magic
|
40
|
+
def current_value(self, line):
|
41
|
+
"""
|
42
|
+
Display the current value of the attribute of the case.
|
43
|
+
"""
|
44
|
+
if self.case_query is None:
|
45
|
+
print(f"{Fore.RED}No case query provided.{Style.RESET_ALL}")
|
46
|
+
return
|
47
|
+
print(self.case_query.current_value_str)
|
48
|
+
|
49
|
+
@line_magic
|
50
|
+
def show_rule_tree(self, line):
|
51
|
+
"""
|
52
|
+
Display the rule tree for the current case query.
|
53
|
+
"""
|
54
|
+
if self.case_query is None:
|
55
|
+
print(f"{Fore.RED}No case query provided.{Style.RESET_ALL}")
|
56
|
+
return
|
57
|
+
if self.case_query.rdr is None:
|
58
|
+
print(f"{Fore.RED}No RDR available for the current case query.{Style.RESET_ALL}")
|
59
|
+
return
|
60
|
+
self.case_query.render_rule_tree(view=True)
|
61
|
+
|
37
62
|
@line_magic
|
38
63
|
def help(self, line):
|
39
64
|
"""
|
@@ -42,12 +67,16 @@ class MyMagics(Magics):
|
|
42
67
|
help_text = f"""
|
43
68
|
Directly write python code in the shell, and then `{Fore.GREEN}return {Fore.RESET}output`. Or use
|
44
69
|
the magic commands to write the code in a temporary file and edit it in PyCharm:
|
45
|
-
{Fore.MAGENTA}
|
70
|
+
{Fore.MAGENTA}%edit{Style.RESET_ALL}
|
46
71
|
Opens a temporary file in PyCharm for editing a function (conclusion or conditions for case)
|
47
72
|
that will be executed on the case object.
|
48
|
-
{Fore.MAGENTA}
|
73
|
+
{Fore.MAGENTA}%load{Style.RESET_ALL}
|
49
74
|
Loads the function defined in the temporary file into the user namespace, that can then be used inside the
|
50
75
|
Ipython shell. You can then do `{Fore.GREEN}return {Fore.RESET}function_name(case)`.
|
76
|
+
{Fore.MAGENTA}%current_value{Style.RESET_ALL}
|
77
|
+
Shows the current value of the case attribute on which the rule are being fit.
|
78
|
+
{Fore.MAGENTA}%show_rule_tree{Style.RESET_ALL}
|
79
|
+
Displays the rule tree for the current case query.
|
51
80
|
"""
|
52
81
|
print(help_text)
|
53
82
|
|
@@ -144,7 +173,7 @@ class IPythonShell:
|
|
144
173
|
self.user_input = None
|
145
174
|
else:
|
146
175
|
self.user_input = encapsulate_code_lines_into_a_function(self.all_code_lines,
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
176
|
+
function_name=self.shell.my_magics.rule_editor.func_name,
|
177
|
+
function_signature=self.shell.my_magics.rule_editor.function_signature,
|
178
|
+
func_doc=self.shell.my_magics.rule_editor.func_doc,
|
179
|
+
case_query=self.case_query)
|