ripple-down-rules 0.4.8__py3-none-any.whl → 0.4.9__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,5 @@
1
+ __version__ = "0.4.9"
2
+
3
+ import logging
4
+ logger = logging.Logger("rdr")
5
+ logger.setLevel(logging.INFO)
@@ -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
  """
@@ -7,7 +7,7 @@ from enum import Enum
7
7
 
8
8
  from pandas import DataFrame
9
9
  from sqlalchemy import MetaData
10
- from sqlalchemy.orm import DeclarativeBase as SQLTable, MappedColumn as SQLColumn, registry
10
+ from sqlalchemy.orm import DeclarativeBase as SQLTable, registry
11
11
  from typing_extensions import Any, Optional, Dict, Type, Set, Hashable, Union, List, TYPE_CHECKING
12
12
 
13
13
  from ..utils import make_set, row_to_dict, table_rows_as_str, get_value_type_from_type_hint, SubclassJSONSerializer, \
@@ -354,11 +354,13 @@ def show_current_and_corner_cases(case: Any, targets: Optional[Dict[str, Any]] =
354
354
  if last_evaluated_rule and last_evaluated_rule.fired:
355
355
  corner_row_dict = copy_case(corner_case)
356
356
 
357
+ case_dict.update(targets)
358
+ case_dict.update(current_conclusions)
359
+ all_table_rows = [case_dict]
357
360
  if corner_row_dict:
358
361
  corner_conclusion = last_evaluated_rule.conclusion(case)
359
362
  corner_row_dict.update({corner_conclusion.__class__.__name__: corner_conclusion})
360
- print(table_rows_as_str(corner_row_dict))
361
- print("=" * 50)
362
- case_dict.update(targets)
363
- case_dict.update(current_conclusions)
364
- print(table_rows_as_str(case_dict))
363
+ all_table_rows.append(corner_row_dict)
364
+ # print(table_rows_as_str(corner_row_dict))
365
+ print("\n" + "=" * 50)
366
+ print(table_rows_as_str(all_table_rows))
@@ -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,6 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import ast
3
4
  import json
5
+ import logging
6
+ import os
4
7
  from abc import ABC, abstractmethod
5
8
 
6
9
  from typing_extensions import Optional, TYPE_CHECKING, List
@@ -9,7 +12,12 @@ from .datastructures.callable_expression import CallableExpression
9
12
  from .datastructures.enums import PromptFor
10
13
  from .datastructures.dataclasses import CaseQuery
11
14
  from .datastructures.case import show_current_and_corner_cases
12
- from .user_interface.gui import RDRCaseViewer
15
+ from .utils import extract_imports, extract_function_source, get_imports_from_scope, encapsulate_user_input
16
+
17
+ try:
18
+ from .user_interface.gui import RDRCaseViewer
19
+ except ImportError as e:
20
+ RDRCaseViewer = None
13
21
  from .user_interface.prompt import UserPrompt
14
22
 
15
23
  if TYPE_CHECKING:
@@ -32,10 +40,19 @@ class Expert(ABC):
32
40
  A flag to indicate if the expert should use loaded answers or not.
33
41
  """
34
42
 
35
- 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):
36
46
  self.all_expert_answers = []
37
47
  self.use_loaded_answers = use_loaded_answers
38
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
39
56
 
40
57
  @abstractmethod
41
58
  def ask_for_conditions(self, case_query: CaseQuery, last_evaluated_rule: Optional[Rule] = None) \
@@ -59,46 +76,138 @@ class Expert(ABC):
59
76
  :return: A callable expression that can be called with a new case as an argument.
60
77
  """
61
78
 
79
+ def clear_answers(self, path: Optional[str] = None):
80
+ """
81
+ Clear the expert answers.
62
82
 
63
- class Human(Expert):
64
- """
65
- The Human Expert class, an expert that asks the human to provide differentiating features and conclusions.
66
- """
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 = []
67
96
 
68
- def __init__(self, use_loaded_answers: bool = False, append: bool = False, viewer: Optional[RDRCaseViewer] = None):
97
+ def save_answers(self, path: Optional[str] = None):
69
98
  """
70
- Initialize the Human expert.
99
+ Save the expert answers to a file.
71
100
 
72
- :param viewer: The RDRCaseViewer instance to use for prompting the user.
101
+ :param path: The path to save the answers to.
73
102
  """
74
- super().__init__(use_loaded_answers=use_loaded_answers, append=append)
75
- 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)
76
113
 
77
- def save_answers(self, path: str):
114
+ def _save_to_json(self, path: str):
78
115
  """
79
- Save the expert answers to a file.
116
+ Save the expert answers to a JSON file.
80
117
 
81
118
  :param path: The path to save the answers to.
82
119
  """
83
- if self.append:
120
+ all_answers = self.all_expert_answers
121
+ if self.append and os.path.exists(path + '.json'):
84
122
  # read the file and append the new answers
85
123
  with open(path + '.json', "r") as f:
86
- all_answers = json.load(f)
87
- all_answers.extend(self.all_expert_answers)
88
- with open(path + '.json', "w") as f:
89
- json.dump(all_answers, f)
90
- else:
91
- with open(path + '.json', "w") as f:
92
- 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)
93
128
 
94
- 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):
95
154
  """
96
155
  Load the expert answers from a file.
97
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
+
98
174
  :param path: The path to load the answers from.
99
175
  """
100
176
  with open(path + '.json', "r") as f:
101
- 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)
102
211
 
103
212
  def ask_for_conditions(self, case_query: CaseQuery,
104
213
  last_evaluated_rule: Optional[Rule] = None) \
@@ -121,13 +230,18 @@ class Human(Expert):
121
230
  if self.use_loaded_answers and len(self.all_expert_answers) == 0 and self.append:
122
231
  self.use_loaded_answers = False
123
232
  if self.use_loaded_answers:
124
- user_input = self.all_expert_answers.pop(0)
125
- 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:
126
238
  condition = CallableExpression(user_input, bool, scope=case_query.scope)
127
239
  else:
128
240
  user_input, condition = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conditions)
129
241
  if not self.use_loaded_answers:
130
- 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()
131
245
  case_query.conditions = condition
132
246
  return condition
133
247
 
@@ -139,18 +253,65 @@ class Human(Expert):
139
253
  :return: The conclusion for the case as a callable expression.
140
254
  """
141
255
  expression: Optional[CallableExpression] = None
256
+ expert_input: Optional[str] = None
142
257
  if self.use_loaded_answers and len(self.all_expert_answers) == 0 and self.append:
143
258
  self.use_loaded_answers = False
144
259
  if self.use_loaded_answers:
145
- expert_input = self.all_expert_answers.pop(0)
146
- if expert_input is not None:
147
- expression = CallableExpression(expert_input, case_query.attribute_type,
148
- scope=case_query.scope,
149
- mutually_exclusive=case_query.mutually_exclusive)
150
- 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:
151
269
  if self.user_prompt.viewer is None:
152
270
  show_current_and_corner_cases(case_query.case)
153
271
  expert_input, expression = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conclusion)
154
- 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()
155
278
  case_query.target = expression
156
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