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.
@@ -1,4 +1,4 @@
1
- __version__ = "0.4.88"
1
+ __version__ = "0.5.1"
2
2
 
3
3
  import logging
4
4
  logger = logging.Logger("rdr")
@@ -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
- return self.original_case._obj_type if isinstance(self.original_case, Case) else type(self.original_case)
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:
@@ -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 = False, append: bool = False):
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
- class Human(Expert):
68
- """
69
- The Human Expert class, an expert that asks the human to provide differentiating features and conclusions.
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 __init__(self, use_loaded_answers: bool = False, append: bool = False, viewer: Optional[RDRCaseViewer] = None):
97
+ def save_answers(self, path: Optional[str] = None):
73
98
  """
74
- Initialize the Human expert.
99
+ Save the expert answers to a file.
75
100
 
76
- :param viewer: The RDRCaseViewer instance to use for prompting the user.
101
+ :param path: The path to save the answers to.
77
102
  """
78
- super().__init__(use_loaded_answers=use_loaded_answers, append=append)
79
- self.user_prompt = UserPrompt(viewer)
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 save_answers(self, path: str):
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
- if self.append:
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
- all_answers = json.load(f)
91
- all_answers.extend(self.all_expert_answers)
92
- with open(path + '.json', "w") as f:
93
- json.dump(all_answers, f)
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 load_answers(self, path: str):
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
- self.all_expert_answers = json.load(f)
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
- user_input = self.all_expert_answers.pop(0)
129
- if user_input:
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
- expert_input = self.all_expert_answers.pop(0)
150
- if expert_input is not None:
151
- expression = CallableExpression(expert_input, case_query.attribute_type,
152
- scope=case_query.scope,
153
- mutually_exclusive=case_query.mutually_exclusive)
154
- else:
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
- self.all_expert_answers.append(expert_input)
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