ripple-down-rules 0.6.27__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,5 +1,5 @@
1
- __version__ = "0.6.27"
1
+ __version__ = "0.6.29"
2
2
 
3
3
  import logging
4
4
  logger = logging.Logger("rdr")
5
- logger.setLevel(logging.INFO)
5
+ logger.setLevel(logging.INFO)
@@ -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)
@@ -7,6 +7,19 @@ from typing_extensions import List, Dict, Any, Type
7
7
  from ripple_down_rules.utils import SubclassJSONSerializer
8
8
 
9
9
 
10
+ class ExitStatus(Enum):
11
+ """
12
+ Describes the status at exit of the user interface.
13
+ """
14
+ CLOSE = auto()
15
+ """
16
+ The user wants to stop the program.
17
+ """
18
+ SUCCESS = auto()
19
+ """
20
+ The user completed the task successfully.
21
+ """
22
+
10
23
  class InteractionMode(Enum):
11
24
  """
12
25
  The interaction mode of the RDR.
@@ -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
 
@@ -213,14 +208,12 @@ class Human(Expert):
213
208
  The Human Expert class, an expert that asks the human to provide differentiating features and conclusions.
214
209
  """
215
210
 
216
- def __init__(self, viewer: Optional[RDRCaseViewer] = None, **kwargs):
211
+ def __init__(self, **kwargs):
217
212
  """
218
213
  Initialize the Human expert.
219
-
220
- :param viewer: The RDRCaseViewer instance to use for prompting the user.
221
214
  """
222
215
  super().__init__(**kwargs)
223
- self.user_prompt = UserPrompt(viewer)
216
+ self.user_prompt = UserPrompt()
224
217
 
225
218
  def ask_for_conditions(self, case_query: CaseQuery,
226
219
  last_evaluated_rule: Optional[Rule] = None) \
@@ -251,6 +244,8 @@ class Human(Expert):
251
244
  if user_input is not None:
252
245
  case_query.scope.update(loaded_scope)
253
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)
254
249
  else:
255
250
  user_input, condition = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conditions, prompt_str=data_to_show)
256
251
  if user_input == 'exit':
@@ -262,6 +257,20 @@ class Human(Expert):
262
257
  case_query.conditions = condition
263
258
  return condition
264
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
+
265
274
  def ask_for_conclusion(self, case_query: CaseQuery) -> Optional[CallableExpression]:
266
275
  """
267
276
  Ask the expert to provide a conclusion for the case.
@@ -281,13 +290,17 @@ class Human(Expert):
281
290
  expression = CallableExpression(expert_input, case_query.attribute_type,
282
291
  scope=case_query.scope,
283
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)
284
296
  except IndexError:
285
297
  self.use_loaded_answers = False
286
298
  if not self.use_loaded_answers:
287
299
  data_to_show = None
288
300
  if self.user_prompt.viewer is None:
289
301
  data_to_show = show_current_and_corner_cases(case_query.case)
290
- 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)
291
304
  if expert_input is None:
292
305
  self.all_expert_answers.append(({}, None))
293
306
  elif expert_input != 'exit':
ripple_down_rules/rdr.py CHANGED
@@ -83,23 +83,18 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
83
83
  Whether the output of the classification of this rdr allows only one possible conclusion or not.
84
84
  """
85
85
 
86
- def __init__(self, start_rule: Optional[Rule] = None, viewer: Optional[RDRCaseViewer] = None,
86
+ def __init__(self, start_rule: Optional[Rule] = None,
87
87
  save_dir: Optional[str] = None, model_name: Optional[str] = None):
88
88
  """
89
89
  :param start_rule: The starting rule for the classifier.
90
- :param viewer: The viewer gui to use for the classifier. If None, no viewer is used.
91
90
  :param save_dir: The directory to save the classifier to.
92
91
  """
93
92
  self.model_name: Optional[str] = model_name
94
93
  self.save_dir = save_dir
95
94
  self.start_rule = start_rule
96
95
  self.fig: Optional[Figure] = None
97
- self.viewer: Optional[RDRCaseViewer] = viewer
98
- if viewer is None and RDRCaseViewer is not None:
99
- if len(RDRCaseViewer.instances) > 0:
100
- self.viewer = RDRCaseViewer.instances[0]
101
- logger.error("No viewer was provided, but there is already an existing viewer. "
102
- "Using the existing viewer.")
96
+ self.viewer: Optional[RDRCaseViewer] = RDRCaseViewer.instances[0]\
97
+ if RDRCaseViewer and any(RDRCaseViewer.instances) else None
103
98
  self.input_node: Optional[Rule] = None
104
99
 
105
100
  @property
@@ -107,10 +102,10 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
107
102
  return self._viewer
108
103
 
109
104
  @viewer.setter
110
- def viewer(self, value):
111
- self._viewer = value
112
- if value is not None:
113
- value.set_save_function(self.save)
105
+ def viewer(self, viewer):
106
+ self._viewer = viewer
107
+ if viewer:
108
+ viewer.set_save_function(self.save)
114
109
 
115
110
  def render_evaluated_rule_tree(self, filename: str, show_full_tree: bool = False) -> None:
116
111
  if show_full_tree:
@@ -235,7 +230,8 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
235
230
  num_rules: int = 0
236
231
  while not stop_iterating:
237
232
  for case_query in case_queries:
238
- 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)
239
235
  if case_query.target is None:
240
236
  continue
241
237
  target = {case_query.attribute_name: case_query.target(case_query.case)}
@@ -313,6 +309,7 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
313
309
  update_existing_rules: bool = True,
314
310
  scenario: Optional[Callable] = None,
315
311
  ask_now: Callable = lambda _: True,
312
+ clear_expert_answers: bool = True,
316
313
  **kwargs) \
317
314
  -> Union[CallableExpression, Dict[str, CallableExpression]]:
318
315
  """
@@ -324,7 +321,8 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
324
321
  :param update_existing_rules: Whether to update the existing same conclusion type rules that already gave
325
322
  some conclusions with the type required by the case query.
326
323
  :param scenario: The scenario at which the case was created, this is used to recreate the case if needed.
327
- :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.
328
326
  :return: The category that the case belongs to.
329
327
  """
330
328
  if case_query is None:
@@ -335,8 +333,7 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
335
333
  self.case_name = case_query.case_name if self.case_name is None else self.case_name
336
334
  case_query.scenario = scenario if case_query.scenario is None else case_query.scenario
337
335
 
338
- expert = expert or Human(viewer=self.viewer,
339
- answers_save_path=self.save_dir + '/expert_answers'
336
+ expert = expert or Human(answers_save_path=self.save_dir + '/expert_answers'
340
337
  if self.save_dir else None)
341
338
  if case_query.target is None:
342
339
  case_query_cp = copy(case_query)
@@ -354,7 +351,8 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
354
351
 
355
352
  if self.save_dir is not None:
356
353
  self.save()
357
- expert.clear_answers()
354
+ if clear_expert_answers:
355
+ expert.clear_answers()
358
356
 
359
357
  return fit_case_result
360
358
 
@@ -96,8 +96,7 @@ class RDRDecorator:
96
96
  if len(self.parsed_output_type) == 0:
97
97
  self.parsed_output_type = self.parse_output_type(func, self.output_type, *args)
98
98
  if self.expert is None:
99
- self.expert = Human(viewer=self.viewer,
100
- answers_save_path=self.rdr_models_dir + f'/{self.model_name}/expert_answers')
99
+ self.expert = Human(answers_save_path=self.rdr_models_dir + f'/{self.model_name}/expert_answers')
101
100
  case_query = self.create_case_query_from_method(func, func_output,
102
101
  self.parsed_output_type,
103
102
  self.mutual_exclusive,
@@ -21,7 +21,7 @@ except ImportError as e:
21
21
  from typing_extensions import Optional, Any, List, Dict, Callable
22
22
 
23
23
  from ..datastructures.dataclasses import CaseQuery
24
- from ..datastructures.enums import PromptFor
24
+ from ..datastructures.enums import PromptFor, ExitStatus
25
25
  from .template_file_creator import TemplateFileCreator
26
26
  from ..utils import is_iterable, contains_return_statement, encapsulate_code_lines_into_a_function
27
27
  from .object_diagram import generate_object_graph
@@ -283,11 +283,13 @@ class RDRCaseViewer(QMainWindow):
283
283
  attributes_widget: Optional[QWidget] = None
284
284
  save_function: Optional[Callable[str, str], None] = None
285
285
  instances: List[RDRCaseViewer] = []
286
+ exit_status: ExitStatus = ExitStatus.CLOSE
286
287
 
287
288
  def __init__(self, parent=None,
288
289
  save_dir: Optional[str] = None,
289
290
  save_model_name: Optional[str] = None):
290
291
  super().__init__(parent)
292
+ self.exit_status = ExitStatus.CLOSE
291
293
  self.instances.clear()
292
294
  self.instances.append(self)
293
295
  self.save_dir = save_dir
@@ -328,7 +330,7 @@ class RDRCaseViewer(QMainWindow):
328
330
 
329
331
  # Add both to main layout
330
332
  main_layout.addWidget(self.attributes_widget, stretch=1)
331
- main_layout.addWidget(middle_widget, stretch=1)
333
+ main_layout.addWidget(middle_widget, stretch=2)
332
334
  main_layout.addWidget(self.obj_diagram_viewer, stretch=2)
333
335
 
334
336
  def set_save_function(self, save_function: Callable[[str, str], None]) -> None:
@@ -474,6 +476,7 @@ class RDRCaseViewer(QMainWindow):
474
476
  return button_widget
475
477
 
476
478
  def _accept(self):
479
+ self.exit_status = ExitStatus.SUCCESS
477
480
  # close the window
478
481
  self.close()
479
482
 
@@ -4,7 +4,7 @@ from _ast import AST
4
4
 
5
5
  try:
6
6
  from PyQt6.QtWidgets import QApplication
7
- from .gui import RDRCaseViewer
7
+ from .gui import RDRCaseViewer, style
8
8
  except ImportError:
9
9
  QApplication = None
10
10
  RDRCaseViewer = None
@@ -17,7 +17,7 @@ from typing_extensions import Optional, Tuple
17
17
 
18
18
  from ..datastructures.callable_expression import CallableExpression, parse_string_to_expression
19
19
  from ..datastructures.dataclasses import CaseQuery
20
- from ..datastructures.enums import PromptFor
20
+ from ..datastructures.enums import PromptFor, ExitStatus
21
21
  from .ipython_custom_shell import IPythonShell
22
22
  from ..utils import make_list
23
23
  from threading import RLock
@@ -29,15 +29,12 @@ class UserPrompt:
29
29
  """
30
30
  shell_lock: RLock = RLock() # To ensure that only one thread can access the shell at a time
31
31
 
32
- def __init__(self, viewer: Optional[RDRCaseViewer] = None):
32
+ def __init__(self):
33
33
  """
34
34
  Initialize the UserPrompt class.
35
-
36
- :param viewer: The RDRCaseViewer instance to use for prompting the user.
37
35
  """
38
- self.viewer = viewer
39
- self.print_func = print if viewer is None else viewer.print
40
-
36
+ self.viewer = RDRCaseViewer.instances[0] if RDRCaseViewer and any(RDRCaseViewer.instances) else None
37
+ self.print_func = self.viewer.print if self.viewer else print
41
38
 
42
39
  def prompt_user_for_expression(self, case_query: CaseQuery, prompt_for: PromptFor, prompt_str: Optional[str] = None) \
43
40
  -> Tuple[Optional[str], Optional[CallableExpression]]:
@@ -99,19 +96,20 @@ class UserPrompt:
99
96
  :return: The user input, and the executable expression that was parsed from the user input.
100
97
  """
101
98
  self.print_func("Entered shell")
102
- prompt_str = f"{Fore.WHITE}{prompt_str}" if prompt_str is not None else ''
99
+ initial_prompt_str = f"{prompt_str}\n" if prompt_str is not None else ''
103
100
  if prompt_for == PromptFor.Conclusion:
104
- prompt_str += f"\n{Fore.MAGENTA}Give possible value(s) for:"
101
+ prompt_for_str = f"Give possible value(s) for:"
105
102
  else:
106
- prompt_str += f"\n{Fore.MAGENTA}Give conditions on when can the rule be evaluated for:"
103
+ prompt_for_str = f"Give conditions for:"
107
104
  case_query.scope.update({'case': case_query.case})
108
105
  shell = None
109
106
  if self.viewer is None:
107
+ prompt_str = f"{Fore.WHITE}{initial_prompt_str}{Fore.MAGENTA}{prompt_for_str}"
110
108
  prompt_str = self.construct_prompt_str_for_shell(case_query, prompt_for, prompt_str)
111
109
  shell = IPythonShell(header=prompt_str, prompt_for=prompt_for, case_query=case_query,
112
110
  code_to_modify=code_to_modify)
113
111
  else:
114
-
112
+ prompt_str = initial_prompt_str + prompt_for_str
115
113
  self.viewer.update_for_case_query(case_query, prompt_str,
116
114
  prompt_for=prompt_for, code_to_modify=code_to_modify)
117
115
  user_input, expression_tree = self.prompt_user_input_and_parse_to_expression(shell=shell)
@@ -176,6 +174,8 @@ class UserPrompt:
176
174
  """
177
175
  if self.viewer is None:
178
176
  shell = IPythonShell() if shell is None else shell
177
+ if not hasattr(shell.shell, "auto_match"):
178
+ shell.shell.auto_match = True # or True, depending on your preference
179
179
  shell.run()
180
180
  user_input = shell.user_input
181
181
  else:
@@ -184,5 +184,7 @@ class UserPrompt:
184
184
  raise RuntimeError("QApplication instance is None. Please run the application first.")
185
185
  self.viewer.show()
186
186
  app.exec()
187
+ if self.viewer.exit_status == ExitStatus.CLOSE:
188
+ exit(0)
187
189
  user_input = self.viewer.user_input
188
190
  return user_input
@@ -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.27
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
@@ -0,0 +1,24 @@
1
+ ripple_down_rules/__init__.py,sha256=RxW-6bq3pIBXNaQQI3F99LSjv47e0QeXHfOWv0IKvdM,99
2
+ ripple_down_rules/experts.py,sha256=irsHfjs_xXljB9g4aA29OB9kXh1q9skWQmYkld-DGvY,14184
3
+ ripple_down_rules/helpers.py,sha256=X1psHOqrb4_xYN4ssQNB8S9aRKKsqgihAyWJurN0dqk,5499
4
+ ripple_down_rules/rdr.py,sha256=Azd2w8otHrmlvXw-tql7M6iaVWNhjZsxivMQ_NmbxBk,61925
5
+ ripple_down_rules/rdr_decorators.py,sha256=xoBGsIJMkJYUdsrsEaPZqoAsGuXkuVZAKCoP-xD2Iv8,11668
6
+ ripple_down_rules/rules.py,sha256=N4dEx-xyqxGZpoEYzRd9P5u97_DcDEVLY_UiNhZ4E7g,28726
7
+ ripple_down_rules/start-code-server.sh,sha256=otClk7VmDgBOX2TS_cjws6K0UwvgAUJhoA0ugkPCLqQ,949
8
+ ripple_down_rules/utils.py,sha256=TUUNwNwxjPepOl-CiLQkFLe75NKmJR87l5A0U6RecJ0,74642
9
+ ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
10
+ ripple_down_rules/datastructures/callable_expression.py,sha256=IrlnufVsKrUDLVkc2owoFQ05oSOby3HiGuNXoFVj4Dw,13494
11
+ ripple_down_rules/datastructures/case.py,sha256=PJ7_-AdxYic6BO5z816piFODj6nU5J6Jt1YzTFH-dds,15510
12
+ ripple_down_rules/datastructures/dataclasses.py,sha256=kI3Kv8GiVR8igMgA_BlKN6djUYxC2mLecvyh19pqQQA,10998
13
+ ripple_down_rules/datastructures/enums.py,sha256=CvcROl8fE7A6uTbMfs2lLpyxwS_ZFtFcQlBDDKFfoHc,6059
14
+ ripple_down_rules/user_interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ ripple_down_rules/user_interface/gui.py,sha256=JCz6vA2kWVvRD6CupoUfMbtGz39cvUlmVnGnw_fY6OA,27635
16
+ ripple_down_rules/user_interface/ipython_custom_shell.py,sha256=yp-F8YRWGhj1PLB33HE6vJkdYWFN5Zn2244S2DUWRTM,6576
17
+ ripple_down_rules/user_interface/object_diagram.py,sha256=FEa2HaYR9QmTE6NsOwBvZ0jqmu3DKyg6mig2VE5ZP4Y,4956
18
+ ripple_down_rules/user_interface/prompt.py,sha256=nLIAviClSmVCY80vQgTazDPs4a1AYmNQmT7sksLDJpE,9449
19
+ ripple_down_rules/user_interface/template_file_creator.py,sha256=kwBbFLyN6Yx2NTIHPSwOoytWgbJDYhgrUOVFw_jkDQ4,13522
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,,
@@ -1,24 +0,0 @@
1
- ripple_down_rules/__init__.py,sha256=TaxvqRWwXyeD_ETYnf_G3Z5VlbdhClbNpLIff72WwGg,100
2
- ripple_down_rules/experts.py,sha256=MYK1-vuvU1Stp82YZa8TcwOzvriIiYb0WrPFpWUNnXc,13005
3
- ripple_down_rules/helpers.py,sha256=X1psHOqrb4_xYN4ssQNB8S9aRKKsqgihAyWJurN0dqk,5499
4
- ripple_down_rules/rdr.py,sha256=PaoNwbrwnZBjXXkIVO3_sz_MsNUBjfooxBs9RT3bb64,62081
5
- ripple_down_rules/rdr_decorators.py,sha256=TRhbaB_ZIXN0n8Up19NI43_mMjmTm24qo8axAAOzbxM,11728
6
- ripple_down_rules/rules.py,sha256=N4dEx-xyqxGZpoEYzRd9P5u97_DcDEVLY_UiNhZ4E7g,28726
7
- ripple_down_rules/start-code-server.sh,sha256=otClk7VmDgBOX2TS_cjws6K0UwvgAUJhoA0ugkPCLqQ,949
8
- ripple_down_rules/utils.py,sha256=9xW0N2cB7X4taVANtLg-kVTPS-6ajWZylKkTqw2PKw4,73825
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
11
- ripple_down_rules/datastructures/case.py,sha256=PJ7_-AdxYic6BO5z816piFODj6nU5J6Jt1YzTFH-dds,15510
12
- ripple_down_rules/datastructures/dataclasses.py,sha256=kI3Kv8GiVR8igMgA_BlKN6djUYxC2mLecvyh19pqQQA,10998
13
- ripple_down_rules/datastructures/enums.py,sha256=R9AkhMKTDErOSZ8J6gEdh2lQ0Bjsxs22eMBtCPrXosI,5804
14
- ripple_down_rules/user_interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- ripple_down_rules/user_interface/gui.py,sha256=druufu9cVeVUajPW-RqGW3ZiEbgdgNBQD2CLhadQo18,27486
16
- ripple_down_rules/user_interface/ipython_custom_shell.py,sha256=yp-F8YRWGhj1PLB33HE6vJkdYWFN5Zn2244S2DUWRTM,6576
17
- ripple_down_rules/user_interface/object_diagram.py,sha256=FEa2HaYR9QmTE6NsOwBvZ0jqmu3DKyg6mig2VE5ZP4Y,4956
18
- ripple_down_rules/user_interface/prompt.py,sha256=e5FzVfiIagwKTK3WCKsHvWaWZ4kb8FP8X-SgieTln6E,9156
19
- ripple_down_rules/user_interface/template_file_creator.py,sha256=kwBbFLyN6Yx2NTIHPSwOoytWgbJDYhgrUOVFw_jkDQ4,13522
20
- ripple_down_rules-0.6.27.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
21
- ripple_down_rules-0.6.27.dist-info/METADATA,sha256=L_eYP6eqnQnZaLw-7FHIV774Yj_Q1IcJ9HAkkwP9um4,48294
22
- ripple_down_rules-0.6.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- ripple_down_rules-0.6.27.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
24
- ripple_down_rules-0.6.27.dist-info/RECORD,,