ripple-down-rules 0.6.28__py3-none-any.whl → 0.6.29__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.6.28"
1
+ __version__ = "0.6.29"
2
2
 
3
3
  import logging
4
4
  logger = logging.Logger("rdr")
@@ -120,7 +120,9 @@ class CallableExpression(SubclassJSONSerializer):
120
120
  self.user_defined_name = user_input.split('(')[0].replace('def ', '')
121
121
  else:
122
122
  self.user_defined_name = user_input
123
- self._user_input: str = encapsulate_user_input(user_input, self.get_encapsulating_function())
123
+ if f"def {self.encapsulating_function_name}" not in user_input:
124
+ user_input = encapsulate_user_input(user_input, self.get_encapsulating_function())
125
+ self._user_input: str = user_input
124
126
  if conclusion_type is not None:
125
127
  if is_iterable(conclusion_type):
126
128
  conclusion_type = tuple(conclusion_type)
@@ -308,7 +310,7 @@ def parse_string_to_expression(expression_str: str) -> AST:
308
310
  :param expression_str: The string which will be parsed.
309
311
  :return: The parsed expression.
310
312
  """
311
- if not expression_str.startswith(CallableExpression.get_encapsulating_function()):
313
+ if not expression_str.startswith(f"def {CallableExpression.encapsulating_function_name}"):
312
314
  expression_str = encapsulate_user_input(expression_str, CallableExpression.get_encapsulating_function())
313
315
  mode = 'exec' if expression_str.startswith('def') else 'eval'
314
316
  tree = ast.parse(expression_str, mode=mode)
@@ -6,6 +6,8 @@ import logging
6
6
  import os
7
7
  import uuid
8
8
  from abc import ABC, abstractmethod
9
+ from textwrap import dedent, indent
10
+ from typing import Tuple, Dict
9
11
 
10
12
  from typing_extensions import Optional, TYPE_CHECKING, List
11
13
 
@@ -13,6 +15,7 @@ from .datastructures.callable_expression import CallableExpression
13
15
  from .datastructures.enums import PromptFor
14
16
  from .datastructures.dataclasses import CaseQuery
15
17
  from .datastructures.case import show_current_and_corner_cases
18
+ from .user_interface.template_file_creator import TemplateFileCreator
16
19
  from .utils import extract_imports, extract_function_source, get_imports_from_scope, encapsulate_user_input
17
20
 
18
21
  try:
@@ -46,14 +49,12 @@ class Expert(ABC):
46
49
  answers_save_path: Optional[str] = None):
47
50
  self.all_expert_answers = []
48
51
  self.use_loaded_answers = use_loaded_answers
49
- self.append = append
52
+ self.append = True
50
53
  self.answers_save_path = answers_save_path
51
54
  if answers_save_path is not None and os.path.exists(answers_save_path + '.py'):
52
55
  if use_loaded_answers:
53
56
  self.load_answers(answers_save_path)
54
- else:
55
- os.remove(answers_save_path + '.py')
56
- self.append = True
57
+ os.remove(answers_save_path + '.py')
57
58
 
58
59
  @abstractmethod
59
60
  def ask_for_conditions(self, case_query: CaseQuery, last_evaluated_rule: Optional[Rule] = None) \
@@ -89,28 +90,26 @@ class Expert(ABC):
89
90
  "answers_save_path attribute.")
90
91
  if path is None:
91
92
  path = self.answers_save_path
92
- if os.path.exists(path + '.json'):
93
- os.remove(path + '.json')
94
93
  if os.path.exists(path + '.py'):
95
94
  os.remove(path + '.py')
96
95
  self.all_expert_answers = []
97
96
 
98
- def save_answers(self, path: Optional[str] = None):
97
+ def save_answers(self, path: Optional[str] = None, expert_answers: Optional[List[Tuple[Dict, str]]] = None):
99
98
  """
100
99
  Save the expert answers to a file.
101
100
 
102
101
  :param path: The path to save the answers to.
102
+ :param expert_answers: The expert answers to save.
103
103
  """
104
+ expert_answers = expert_answers if expert_answers else self.all_expert_answers
105
+ if not any(expert_answers):
106
+ return
104
107
  if path is None and self.answers_save_path is None:
105
108
  raise ValueError("No path provided to save expert answers, either provide a path or set the "
106
109
  "answers_save_path attribute.")
107
110
  if path is None:
108
111
  path = self.answers_save_path
109
- is_json = os.path.exists(path + '.json')
110
- if is_json:
111
- self._save_to_json(path)
112
- else:
113
- self._save_to_python(path)
112
+ self._save_to_python(path, expert_answers=expert_answers)
114
113
 
115
114
  def _save_to_json(self, path: str):
116
115
  """
@@ -127,12 +126,14 @@ class Expert(ABC):
127
126
  with open(path + '.json', "w") as f:
128
127
  json.dump(all_answers, f)
129
128
 
130
- def _save_to_python(self, path: str):
129
+ def _save_to_python(self, path: str, expert_answers: Optional[List[Tuple[Dict, str]]] = None):
131
130
  """
132
131
  Save the expert answers to a Python file.
133
132
 
134
133
  :param path: The path to save the answers to.
134
+ :param expert_answers: The expert answers to save.
135
135
  """
136
+ expert_answers = expert_answers if expert_answers else self.all_expert_answers
136
137
  dir_name = os.path.dirname(path)
137
138
  if not os.path.exists(dir_name + '/__init__.py'):
138
139
  os.makedirs(dir_name, exist_ok=True)
@@ -145,18 +146,13 @@ class Expert(ABC):
145
146
  current_file_data = f.read()
146
147
  action = 'a' if self.append and current_file_data is not None else 'w'
147
148
  with open(path + '.py', action) as f:
148
- for scope, func_source in self.all_expert_answers:
149
+ for scope, func_source in expert_answers:
149
150
  if len(scope) > 0:
150
151
  imports = '\n'.join(get_imports_from_scope(scope)) + '\n\n\n'
151
152
  else:
152
153
  imports = ''
153
- if func_source is not None:
154
- uid = uuid.uuid4().hex
155
- func_source = encapsulate_user_input(func_source, CallableExpression.get_encapsulating_function(f'_{uid}'))
156
- else:
154
+ if func_source is None:
157
155
  func_source = 'pass # No user input provided for this case.\n'
158
- if current_file_data is not None and func_source[1:] in current_file_data:
159
- continue
160
156
  f.write(imports + func_source + '\n' + '\n\n\n\'===New Answer===\'\n\n\n')
161
157
 
162
158
  def load_answers(self, path: Optional[str] = None):
@@ -170,11 +166,10 @@ class Expert(ABC):
170
166
  "answers_save_path attribute.")
171
167
  if path is None:
172
168
  path = self.answers_save_path
173
- is_json = os.path.exists(path + '.json')
174
- if is_json:
175
- self._load_answers_from_json(path)
176
- elif os.path.exists(path + '.py'):
169
+ if os.path.exists(path + '.py'):
177
170
  self._load_answers_from_python(path)
171
+ elif os.path.exists(path + '.json'):
172
+ self._load_answers_from_json(path)
178
173
 
179
174
  def _load_answers_from_json(self, path: str):
180
175
  """
@@ -195,15 +190,15 @@ class Expert(ABC):
195
190
  file_path = path + '.py'
196
191
  with open(file_path, "r") as f:
197
192
  all_answers = f.read().split('\n\n\n\'===New Answer===\'\n\n\n')[:-1]
198
- all_function_sources = list(extract_function_source(file_path, []).values())
199
- all_function_sources_names = list(extract_function_source(file_path, []).keys())
193
+ all_function_sources = extract_function_source(file_path, [], as_list=True)
200
194
  for i, answer in enumerate(all_answers):
201
195
  answer = answer.strip('\n').strip()
202
196
  if 'def ' not in answer and 'pass' in answer:
203
197
  self.all_expert_answers.append(({}, None))
204
198
  continue
205
199
  scope = extract_imports(tree=ast.parse(answer))
206
- function_source = all_function_sources[i].replace(all_function_sources_names[i],
200
+ func_name = all_function_sources[i].split('def ')[1].split('(')[0]
201
+ function_source = all_function_sources[i].replace(func_name,
207
202
  CallableExpression.encapsulating_function_name)
208
203
  self.all_expert_answers.append((scope, function_source))
209
204
 
@@ -249,6 +244,8 @@ class Human(Expert):
249
244
  if user_input is not None:
250
245
  case_query.scope.update(loaded_scope)
251
246
  condition = CallableExpression(user_input, bool, scope=case_query.scope)
247
+ if self.answers_save_path is not None and not any(loaded_scope):
248
+ self.convert_json_answer_to_python_answer(case_query, user_input, condition, PromptFor.Conditions)
252
249
  else:
253
250
  user_input, condition = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conditions, prompt_str=data_to_show)
254
251
  if user_input == 'exit':
@@ -260,6 +257,20 @@ class Human(Expert):
260
257
  case_query.conditions = condition
261
258
  return condition
262
259
 
260
+ def convert_json_answer_to_python_answer(self, case_query: CaseQuery, user_input: str,
261
+ callable_expression: CallableExpression,
262
+ prompt_for: PromptFor):
263
+ case_query.scope['case'] = case_query.case
264
+ tfc = TemplateFileCreator(case_query, prompt_for=prompt_for)
265
+ code = tfc.build_boilerplate_code()
266
+ if user_input.startswith('def'):
267
+ user_input = '\n'.join(user_input.split('\n')[1:])
268
+ user_input = indent(dedent(user_input), " " * 4).strip()
269
+ code = code.replace('pass', user_input)
270
+ else:
271
+ code = code.replace('pass', f"return {user_input}")
272
+ self.save_answers(expert_answers=[({}, code)])
273
+
263
274
  def ask_for_conclusion(self, case_query: CaseQuery) -> Optional[CallableExpression]:
264
275
  """
265
276
  Ask the expert to provide a conclusion for the case.
@@ -279,13 +290,17 @@ class Human(Expert):
279
290
  expression = CallableExpression(expert_input, case_query.attribute_type,
280
291
  scope=case_query.scope,
281
292
  mutually_exclusive=case_query.mutually_exclusive)
293
+ if self.answers_save_path is not None and not any(loaded_scope):
294
+ self.convert_json_answer_to_python_answer(case_query, expert_input, expression,
295
+ PromptFor.Conclusion)
282
296
  except IndexError:
283
297
  self.use_loaded_answers = False
284
298
  if not self.use_loaded_answers:
285
299
  data_to_show = None
286
300
  if self.user_prompt.viewer is None:
287
301
  data_to_show = show_current_and_corner_cases(case_query.case)
288
- expert_input, expression = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conclusion, prompt_str=data_to_show)
302
+ expert_input, expression = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conclusion,
303
+ prompt_str=data_to_show)
289
304
  if expert_input is None:
290
305
  self.all_expert_answers.append(({}, None))
291
306
  elif expert_input != 'exit':
ripple_down_rules/rdr.py CHANGED
@@ -230,7 +230,8 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
230
230
  num_rules: int = 0
231
231
  while not stop_iterating:
232
232
  for case_query in case_queries:
233
- pred_cat = self.fit_case(case_query, expert=expert, **kwargs_for_fit_case)
233
+ pred_cat = self.fit_case(case_query, expert=expert, clear_expert_answers=False,
234
+ **kwargs_for_fit_case)
234
235
  if case_query.target is None:
235
236
  continue
236
237
  target = {case_query.attribute_name: case_query.target(case_query.case)}
@@ -308,6 +309,7 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
308
309
  update_existing_rules: bool = True,
309
310
  scenario: Optional[Callable] = None,
310
311
  ask_now: Callable = lambda _: True,
312
+ clear_expert_answers: bool = True,
311
313
  **kwargs) \
312
314
  -> Union[CallableExpression, Dict[str, CallableExpression]]:
313
315
  """
@@ -319,7 +321,8 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
319
321
  :param update_existing_rules: Whether to update the existing same conclusion type rules that already gave
320
322
  some conclusions with the type required by the case query.
321
323
  :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.
324
+ :param ask_now: Whether to ask the expert for refinements or alternatives.
325
+ :param clear_expert_answers: Whether to clear expert answers after saving the new rule.
323
326
  :return: The category that the case belongs to.
324
327
  """
325
328
  if case_query is None:
@@ -348,7 +351,8 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
348
351
 
349
352
  if self.save_dir is not None:
350
353
  self.save()
351
- expert.clear_answers()
354
+ if clear_expert_answers:
355
+ expert.clear_answers()
352
356
 
353
357
  return fit_case_result
354
358
 
@@ -146,7 +146,8 @@ def extract_imports(file_path: Optional[str] = None, tree: Optional[ast.AST] = N
146
146
  def extract_function_source(file_path: str,
147
147
  function_names: List[str], join_lines: bool = True,
148
148
  return_line_numbers: bool = False,
149
- include_signature: bool = True) \
149
+ include_signature: bool = True,
150
+ as_list: bool = False) \
150
151
  -> Union[Dict[str, Union[str, List[str]]],
151
152
  Tuple[Dict[str, Union[str, List[str]]], Dict[str, Tuple[int, int]]]]:
152
153
  """
@@ -157,6 +158,8 @@ def extract_function_source(file_path: str,
157
158
  :param join_lines: Whether to join the lines of the function.
158
159
  :param return_line_numbers: Whether to return the line numbers of the function.
159
160
  :param include_signature: Whether to include the function signature in the source code.
161
+ :param as_list: Whether to return a list of function sources instead of dict (useful when there is multiple
162
+ functions with same name).
160
163
  :return: A dictionary mapping function names to their source code as a string if join_lines is True,
161
164
  otherwise as a list of strings.
162
165
  """
@@ -167,7 +170,9 @@ def extract_function_source(file_path: str,
167
170
  tree = ast.parse(source)
168
171
  function_names = make_list(function_names)
169
172
  functions_source: Dict[str, Union[str, List[str]]] = {}
173
+ functions_source_list: List[Union[str, List[str]]] = []
170
174
  line_numbers: Dict[str, Tuple[int, int]] = {}
175
+ line_numbers_list: List[Tuple[int, int]] = []
171
176
  for node in tree.body:
172
177
  if isinstance(node, ast.FunctionDef) and (node.name in function_names or len(function_names) == 0):
173
178
  # Get the line numbers of the function
@@ -175,16 +180,24 @@ def extract_function_source(file_path: str,
175
180
  func_lines = lines[node.lineno - 1:node.end_lineno]
176
181
  if not include_signature:
177
182
  func_lines = func_lines[1:]
178
- line_numbers[node.name] = (node.lineno, node.end_lineno)
179
- functions_source[node.name] = dedent("\n".join(func_lines)) if join_lines else func_lines
180
- if (len(functions_source) >= len(function_names)) and (not len(function_names) == 0):
181
- break
182
- if len(functions_source) < len(function_names):
183
+ if as_list:
184
+ line_numbers_list.append((node.lineno, node.end_lineno))
185
+ else:
186
+ line_numbers[node.name] = (node.lineno, node.end_lineno)
187
+ parsed_function = dedent("\n".join(func_lines)) if join_lines else func_lines
188
+ if as_list:
189
+ functions_source_list.append(parsed_function)
190
+ else:
191
+ functions_source[node.name] = parsed_function
192
+ if len(function_names) > 0:
193
+ if len(functions_source) >= len(function_names) or len(functions_source_list) >= len(function_names):
194
+ break
195
+ if len(functions_source) < len(function_names) and len(functions_source_list) < len(function_names):
183
196
  logger.warning(f"Could not find all functions in {file_path}: {function_names} not found, "
184
197
  f"functions not found: {set(function_names) - set(functions_source.keys())}")
185
198
  if return_line_numbers:
186
- return functions_source, line_numbers
187
- return functions_source
199
+ return functions_source if not as_list else functions_source_list, line_numbers if not as_list else line_numbers_list
200
+ return functions_source if not as_list else functions_source_list
188
201
 
189
202
 
190
203
  def encapsulate_user_input(user_input: str, func_signature: str, func_doc: Optional[str] = None) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.6.28
3
+ Version: 0.6.29
4
4
  Summary: Implements the various versions of Ripple Down Rules (RDR) for knowledge representation and reasoning.
5
5
  Author-email: Abdelrhman Bassiouny <abassiou@uni-bremen.de>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,13 +1,13 @@
1
- ripple_down_rules/__init__.py,sha256=XvJJjrncdmKjrvKVVILZq7uuTOYiohoP3iLBN_oKp_M,99
2
- ripple_down_rules/experts.py,sha256=bQfDB7RZE_CANVkpBMb9bi8ZWFwk5p7hn1AzjpaO5ow,12877
1
+ ripple_down_rules/__init__.py,sha256=RxW-6bq3pIBXNaQQI3F99LSjv47e0QeXHfOWv0IKvdM,99
2
+ ripple_down_rules/experts.py,sha256=irsHfjs_xXljB9g4aA29OB9kXh1q9skWQmYkld-DGvY,14184
3
3
  ripple_down_rules/helpers.py,sha256=X1psHOqrb4_xYN4ssQNB8S9aRKKsqgihAyWJurN0dqk,5499
4
- ripple_down_rules/rdr.py,sha256=hVqfCRrru6TUnqzY7yGMjiNnxUIFjfL2U7txmGNjeGI,61661
4
+ ripple_down_rules/rdr.py,sha256=Azd2w8otHrmlvXw-tql7M6iaVWNhjZsxivMQ_NmbxBk,61925
5
5
  ripple_down_rules/rdr_decorators.py,sha256=xoBGsIJMkJYUdsrsEaPZqoAsGuXkuVZAKCoP-xD2Iv8,11668
6
6
  ripple_down_rules/rules.py,sha256=N4dEx-xyqxGZpoEYzRd9P5u97_DcDEVLY_UiNhZ4E7g,28726
7
7
  ripple_down_rules/start-code-server.sh,sha256=otClk7VmDgBOX2TS_cjws6K0UwvgAUJhoA0ugkPCLqQ,949
8
- ripple_down_rules/utils.py,sha256=9xW0N2cB7X4taVANtLg-kVTPS-6ajWZylKkTqw2PKw4,73825
8
+ ripple_down_rules/utils.py,sha256=TUUNwNwxjPepOl-CiLQkFLe75NKmJR87l5A0U6RecJ0,74642
9
9
  ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
10
- ripple_down_rules/datastructures/callable_expression.py,sha256=P3o-z54Jt4rtIczeFWiuHFTNqMzYEOm94OyOP535D6Q,13378
10
+ ripple_down_rules/datastructures/callable_expression.py,sha256=IrlnufVsKrUDLVkc2owoFQ05oSOby3HiGuNXoFVj4Dw,13494
11
11
  ripple_down_rules/datastructures/case.py,sha256=PJ7_-AdxYic6BO5z816piFODj6nU5J6Jt1YzTFH-dds,15510
12
12
  ripple_down_rules/datastructures/dataclasses.py,sha256=kI3Kv8GiVR8igMgA_BlKN6djUYxC2mLecvyh19pqQQA,10998
13
13
  ripple_down_rules/datastructures/enums.py,sha256=CvcROl8fE7A6uTbMfs2lLpyxwS_ZFtFcQlBDDKFfoHc,6059
@@ -17,8 +17,8 @@ ripple_down_rules/user_interface/ipython_custom_shell.py,sha256=yp-F8YRWGhj1PLB3
17
17
  ripple_down_rules/user_interface/object_diagram.py,sha256=FEa2HaYR9QmTE6NsOwBvZ0jqmu3DKyg6mig2VE5ZP4Y,4956
18
18
  ripple_down_rules/user_interface/prompt.py,sha256=nLIAviClSmVCY80vQgTazDPs4a1AYmNQmT7sksLDJpE,9449
19
19
  ripple_down_rules/user_interface/template_file_creator.py,sha256=kwBbFLyN6Yx2NTIHPSwOoytWgbJDYhgrUOVFw_jkDQ4,13522
20
- ripple_down_rules-0.6.28.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
21
- ripple_down_rules-0.6.28.dist-info/METADATA,sha256=fPfcVvFD7eEuG5FKX26HRRWPJkZatA0mfWdarnTqPa8,48294
22
- ripple_down_rules-0.6.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- ripple_down_rules-0.6.28.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
24
- ripple_down_rules-0.6.28.dist-info/RECORD,,
20
+ ripple_down_rules-0.6.29.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
21
+ ripple_down_rules-0.6.29.dist-info/METADATA,sha256=ntefL7_Z_J2ncA_Jfo0qBt9cO5_0ax2JJM4Os1ucM98,48294
22
+ ripple_down_rules-0.6.29.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ ripple_down_rules-0.6.29.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
24
+ ripple_down_rules-0.6.29.dist-info/RECORD,,