ripple-down-rules 0.4.88__py3-none-any.whl → 0.5.1__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 +20 -1
- ripple_down_rules/datastructures/dataclasses.py +9 -1
- ripple_down_rules/experts.py +189 -32
- ripple_down_rules/rdr.py +162 -76
- ripple_down_rules/rdr_decorators.py +73 -52
- ripple_down_rules/rules.py +5 -4
- ripple_down_rules/user_interface/template_file_creator.py +6 -6
- ripple_down_rules/utils.py +3 -6
- {ripple_down_rules-0.4.88.dist-info → ripple_down_rules-0.5.1.dist-info}/METADATA +3 -2
- ripple_down_rules-0.5.1.dist-info/RECORD +25 -0
- ripple_down_rules/datasets.py +0 -222
- ripple_down_rules-0.4.88.dist-info/RECORD +0 -26
- {ripple_down_rules-0.4.88.dist-info → ripple_down_rules-0.5.1.dist-info}/WHEEL +0 -0
- {ripple_down_rules-0.4.88.dist-info → ripple_down_rules-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {ripple_down_rules-0.4.88.dist-info → ripple_down_rules-0.5.1.dist-info}/top_level.txt +0 -0
ripple_down_rules/__init__.py
CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import ast
|
4
4
|
import logging
|
5
|
+
import os
|
5
6
|
from _ast import AST
|
6
7
|
from enum import Enum
|
7
8
|
|
@@ -10,7 +11,7 @@ from typing_extensions import Type, Optional, Any, List, Union, Tuple, Dict, Set
|
|
10
11
|
from .case import create_case, Case
|
11
12
|
from ..utils import SubclassJSONSerializer, get_full_class_name, get_type_from_string, conclusion_to_json, is_iterable, \
|
12
13
|
build_user_input_from_conclusion, encapsulate_user_input, extract_function_source, are_results_subclass_of_types, \
|
13
|
-
make_list
|
14
|
+
make_list, get_imports_from_scope
|
14
15
|
|
15
16
|
|
16
17
|
class VariableVisitor(ast.NodeVisitor):
|
@@ -175,6 +176,24 @@ class CallableExpression(SubclassJSONSerializer):
|
|
175
176
|
return
|
176
177
|
self.user_input = self.encapsulating_function + '\n' + new_function_body
|
177
178
|
|
179
|
+
def write_to_python_file(self, file_path: str, append: bool = False):
|
180
|
+
"""
|
181
|
+
Write the callable expression to a python file.
|
182
|
+
|
183
|
+
:param file_path: The path to the file where the callable expression will be written.
|
184
|
+
:param append: If True, the callable expression will be appended to the file. If False,
|
185
|
+
the file will be overwritten.
|
186
|
+
"""
|
187
|
+
imports = '\n'.join(get_imports_from_scope(self.scope))
|
188
|
+
if append and os.path.exists(file_path):
|
189
|
+
with open(file_path, 'a') as f:
|
190
|
+
f.write('\n\n\n' + imports + '\n\n\n')
|
191
|
+
f.write(self.user_input)
|
192
|
+
else:
|
193
|
+
with open(file_path, 'w') as f:
|
194
|
+
f.write(imports + '\n\n\n')
|
195
|
+
f.write(self.user_input)
|
196
|
+
|
178
197
|
@property
|
179
198
|
def user_input(self):
|
180
199
|
"""
|
@@ -78,7 +78,15 @@ class CaseQuery:
|
|
78
78
|
"""
|
79
79
|
:return: The type of the case that the attribute belongs to.
|
80
80
|
"""
|
81
|
-
|
81
|
+
if self.is_function:
|
82
|
+
if self.function_args_type_hints is not None:
|
83
|
+
func_args = [arg for name, arg in self.function_args_type_hints.items() if name != 'return']
|
84
|
+
case_type_args = Union[tuple(func_args)]
|
85
|
+
else:
|
86
|
+
case_type_args = Any
|
87
|
+
return Dict[str, case_type_args]
|
88
|
+
else:
|
89
|
+
return self.original_case._obj_type if isinstance(self.original_case, Case) else type(self.original_case)
|
82
90
|
|
83
91
|
@property
|
84
92
|
def case(self) -> Any:
|
ripple_down_rules/experts.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import ast
|
3
4
|
import json
|
4
5
|
import logging
|
6
|
+
import os
|
5
7
|
from abc import ABC, abstractmethod
|
6
8
|
|
7
9
|
from typing_extensions import Optional, TYPE_CHECKING, List
|
@@ -10,6 +12,8 @@ from .datastructures.callable_expression import CallableExpression
|
|
10
12
|
from .datastructures.enums import PromptFor
|
11
13
|
from .datastructures.dataclasses import CaseQuery
|
12
14
|
from .datastructures.case import show_current_and_corner_cases
|
15
|
+
from .utils import extract_imports, extract_function_source, get_imports_from_scope, encapsulate_user_input
|
16
|
+
|
13
17
|
try:
|
14
18
|
from .user_interface.gui import RDRCaseViewer
|
15
19
|
except ImportError as e:
|
@@ -36,10 +40,19 @@ class Expert(ABC):
|
|
36
40
|
A flag to indicate if the expert should use loaded answers or not.
|
37
41
|
"""
|
38
42
|
|
39
|
-
def __init__(self, use_loaded_answers: bool =
|
43
|
+
def __init__(self, use_loaded_answers: bool = True,
|
44
|
+
append: bool = False,
|
45
|
+
answers_save_path: Optional[str] = None):
|
40
46
|
self.all_expert_answers = []
|
41
47
|
self.use_loaded_answers = use_loaded_answers
|
42
48
|
self.append = append
|
49
|
+
self.answers_save_path = answers_save_path
|
50
|
+
if answers_save_path is not None:
|
51
|
+
if use_loaded_answers:
|
52
|
+
self.load_answers(answers_save_path)
|
53
|
+
else:
|
54
|
+
os.remove(answers_save_path + '.py')
|
55
|
+
self.append = True
|
43
56
|
|
44
57
|
@abstractmethod
|
45
58
|
def ask_for_conditions(self, case_query: CaseQuery, last_evaluated_rule: Optional[Rule] = None) \
|
@@ -63,46 +76,138 @@ class Expert(ABC):
|
|
63
76
|
:return: A callable expression that can be called with a new case as an argument.
|
64
77
|
"""
|
65
78
|
|
79
|
+
def clear_answers(self, path: Optional[str] = None):
|
80
|
+
"""
|
81
|
+
Clear the expert answers.
|
66
82
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
83
|
+
:param path: The path to clear the answers from. If None, the answers will be cleared from the
|
84
|
+
answers_save_path attribute.
|
85
|
+
"""
|
86
|
+
if path is None and self.answers_save_path is None:
|
87
|
+
raise ValueError("No path provided to clear expert answers, either provide a path or set the "
|
88
|
+
"answers_save_path attribute.")
|
89
|
+
if path is None:
|
90
|
+
path = self.answers_save_path
|
91
|
+
if os.path.exists(path + '.json'):
|
92
|
+
os.remove(path + '.json')
|
93
|
+
if os.path.exists(path + '.py'):
|
94
|
+
os.remove(path + '.py')
|
95
|
+
self.all_expert_answers = []
|
71
96
|
|
72
|
-
def
|
97
|
+
def save_answers(self, path: Optional[str] = None):
|
73
98
|
"""
|
74
|
-
|
99
|
+
Save the expert answers to a file.
|
75
100
|
|
76
|
-
:param
|
101
|
+
:param path: The path to save the answers to.
|
77
102
|
"""
|
78
|
-
|
79
|
-
|
103
|
+
if path is None and self.answers_save_path is None:
|
104
|
+
raise ValueError("No path provided to save expert answers, either provide a path or set the "
|
105
|
+
"answers_save_path attribute.")
|
106
|
+
if path is None:
|
107
|
+
path = self.answers_save_path
|
108
|
+
is_json = os.path.exists(path + '.json')
|
109
|
+
if is_json:
|
110
|
+
self._save_to_json(path)
|
111
|
+
else:
|
112
|
+
self._save_to_python(path)
|
80
113
|
|
81
|
-
def
|
114
|
+
def _save_to_json(self, path: str):
|
82
115
|
"""
|
83
|
-
Save the expert answers to a file.
|
116
|
+
Save the expert answers to a JSON file.
|
84
117
|
|
85
118
|
:param path: The path to save the answers to.
|
86
119
|
"""
|
87
|
-
|
120
|
+
all_answers = self.all_expert_answers
|
121
|
+
if self.append and os.path.exists(path + '.json'):
|
88
122
|
# read the file and append the new answers
|
89
123
|
with open(path + '.json', "r") as f:
|
90
|
-
|
91
|
-
all_answers
|
92
|
-
|
93
|
-
|
94
|
-
else:
|
95
|
-
with open(path + '.json', "w") as f:
|
96
|
-
json.dump(self.all_expert_answers, f)
|
124
|
+
old_answers = json.load(f)
|
125
|
+
all_answers = old_answers + all_answers
|
126
|
+
with open(path + '.json', "w") as f:
|
127
|
+
json.dump(all_answers, f)
|
97
128
|
|
98
|
-
def
|
129
|
+
def _save_to_python(self, path: str):
|
130
|
+
"""
|
131
|
+
Save the expert answers to a Python file.
|
132
|
+
|
133
|
+
:param path: The path to save the answers to.
|
134
|
+
"""
|
135
|
+
dir_name = os.path.dirname(path)
|
136
|
+
if not os.path.exists(dir_name + '/__init__.py'):
|
137
|
+
os.makedirs(dir_name, exist_ok=True)
|
138
|
+
with open(dir_name + '/__init__.py', 'w') as f:
|
139
|
+
f.write('# This is an empty init file to make the directory a package.\n')
|
140
|
+
action = 'w' if not self.append else 'a'
|
141
|
+
with open(path + '.py', action) as f:
|
142
|
+
for scope, func_source in self.all_expert_answers:
|
143
|
+
if len(scope) > 0:
|
144
|
+
imports = '\n'.join(get_imports_from_scope(scope)) + '\n\n\n'
|
145
|
+
else:
|
146
|
+
imports = ''
|
147
|
+
if func_source is not None:
|
148
|
+
func_source = encapsulate_user_input(func_source, CallableExpression.encapsulating_function)
|
149
|
+
else:
|
150
|
+
func_source = 'pass # No user input provided for this case.\n'
|
151
|
+
f.write(imports + func_source + '\n' + '\n\n\n\'===New Answer===\'\n\n\n')
|
152
|
+
|
153
|
+
def load_answers(self, path: Optional[str] = None):
|
99
154
|
"""
|
100
155
|
Load the expert answers from a file.
|
101
156
|
|
157
|
+
:param path: The path to load the answers from.
|
158
|
+
"""
|
159
|
+
if path is None and self.answers_save_path is None:
|
160
|
+
raise ValueError("No path provided to load expert answers from, either provide a path or set the "
|
161
|
+
"answers_save_path attribute.")
|
162
|
+
if path is None:
|
163
|
+
path = self.answers_save_path
|
164
|
+
is_json = os.path.exists(path + '.json')
|
165
|
+
if is_json:
|
166
|
+
self._load_answers_from_json(path)
|
167
|
+
elif os.path.exists(path + '.py'):
|
168
|
+
self._load_answers_from_python(path)
|
169
|
+
|
170
|
+
def _load_answers_from_json(self, path: str):
|
171
|
+
"""
|
172
|
+
Load the expert answers from a JSON file.
|
173
|
+
|
102
174
|
:param path: The path to load the answers from.
|
103
175
|
"""
|
104
176
|
with open(path + '.json', "r") as f:
|
105
|
-
|
177
|
+
all_answers = json.load(f)
|
178
|
+
self.all_expert_answers = [({}, answer) for answer in all_answers]
|
179
|
+
|
180
|
+
def _load_answers_from_python(self, path: str):
|
181
|
+
"""
|
182
|
+
Load the expert answers from a Python file.
|
183
|
+
|
184
|
+
:param path: The path to load the answers from.
|
185
|
+
"""
|
186
|
+
file_path = path + '.py'
|
187
|
+
with open(file_path, "r") as f:
|
188
|
+
all_answers = f.read().split('\n\n\n\'===New Answer===\'\n\n\n')
|
189
|
+
for answer in all_answers:
|
190
|
+
answer = answer.strip('\n').strip()
|
191
|
+
if 'def ' not in answer and 'pass' in answer:
|
192
|
+
self.all_expert_answers.append(({}, None))
|
193
|
+
scope = extract_imports(tree=ast.parse(answer))
|
194
|
+
func_source = list(extract_function_source(file_path, []).values())[0]
|
195
|
+
self.all_expert_answers.append((scope, func_source))
|
196
|
+
|
197
|
+
|
198
|
+
class Human(Expert):
|
199
|
+
"""
|
200
|
+
The Human Expert class, an expert that asks the human to provide differentiating features and conclusions.
|
201
|
+
"""
|
202
|
+
|
203
|
+
def __init__(self, viewer: Optional[RDRCaseViewer] = None, **kwargs):
|
204
|
+
"""
|
205
|
+
Initialize the Human expert.
|
206
|
+
|
207
|
+
:param viewer: The RDRCaseViewer instance to use for prompting the user.
|
208
|
+
"""
|
209
|
+
super().__init__(**kwargs)
|
210
|
+
self.user_prompt = UserPrompt(viewer)
|
106
211
|
|
107
212
|
def ask_for_conditions(self, case_query: CaseQuery,
|
108
213
|
last_evaluated_rule: Optional[Rule] = None) \
|
@@ -125,13 +230,18 @@ class Human(Expert):
|
|
125
230
|
if self.use_loaded_answers and len(self.all_expert_answers) == 0 and self.append:
|
126
231
|
self.use_loaded_answers = False
|
127
232
|
if self.use_loaded_answers:
|
128
|
-
|
129
|
-
|
233
|
+
try:
|
234
|
+
loaded_scope, user_input = self.all_expert_answers.pop(0)
|
235
|
+
except IndexError:
|
236
|
+
self.use_loaded_answers = False
|
237
|
+
if user_input is not None:
|
130
238
|
condition = CallableExpression(user_input, bool, scope=case_query.scope)
|
131
239
|
else:
|
132
240
|
user_input, condition = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conditions)
|
133
241
|
if not self.use_loaded_answers:
|
134
|
-
self.all_expert_answers.append(user_input)
|
242
|
+
self.all_expert_answers.append((condition.scope, user_input))
|
243
|
+
if self.answers_save_path is not None:
|
244
|
+
self.save_answers()
|
135
245
|
case_query.conditions = condition
|
136
246
|
return condition
|
137
247
|
|
@@ -143,18 +253,65 @@ class Human(Expert):
|
|
143
253
|
:return: The conclusion for the case as a callable expression.
|
144
254
|
"""
|
145
255
|
expression: Optional[CallableExpression] = None
|
256
|
+
expert_input: Optional[str] = None
|
146
257
|
if self.use_loaded_answers and len(self.all_expert_answers) == 0 and self.append:
|
147
258
|
self.use_loaded_answers = False
|
148
259
|
if self.use_loaded_answers:
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
260
|
+
try:
|
261
|
+
loaded_scope, expert_input = self.all_expert_answers.pop(0)
|
262
|
+
if expert_input is not None:
|
263
|
+
expression = CallableExpression(expert_input, case_query.attribute_type,
|
264
|
+
scope=case_query.scope,
|
265
|
+
mutually_exclusive=case_query.mutually_exclusive)
|
266
|
+
except IndexError:
|
267
|
+
self.use_loaded_answers = False
|
268
|
+
if not self.use_loaded_answers:
|
155
269
|
if self.user_prompt.viewer is None:
|
156
270
|
show_current_and_corner_cases(case_query.case)
|
157
271
|
expert_input, expression = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conclusion)
|
158
|
-
|
272
|
+
if expression is None:
|
273
|
+
self.all_expert_answers.append(({}, None))
|
274
|
+
else:
|
275
|
+
self.all_expert_answers.append((expression.scope, expert_input))
|
276
|
+
if self.answers_save_path is not None:
|
277
|
+
self.save_answers()
|
159
278
|
case_query.target = expression
|
160
279
|
return expression
|
280
|
+
|
281
|
+
|
282
|
+
class File(Expert):
|
283
|
+
"""
|
284
|
+
The File Expert class, an expert that reads the answers from a file.
|
285
|
+
This is used for testing purposes.
|
286
|
+
"""
|
287
|
+
|
288
|
+
def __init__(self, filename: str, **kwargs):
|
289
|
+
"""
|
290
|
+
Initialize the File expert.
|
291
|
+
|
292
|
+
:param filename: The path to the file containing the expert answers.
|
293
|
+
"""
|
294
|
+
super().__init__(**kwargs)
|
295
|
+
self.filename = filename
|
296
|
+
self.load_answers(filename)
|
297
|
+
|
298
|
+
def ask_for_conditions(self, case_query: CaseQuery,
|
299
|
+
last_evaluated_rule: Optional[Rule] = None) -> CallableExpression:
|
300
|
+
loaded_scope, user_input = self.all_expert_answers.pop(0)
|
301
|
+
if user_input:
|
302
|
+
condition = CallableExpression(user_input, bool, scope=case_query.scope)
|
303
|
+
else:
|
304
|
+
raise ValueError("No user input found in the expert answers file.")
|
305
|
+
case_query.conditions = condition
|
306
|
+
return condition
|
307
|
+
|
308
|
+
def ask_for_conclusion(self, case_query: CaseQuery) -> Optional[CallableExpression]:
|
309
|
+
loaded_scope, expert_input = self.all_expert_answers.pop(0)
|
310
|
+
if expert_input is not None:
|
311
|
+
expression = CallableExpression(expert_input, case_query.attribute_type,
|
312
|
+
scope=case_query.scope,
|
313
|
+
mutually_exclusive=case_query.mutually_exclusive)
|
314
|
+
else:
|
315
|
+
raise ValueError("No expert input found in the expert answers file.")
|
316
|
+
case_query.target = expression
|
317
|
+
return expression
|