ripple-down-rules 0.1.69__tar.gz → 0.2.1__tar.gz

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.
Files changed (33) hide show
  1. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/PKG-INFO +1 -1
  2. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/pyproject.toml +1 -1
  3. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/datastructures/callable_expression.py +41 -10
  4. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/datastructures/dataclasses.py +7 -0
  5. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/datastructures/enums.py +1 -1
  6. ripple_down_rules-0.2.1/src/ripple_down_rules/prompt.py +354 -0
  7. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/rdr.py +89 -71
  8. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/rules.py +38 -33
  9. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/utils.py +143 -28
  10. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules.egg-info/PKG-INFO +1 -1
  11. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/test/test_rdr.py +33 -2
  12. ripple_down_rules-0.1.69/src/ripple_down_rules/prompt.py +0 -161
  13. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/LICENSE +0 -0
  14. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/README.md +0 -0
  15. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/setup.cfg +0 -0
  16. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/__init__.py +0 -0
  17. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/datasets.py +0 -0
  18. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/datastructures/__init__.py +0 -0
  19. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/datastructures/case.py +0 -0
  20. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/experts.py +0 -0
  21. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/failures.py +0 -0
  22. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/helpers.py +0 -0
  23. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules/rdr_decorators.py +0 -0
  24. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules.egg-info/SOURCES.txt +0 -0
  25. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules.egg-info/dependency_links.txt +0 -0
  26. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/src/ripple_down_rules.egg-info/top_level.txt +0 -0
  27. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/test/test_json_serialization.py +0 -0
  28. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/test/test_on_mutagenic.py +0 -0
  29. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/test/test_rdr_alchemy.py +0 -0
  30. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/test/test_rdr_world.py +0 -0
  31. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/test/test_relational_rdr.py +0 -0
  32. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/test/test_relational_rdr_alchemy.py +0 -0
  33. {ripple_down_rules-0.1.69 → ripple_down_rules-0.2.1}/test/test_sql_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.1.69
3
+ Version: 0.2.1
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
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "ripple_down_rules"
9
- version = "0.1.69"
9
+ version = "0.2.1"
10
10
  description = "Implements the various versions of Ripple Down Rules (RDR) for knowledge representation and reasoning."
11
11
  readme = "README.md"
12
12
  authors = [{ name = "Abdelrhman Bassiouny", email = "abassiou@uni-bremen.de" }]
@@ -9,7 +9,8 @@ from typing_extensions import Type, Optional, Any, List, Union, Tuple, Dict, Set
9
9
 
10
10
  from .case import create_case, Case
11
11
  from ..utils import SubclassJSONSerializer, get_full_class_name, get_type_from_string, conclusion_to_json, is_iterable, \
12
- build_user_input_from_conclusion, encapsulate_user_input
12
+ build_user_input_from_conclusion, encapsulate_user_input, extract_function_source, are_results_subclass_of_types, \
13
+ make_list
13
14
 
14
15
 
15
16
  class VariableVisitor(ast.NodeVisitor):
@@ -109,7 +110,7 @@ class CallableExpression(SubclassJSONSerializer):
109
110
  if user_input is None:
110
111
  user_input = build_user_input_from_conclusion(conclusion)
111
112
  self.conclusion: Optional[Any] = conclusion
112
- self.user_input: str = encapsulate_user_input(user_input, self.encapsulating_function)
113
+ self._user_input: str = encapsulate_user_input(user_input, self.encapsulating_function)
113
114
  if conclusion_type is not None:
114
115
  if is_iterable(conclusion_type):
115
116
  conclusion_type = tuple(conclusion_type)
@@ -133,15 +134,16 @@ class CallableExpression(SubclassJSONSerializer):
133
134
  if output is None:
134
135
  output = scope['_get_value'](case)
135
136
  if self.conclusion_type is not None:
136
- if is_iterable(output) and not isinstance(output, self.conclusion_type):
137
- assert isinstance(list(output)[0], self.conclusion_type), (f"Expected output type {self.conclusion_type},"
138
- f" got {type(output)}")
139
- else:
140
- assert isinstance(output, self.conclusion_type), (f"Expected output type {self.conclusion_type},"
141
- f" got {type(output)}")
137
+ output_types = {type(o) for o in make_list(output)}
138
+ output_types.add(type(output))
139
+ if not are_results_subclass_of_types(output_types, self.conclusion_type):
140
+ raise ValueError(f"Not all result types {output_types} are subclasses of expected types"
141
+ f" {self.conclusion_type}")
142
142
  return output
143
- else:
143
+ elif self.conclusion is not None:
144
144
  return self.conclusion
145
+ else:
146
+ raise ValueError("Either user_input or conclusion must be provided.")
145
147
  except Exception as e:
146
148
  raise ValueError(f"Error during evaluation: {e}")
147
149
 
@@ -156,6 +158,35 @@ class CallableExpression(SubclassJSONSerializer):
156
158
  f"return _cond1(case) and _cond2(case)")
157
159
  return CallableExpression(new_user_input, conclusion_type=self.conclusion_type)
158
160
 
161
+ def update_user_input_from_file(self, file_path: str, function_name: str):
162
+ """
163
+ Update the user input from a file.
164
+ """
165
+ new_function_body = extract_function_source(file_path, [function_name])[function_name]
166
+ if new_function_body is None:
167
+ return
168
+ self.user_input = self.encapsulating_function + '\n' + new_function_body
169
+
170
+ @property
171
+ def user_input(self):
172
+ """
173
+ Get the user input.
174
+ """
175
+ return self._user_input
176
+
177
+ @user_input.setter
178
+ def user_input(self, value: str):
179
+ """
180
+ Set the user input.
181
+ """
182
+ if value is not None:
183
+ self._user_input = encapsulate_user_input(value, self.encapsulating_function)
184
+ self.scope = get_used_scope(self.user_input, self.scope)
185
+ self.expression_tree = parse_string_to_expression(self.user_input)
186
+ self.code = compile_expression_to_code(self.expression_tree)
187
+ self.visitor = VariableVisitor()
188
+ self.visitor.visit(self.expression_tree)
189
+
159
190
  def __eq__(self, other):
160
191
  """
161
192
  Check if two callable expressions are equal.
@@ -225,7 +256,7 @@ def parse_string_to_expression(expression_str: str) -> AST:
225
256
  :param expression_str: The string which will be parsed.
226
257
  :return: The parsed expression.
227
258
  """
228
- if not expression_str.startswith('def'):
259
+ if not expression_str.startswith(CallableExpression.encapsulating_function):
229
260
  expression_str = encapsulate_user_input(expression_str, CallableExpression.encapsulating_function)
230
261
  mode = 'exec' if expression_str.startswith('def') else 'eval'
231
262
  tree = ast.parse(expression_str, mode=mode)
@@ -83,6 +83,13 @@ class CaseQuery:
83
83
  raise ValueError("The case must be a Case or SQLTable object.")
84
84
  self._case = value
85
85
 
86
+ @property
87
+ def core_attribute_type(self) -> Tuple[Type]:
88
+ """
89
+ :return: The core type of the attribute.
90
+ """
91
+ return (t for t in self.attribute_type if t not in (set, list))
92
+
86
93
  @property
87
94
  def attribute_type(self) -> Tuple[Type]:
88
95
  """
@@ -59,7 +59,7 @@ class PromptFor(Enum):
59
59
  """
60
60
  Prompt for rule conditions about a case.
61
61
  """
62
- Conclusion: str = "conclusion"
62
+ Conclusion: str = "value"
63
63
  """
64
64
  Prompt for rule conclusion about a case.
65
65
  """
@@ -0,0 +1,354 @@
1
+ import ast
2
+ import logging
3
+ import os
4
+ import subprocess
5
+ import tempfile
6
+ from _ast import AST
7
+ from functools import cached_property
8
+ from textwrap import indent, dedent
9
+
10
+ from IPython.core.magic import register_line_magic, line_magic, Magics, magics_class
11
+ from IPython.terminal.embed import InteractiveShellEmbed
12
+ from traitlets.config import Config
13
+ from typing_extensions import List, Optional, Tuple, Dict, Type, Union, Any
14
+
15
+ from .datastructures.enums import PromptFor
16
+ from .datastructures.case import Case
17
+ from .datastructures.callable_expression import CallableExpression, parse_string_to_expression
18
+ from .datastructures.dataclasses import CaseQuery
19
+ from .utils import extract_dependencies, contains_return_statement, make_set, get_imports_from_scope, make_list, \
20
+ get_import_from_type, get_imports_from_types, is_iterable, extract_function_source, encapsulate_user_input, \
21
+ are_results_subclass_of_types
22
+
23
+
24
+ @magics_class
25
+ class MyMagics(Magics):
26
+ def __init__(self, shell, scope, output_type: Optional[Type] = None, func_name: str = "user_case",
27
+ func_doc: str = "User defined function to be executed on the case.",
28
+ code_to_modify: Optional[str] = None):
29
+ super().__init__(shell)
30
+ self.scope = scope
31
+ self.temp_file_path = None
32
+ self.func_name = func_name
33
+ self.func_doc = func_doc
34
+ self.code_to_modify = code_to_modify
35
+ self.output_type = make_list(output_type) if output_type is not None else None
36
+ self.user_edit_line = 0
37
+ self.function_signature: Optional[str] = None
38
+ self.build_function_signature()
39
+
40
+ @line_magic
41
+ def edit_case(self, line):
42
+
43
+ boilerplate_code = self.build_boilerplate_code()
44
+
45
+ self.write_to_file(boilerplate_code)
46
+
47
+ print(f"Opening {self.temp_file_path} in PyCharm...")
48
+ subprocess.Popen(["pycharm", "--line", str(self.user_edit_line), self.temp_file_path])
49
+
50
+ def build_boilerplate_code(self):
51
+ imports = self.get_imports()
52
+ self.build_function_signature()
53
+ if self.code_to_modify is not None:
54
+ body = indent(dedent(self.code_to_modify), ' ')
55
+ else:
56
+ body = " # Write your code here\n pass"
57
+ boilerplate = f"""{imports}\n\n{self.function_signature}\n \"\"\"{self.func_doc}\"\"\"\n{body}"""
58
+ self.user_edit_line = imports.count('\n')+6
59
+ return boilerplate
60
+
61
+ def build_function_signature(self):
62
+ if self.output_type is None:
63
+ output_type_hint = ""
64
+ elif len(self.output_type) == 1:
65
+ output_type_hint = f" -> {self.output_type[0].__name__}"
66
+ else:
67
+ output_type_hint = f" -> Union[{', '.join([t.__name__ for t in self.output_type])}]"
68
+ self.function_signature = f"def {self.func_name}(case: {self.case_type.__name__}){output_type_hint}:"
69
+
70
+ def write_to_file(self, code: str):
71
+ tmp = tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix=".py",
72
+ dir=os.path.dirname(self.scope['__file__']))
73
+ tmp.write(code)
74
+ tmp.flush()
75
+ self.temp_file_path = tmp.name
76
+ tmp.close()
77
+
78
+ def get_imports(self):
79
+ case_type_import = f"from {self.case_type.__module__} import {self.case_type.__name__}"
80
+ if self.output_type is None:
81
+ output_type_imports = [f"from typing_extensions import Any"]
82
+ else:
83
+ output_type_imports = get_imports_from_types(self.output_type)
84
+ if len(self.output_type) > 1:
85
+ output_type_imports.append("from typing_extensions import Union")
86
+ print(output_type_imports)
87
+ imports = get_imports_from_scope(self.scope)
88
+ imports = [i for i in imports if ("get_ipython" not in i)]
89
+ if case_type_import not in imports:
90
+ imports.append(case_type_import)
91
+ imports.extend([oti for oti in output_type_imports if oti not in imports])
92
+ imports = set(imports)
93
+ return '\n'.join(imports)
94
+
95
+ @cached_property
96
+ def case_type(self) -> Type:
97
+ """
98
+ Get the type of the case object in the current scope.
99
+
100
+ :return: The type of the case object.
101
+ """
102
+ case = self.scope['case']
103
+ return case._obj_type if isinstance(case, Case) else type(case)
104
+
105
+ @line_magic
106
+ def load_case(self, line):
107
+ if not self.temp_file_path:
108
+ print("No file to load. Run %edit_case first.")
109
+ return
110
+
111
+ with open(self.temp_file_path, 'r') as f:
112
+ source = f.read()
113
+
114
+ tree = ast.parse(source)
115
+ for node in tree.body:
116
+ if isinstance(node, ast.FunctionDef) and node.name == self.func_name:
117
+ exec_globals = {}
118
+ exec(source, self.scope, exec_globals)
119
+ user_function = exec_globals[self.func_name]
120
+ self.shell.user_ns[self.func_name] = user_function
121
+ print(f"Loaded `{self.func_name}` function into user namespace.")
122
+ return
123
+
124
+ print(f"Function `{self.func_name}` not found.")
125
+
126
+
127
+ class CustomInteractiveShell(InteractiveShellEmbed):
128
+ def __init__(self, output_type: Union[Type, Tuple[Type], None] = None, func_name: Optional[str] = None,
129
+ func_doc: Optional[str] = None, code_to_modify: Optional[str] = None, **kwargs):
130
+ super().__init__(**kwargs)
131
+ keys = ['output_type', 'func_name', 'func_doc', 'code_to_modify']
132
+ values = [output_type, func_name, func_doc, code_to_modify]
133
+ magics_kwargs = {key: value for key, value in zip(keys, values) if value is not None}
134
+ self.my_magics = MyMagics(self, self.user_ns, **magics_kwargs)
135
+ self.register_magics(self.my_magics)
136
+ self.all_lines = []
137
+
138
+ def run_cell(self, raw_cell: str, **kwargs):
139
+ """
140
+ Override the run_cell method to capture return statements.
141
+ """
142
+ if contains_return_statement(raw_cell) and 'def ' not in raw_cell:
143
+ if self.my_magics.func_name in raw_cell:
144
+ self.all_lines = extract_function_source(self.my_magics.temp_file_path,
145
+ self.my_magics.func_name,
146
+ join_lines=False)[self.my_magics.func_name]
147
+ self.all_lines.append(raw_cell)
148
+ print("Exiting shell on `return` statement.")
149
+ self.history_manager.store_inputs(line_num=self.execution_count, source=raw_cell)
150
+ self.ask_exit()
151
+ return None
152
+ result = super().run_cell(raw_cell, **kwargs)
153
+ if result.error_in_exec is None and result.error_before_exec is None:
154
+ self.all_lines.append(raw_cell)
155
+ return result
156
+
157
+
158
+ class IPythonShell:
159
+ """
160
+ Create an embedded Ipython shell that can be used to prompt the user for input.
161
+ """
162
+
163
+ def __init__(self, scope: Optional[Dict] = None, header: Optional[str] = None,
164
+ output_type: Optional[Type] = None, prompt_for: Optional[PromptFor] = None,
165
+ attribute_name: Optional[str] = None, attribute_type: Optional[Type] = None,
166
+ code_to_modify: Optional[str] = None):
167
+ """
168
+ Initialize the Ipython shell with the given scope and header.
169
+
170
+ :param scope: The scope to use for the shell.
171
+ :param header: The header to display when the shell is started.
172
+ :param output_type: The type of the output from user input.
173
+ :param prompt_for: The type of information to ask the user about.
174
+ :param attribute_name: The name of the attribute of the case.
175
+ :param attribute_type: The type of the attribute of the case.
176
+ :param code_to_modify: The code to modify. If given, will be used as a start for user to modify.
177
+ """
178
+ self.scope: Dict = scope or {}
179
+ self.header: str = header or ">>> Embedded Ipython Shell"
180
+ self.output_type: Optional[Type] = output_type
181
+ self.prompt_for: Optional[PromptFor] = prompt_for
182
+ self.attribute_name: Optional[str] = attribute_name
183
+ self.attribute_type: Optional[Type] = attribute_type
184
+ self.code_to_modify: Optional[str] = code_to_modify
185
+ self.user_input: Optional[str] = None
186
+ self.func_name: str = ""
187
+ self.func_doc: str = ""
188
+ self.shell: CustomInteractiveShell = self._init_shell()
189
+ self.all_code_lines: List[str] = []
190
+
191
+ def _init_shell(self):
192
+ """
193
+ Initialize the Ipython shell with a custom configuration.
194
+ """
195
+ cfg = Config()
196
+ self.build_func_name_and_doc()
197
+ shell = CustomInteractiveShell(config=cfg, user_ns=self.scope, banner1=self.header,
198
+ output_type=self.output_type, func_name=self.func_name, func_doc=self.func_doc,
199
+ code_to_modify=self.code_to_modify)
200
+ return shell
201
+
202
+ def build_func_name_and_doc(self) -> Tuple[str, str]:
203
+ """
204
+ Build the function name and docstring for the user-defined function.
205
+
206
+ :return: A tuple containing the function name and docstring.
207
+ """
208
+ case = self.scope['case']
209
+ case_type = case._obj_type if isinstance(case, Case) else type(case)
210
+ self.func_name = self.build_func_name(case_type)
211
+ self.func_doc = self.build_func_doc(case_type)
212
+
213
+ def build_func_doc(self, case_type: Type):
214
+ if self.prompt_for == PromptFor.Conditions:
215
+ func_doc = (f"Get conditions on whether it's possible to conclude a value"
216
+ f" for {case_type.__name__}.{self.attribute_name}")
217
+ else:
218
+ func_doc = f"Get possible value(s) for {case_type.__name__}.{self.attribute_name}"
219
+ if is_iterable(self.attribute_type):
220
+ possible_types = [t.__name__ for t in self.attribute_type if t not in [list, set]]
221
+ func_doc += f" of types list/set of {' and/or '.join(possible_types)}"
222
+ else:
223
+ func_doc += f" of type {self.attribute_type.__name__}"
224
+ return func_doc
225
+
226
+ def build_func_name(self, case_type: Type):
227
+ func_name = f"get_{self.prompt_for.value.lower()}_for"
228
+ func_name += f"_{case_type.__name__}"
229
+ if self.attribute_name is not None:
230
+ func_name += f"_{self.attribute_name}"
231
+ if is_iterable(self.attribute_type):
232
+ output_names = [f"{t.__name__}" for t in self.attribute_type if t not in [list, set]]
233
+ else:
234
+ output_names = [self.attribute_type.__name__] if self.attribute_type is not None else None
235
+ if output_names is not None:
236
+ func_name += '_of_type_' + '_'.join(output_names)
237
+ return func_name.lower()
238
+
239
+ def run(self):
240
+ """
241
+ Run the embedded shell.
242
+ """
243
+ while True:
244
+ try:
245
+ self.shell()
246
+ self.update_user_input_from_code_lines()
247
+ break
248
+ except Exception as e:
249
+ logging.error(e)
250
+ print(e)
251
+
252
+ def update_user_input_from_code_lines(self):
253
+ """
254
+ Update the user input from the code lines captured in the shell.
255
+ """
256
+ if len(self.shell.all_lines) == 1 and self.shell.all_lines[0].replace('return', '').strip() == '':
257
+ self.user_input = None
258
+ else:
259
+ self.all_code_lines = extract_dependencies(self.shell.all_lines)
260
+ if len(self.all_code_lines) == 1 and self.all_code_lines[0].strip() == '':
261
+ self.user_input = None
262
+ else:
263
+ self.user_input = '\n'.join(self.all_code_lines)
264
+ self.user_input = encapsulate_user_input(self.user_input, self.shell.my_magics.function_signature,
265
+ self.func_doc)
266
+ if f"return {self.func_name}(case)" not in self.user_input:
267
+ self.user_input = self.user_input.strip() + f"\nreturn {self.func_name}(case)"
268
+
269
+
270
+ def prompt_user_for_expression(case_query: CaseQuery, prompt_for: PromptFor, prompt_str: Optional[str] = None)\
271
+ -> Tuple[Optional[str], Optional[CallableExpression]]:
272
+ """
273
+ Prompt the user for an executable python expression to the given case query.
274
+
275
+ :param case_query: The case query to prompt the user for.
276
+ :param prompt_for: The type of information ask user about.
277
+ :param prompt_str: The prompt string to display to the user.
278
+ :return: A callable expression that takes a case and executes user expression on it.
279
+ """
280
+ prev_user_input: Optional[str] = None
281
+ callable_expression: Optional[CallableExpression] = None
282
+ while True:
283
+ user_input, expression_tree = prompt_user_about_case(case_query, prompt_for, prompt_str,
284
+ code_to_modify=prev_user_input)
285
+ prev_user_input = '\n'.join(user_input.split('\n')[2:-1])
286
+ if user_input is None:
287
+ if prompt_for == PromptFor.Conclusion:
288
+ print("No conclusion provided. Exiting.")
289
+ return None, None
290
+ else:
291
+ print("Conditions must be provided. Please try again.")
292
+ continue
293
+ conclusion_type = bool if prompt_for == PromptFor.Conditions else case_query.attribute_type
294
+ callable_expression = CallableExpression(user_input, conclusion_type, expression_tree=expression_tree,
295
+ scope=case_query.scope)
296
+ try:
297
+ result = callable_expression(case_query.case)
298
+ if len(make_list(result)) == 0:
299
+ print(f"The given expression gave an empty result for case {case_query.name}. Please modify!")
300
+ continue
301
+ break
302
+ except Exception as e:
303
+ logging.error(e)
304
+ print(e)
305
+ return user_input, callable_expression
306
+
307
+
308
+ def prompt_user_about_case(case_query: CaseQuery, prompt_for: PromptFor,
309
+ prompt_str: Optional[str] = None,
310
+ code_to_modify: Optional[str] = None) -> Tuple[Optional[str], Optional[AST]]:
311
+ """
312
+ Prompt the user for input.
313
+
314
+ :param case_query: The case query to prompt the user for.
315
+ :param prompt_for: The type of information the user should provide for the given case.
316
+ :param prompt_str: The prompt string to display to the user.
317
+ :param code_to_modify: The code to modify. If given will be used as a start for user to modify.
318
+ :return: The user input, and the executable expression that was parsed from the user input.
319
+ """
320
+ if prompt_str is None:
321
+ prompt_str = f"Give {prompt_for} for {case_query.name}"
322
+ scope = {'case': case_query.case, **case_query.scope}
323
+ output_type = case_query.attribute_type if prompt_for == PromptFor.Conclusion else bool
324
+ shell = IPythonShell(scope=scope, header=prompt_str, output_type=output_type, prompt_for=prompt_for,
325
+ attribute_name=case_query.attribute_name, attribute_type=case_query.attribute_type,
326
+ code_to_modify=code_to_modify)
327
+ return prompt_user_input_and_parse_to_expression(shell=shell)
328
+
329
+
330
+ def prompt_user_input_and_parse_to_expression(shell: Optional[IPythonShell] = None,
331
+ user_input: Optional[str] = None)\
332
+ -> Tuple[Optional[str], Optional[ast.AST]]:
333
+ """
334
+ Prompt the user for input.
335
+
336
+ :param shell: The Ipython shell to use for prompting the user.
337
+ :param user_input: The user input to use. If given, the user input will be used instead of prompting the user.
338
+ :return: The user input and the AST tree.
339
+ """
340
+ while True:
341
+ if user_input is None:
342
+ shell = IPythonShell() if shell is None else shell
343
+ shell.run()
344
+ user_input = shell.user_input
345
+ if user_input is None:
346
+ return None, None
347
+ print(user_input)
348
+ try:
349
+ return user_input, parse_string_to_expression(user_input)
350
+ except Exception as e:
351
+ msg = f"Error parsing expression: {e}"
352
+ logging.error(msg)
353
+ print(msg)
354
+ user_input = None