quantalogic 0.35.0__py3-none-any.whl → 0.40.0__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.
Files changed (107) hide show
  1. quantalogic/__init__.py +0 -4
  2. quantalogic/agent.py +603 -363
  3. quantalogic/agent_config.py +233 -46
  4. quantalogic/agent_factory.py +34 -22
  5. quantalogic/coding_agent.py +16 -14
  6. quantalogic/config.py +2 -1
  7. quantalogic/console_print_events.py +4 -8
  8. quantalogic/console_print_token.py +2 -2
  9. quantalogic/docs_cli.py +15 -10
  10. quantalogic/event_emitter.py +258 -83
  11. quantalogic/flow/__init__.py +23 -0
  12. quantalogic/flow/flow.py +595 -0
  13. quantalogic/flow/flow_extractor.py +672 -0
  14. quantalogic/flow/flow_generator.py +89 -0
  15. quantalogic/flow/flow_manager.py +407 -0
  16. quantalogic/flow/flow_manager_schema.py +169 -0
  17. quantalogic/flow/flow_yaml.md +419 -0
  18. quantalogic/generative_model.py +109 -77
  19. quantalogic/get_model_info.py +5 -5
  20. quantalogic/interactive_text_editor.py +100 -73
  21. quantalogic/main.py +17 -21
  22. quantalogic/model_info_list.py +3 -3
  23. quantalogic/model_info_litellm.py +14 -14
  24. quantalogic/prompts.py +2 -1
  25. quantalogic/{llm.py → quantlitellm.py} +29 -39
  26. quantalogic/search_agent.py +4 -4
  27. quantalogic/server/models.py +4 -1
  28. quantalogic/task_file_reader.py +5 -5
  29. quantalogic/task_runner.py +20 -20
  30. quantalogic/tool_manager.py +10 -21
  31. quantalogic/tools/__init__.py +98 -68
  32. quantalogic/tools/composio/composio.py +416 -0
  33. quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
  34. quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
  35. quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
  36. quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
  37. quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
  38. quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
  39. quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
  40. quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
  41. quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
  42. quantalogic/tools/duckduckgo_search_tool.py +2 -4
  43. quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
  44. quantalogic/tools/finance/ccxt_tool.py +373 -0
  45. quantalogic/tools/finance/finance_llm_tool.py +387 -0
  46. quantalogic/tools/finance/google_finance.py +192 -0
  47. quantalogic/tools/finance/market_intelligence_tool.py +520 -0
  48. quantalogic/tools/finance/technical_analysis_tool.py +491 -0
  49. quantalogic/tools/finance/tradingview_tool.py +336 -0
  50. quantalogic/tools/finance/yahoo_finance.py +236 -0
  51. quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
  52. quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
  53. quantalogic/tools/git/clone_repo_tool.py +189 -0
  54. quantalogic/tools/git/git_operations_tool.py +532 -0
  55. quantalogic/tools/google_packages/google_news_tool.py +480 -0
  56. quantalogic/tools/grep_app_tool.py +123 -186
  57. quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
  58. quantalogic/tools/jinja_tool.py +6 -10
  59. quantalogic/tools/language_handlers/__init__.py +22 -9
  60. quantalogic/tools/list_directory_tool.py +131 -42
  61. quantalogic/tools/llm_tool.py +45 -15
  62. quantalogic/tools/llm_vision_tool.py +59 -7
  63. quantalogic/tools/markitdown_tool.py +17 -5
  64. quantalogic/tools/nasa_packages/models.py +47 -0
  65. quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
  66. quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
  67. quantalogic/tools/nasa_packages/services.py +82 -0
  68. quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
  69. quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
  70. quantalogic/tools/product_hunt/services.py +63 -0
  71. quantalogic/tools/rag_tool/__init__.py +48 -0
  72. quantalogic/tools/rag_tool/document_metadata.py +15 -0
  73. quantalogic/tools/rag_tool/query_response.py +20 -0
  74. quantalogic/tools/rag_tool/rag_tool.py +566 -0
  75. quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
  76. quantalogic/tools/read_html_tool.py +24 -38
  77. quantalogic/tools/replace_in_file_tool.py +10 -10
  78. quantalogic/tools/safe_python_interpreter_tool.py +10 -24
  79. quantalogic/tools/search_definition_names.py +2 -2
  80. quantalogic/tools/sequence_tool.py +14 -23
  81. quantalogic/tools/sql_query_tool.py +17 -19
  82. quantalogic/tools/tool.py +39 -15
  83. quantalogic/tools/unified_diff_tool.py +1 -1
  84. quantalogic/tools/utilities/csv_processor_tool.py +234 -0
  85. quantalogic/tools/utilities/download_file_tool.py +179 -0
  86. quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
  87. quantalogic/tools/utils/__init__.py +1 -4
  88. quantalogic/tools/utils/create_sample_database.py +24 -38
  89. quantalogic/tools/utils/generate_database_report.py +74 -82
  90. quantalogic/tools/wikipedia_search_tool.py +17 -21
  91. quantalogic/utils/ask_user_validation.py +1 -1
  92. quantalogic/utils/async_utils.py +35 -0
  93. quantalogic/utils/check_version.py +3 -5
  94. quantalogic/utils/get_all_models.py +2 -1
  95. quantalogic/utils/git_ls.py +21 -7
  96. quantalogic/utils/lm_studio_model_info.py +9 -7
  97. quantalogic/utils/python_interpreter.py +113 -43
  98. quantalogic/utils/xml_utility.py +178 -0
  99. quantalogic/version_check.py +1 -1
  100. quantalogic/welcome_message.py +7 -7
  101. quantalogic/xml_parser.py +0 -1
  102. {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/METADATA +41 -1
  103. quantalogic-0.40.0.dist-info/RECORD +148 -0
  104. quantalogic-0.35.0.dist-info/RECORD +0 -102
  105. {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/LICENSE +0 -0
  106. {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/WHEEL +0 -0
  107. {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/entry_points.txt +0 -0
@@ -1,28 +1,37 @@
1
1
  import ast
2
2
  import builtins
3
3
  import textwrap
4
- from typing import Any, List, Dict, Optional, Tuple
4
+ from typing import Any, Dict, List, Optional, Tuple
5
+
5
6
 
6
7
  # Exception used to signal a "return" from a function call.
7
8
  class ReturnException(Exception):
8
9
  def __init__(self, value: Any) -> None:
9
10
  self.value: Any = value
10
11
 
12
+
11
13
  # Exceptions used for loop control.
12
14
  class BreakException(Exception):
13
15
  pass
14
16
 
17
+
15
18
  class ContinueException(Exception):
16
19
  pass
17
20
 
21
+
18
22
  # The main interpreter class.
19
23
  class ASTInterpreter:
20
24
  def __init__(
21
- self,
22
- allowed_modules: List[str],
23
- env_stack: Optional[List[Dict[str, Any]]] = None,
24
- source: Optional[str] = None
25
+ self, allowed_modules: List[str], env_stack: Optional[List[Dict[str, Any]]] = None, source: Optional[str] = None
25
26
  ) -> None:
27
+ """
28
+ Initialize the AST interpreter with restricted module access and environment stack.
29
+
30
+ Args:
31
+ allowed_modules: List of module names allowed to be imported.
32
+ env_stack: Optional pre-existing environment stack; if None, a new one is created.
33
+ source: Optional source code string for error reporting.
34
+ """
26
35
  self.allowed_modules: List[str] = allowed_modules
27
36
  self.modules: Dict[str, Any] = {}
28
37
  # Import only the allowed modules.
@@ -61,7 +70,7 @@ class ASTInterpreter:
61
70
  else:
62
71
  self.source_lines = None
63
72
 
64
- # NEW: Add standard Decimal features if allowed.
73
+ # Add standard Decimal features if allowed.
65
74
  if "decimal" in self.modules:
66
75
  dec = self.modules["decimal"]
67
76
  self.env_stack[0]["Decimal"] = dec.Decimal
@@ -77,8 +86,9 @@ class ASTInterpreter:
77
86
  globals: Optional[Dict[str, Any]] = None,
78
87
  locals: Optional[Dict[str, Any]] = None,
79
88
  fromlist: Tuple[str, ...] = (),
80
- level: int = 0
89
+ level: int = 0,
81
90
  ) -> Any:
91
+ """Restrict imports to only allowed modules."""
82
92
  if name not in self.allowed_modules:
83
93
  error_msg = f"Import Error: Module '{name}' is not allowed. Only {self.allowed_modules} are permitted."
84
94
  raise ImportError(error_msg)
@@ -86,14 +96,14 @@ class ASTInterpreter:
86
96
 
87
97
  # Helper: create a new interpreter instance using a given environment stack.
88
98
  def spawn_from_env(self, env_stack: List[Dict[str, Any]]) -> "ASTInterpreter":
99
+ """Spawn a new interpreter with the provided environment stack."""
89
100
  return ASTInterpreter(
90
- self.allowed_modules,
91
- env_stack,
92
- source="\n".join(self.source_lines) if self.source_lines else None
101
+ self.allowed_modules, env_stack, source="\n".join(self.source_lines) if self.source_lines else None
93
102
  )
94
103
 
95
104
  # Look up a variable in the chain of environment frames.
96
105
  def get_variable(self, name: str) -> Any:
106
+ """Retrieve a variable's value from the environment stack."""
97
107
  for frame in reversed(self.env_stack):
98
108
  if name in frame:
99
109
  return frame[name]
@@ -101,10 +111,12 @@ class ASTInterpreter:
101
111
 
102
112
  # Always assign to the most local environment.
103
113
  def set_variable(self, name: str, value: Any) -> None:
114
+ """Set a variable in the most local environment frame."""
104
115
  self.env_stack[-1][name] = value
105
116
 
106
117
  # Used for assignment targets. This handles names and destructuring.
107
118
  def assign(self, target: ast.AST, value: Any) -> None:
119
+ """Assign a value to a target (name, tuple, attribute, or subscript)."""
108
120
  if isinstance(target, ast.Name):
109
121
  # If current frame declares the name as global, update global frame.
110
122
  if "__global_names__" in self.env_stack[-1] and target.id in self.env_stack[-1]["__global_names__"]:
@@ -127,15 +139,15 @@ class ASTInterpreter:
127
139
  else:
128
140
  total = len(value)
129
141
  before = target.elts[:star_index]
130
- after = target.elts[star_index+1:]
142
+ after = target.elts[star_index + 1 :]
131
143
  if len(before) + len(after) > total:
132
144
  raise ValueError("Unpacking mismatch")
133
145
  for i, elt2 in enumerate(before):
134
146
  self.assign(elt2, value[i])
135
147
  starred_count = total - len(before) - len(after)
136
- self.assign(target.elts[star_index].value, value[len(before):len(before)+starred_count])
148
+ self.assign(target.elts[star_index].value, value[len(before) : len(before) + starred_count])
137
149
  for j, elt2 in enumerate(after):
138
- self.assign(elt2, value[len(before)+starred_count+j])
150
+ self.assign(elt2, value[len(before) + starred_count + j])
139
151
  elif isinstance(target, ast.Attribute):
140
152
  obj = self.visit(target.value)
141
153
  setattr(obj, target.attr, value)
@@ -148,6 +160,7 @@ class ASTInterpreter:
148
160
 
149
161
  # Main visitor dispatch.
150
162
  def visit(self, node: ast.AST) -> Any:
163
+ """Dispatch to the appropriate visitor method for the AST node."""
151
164
  method_name: str = "visit_" + node.__class__.__name__
152
165
  method = getattr(self, method_name, self.generic_visit)
153
166
  try:
@@ -162,12 +175,11 @@ class ASTInterpreter:
162
175
  context_line = ""
163
176
  if self.source_lines and 1 <= lineno <= len(self.source_lines):
164
177
  context_line = self.source_lines[lineno - 1]
165
- raise Exception(
166
- f"Error line {lineno}, col {col}:\n{context_line}\nDescription: {str(e)}"
167
- ) from e
178
+ raise Exception(f"Error line {lineno}, col {col}:\n{context_line}\nDescription: {str(e)}") from e
168
179
 
169
180
  # Fallback for unsupported nodes.
170
181
  def generic_visit(self, node: ast.AST) -> Any:
182
+ """Handle unsupported AST nodes with an error."""
171
183
  lineno = getattr(node, "lineno", None)
172
184
  context_line = ""
173
185
  if self.source_lines and lineno is not None and 1 <= lineno <= len(self.source_lines):
@@ -186,14 +198,19 @@ class ASTInterpreter:
186
198
  module_name: str = alias.name
187
199
  asname: str = alias.asname if alias.asname is not None else module_name
188
200
  if module_name not in self.allowed_modules:
189
- raise Exception(f"Import Error: Module '{module_name}' is not allowed. Only {self.allowed_modules} are permitted.")
201
+ raise Exception(
202
+ f"Import Error: Module '{module_name}' is not allowed. Only {self.allowed_modules} are permitted."
203
+ )
190
204
  self.set_variable(asname, self.modules[module_name])
191
205
 
192
206
  def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
207
+ """Process 'from ... import ...' statements with restricted module access."""
193
208
  if not node.module:
194
209
  raise Exception("Import Error: Missing module name in 'from ... import ...' statement")
195
210
  if node.module not in self.allowed_modules:
196
- raise Exception(f"Import Error: Module '{node.module}' is not allowed. Only {self.allowed_modules} are permitted.")
211
+ raise Exception(
212
+ f"Import Error: Module '{node.module}' is not allowed. Only {self.allowed_modules} are permitted."
213
+ )
197
214
  for alias in node.names:
198
215
  if alias.name == "*":
199
216
  raise Exception("Import Error: 'from ... import *' is not supported.")
@@ -205,8 +222,7 @@ class ASTInterpreter:
205
222
  def visit_ListComp(self, node: ast.ListComp) -> List[Any]:
206
223
  """
207
224
  Process a list comprehension, e.g., [elt for ... in ... if ...].
208
- The comprehension is executed in a new local frame that inherits the
209
- current environment.
225
+ The comprehension is executed in a new local frame that inherits the current environment.
210
226
  """
211
227
  result: List[Any] = []
212
228
  # Copy the current top-level frame for the comprehension scope.
@@ -234,19 +250,22 @@ class ASTInterpreter:
234
250
 
235
251
  # --- Other node visitors below ---
236
252
  def visit_Module(self, node: ast.Module) -> Any:
237
- # Execute all statements then return the 'result' variable if set.
253
+ """Execute module body and return 'result' or last value."""
238
254
  last_value: Any = None
239
255
  for stmt in node.body:
240
256
  last_value = self.visit(stmt)
241
257
  return self.env_stack[0].get("result", last_value)
242
258
 
243
259
  def visit_Expr(self, node: ast.Expr) -> Any:
260
+ """Evaluate an expression statement."""
244
261
  return self.visit(node.value)
245
262
 
246
263
  def visit_Constant(self, node: ast.Constant) -> Any:
264
+ """Return the value of a constant node."""
247
265
  return node.value
248
266
 
249
267
  def visit_Name(self, node: ast.Name) -> Any:
268
+ """Handle variable name lookups or stores."""
250
269
  if isinstance(node.ctx, ast.Load):
251
270
  return self.get_variable(node.id)
252
271
  elif isinstance(node.ctx, ast.Store):
@@ -255,6 +274,7 @@ class ASTInterpreter:
255
274
  raise Exception("Unsupported context for Name")
256
275
 
257
276
  def visit_BinOp(self, node: ast.BinOp) -> Any:
277
+ """Evaluate binary operations."""
258
278
  left: Any = self.visit(node.left)
259
279
  right: Any = self.visit(node.right)
260
280
  op = node.op
@@ -271,7 +291,7 @@ class ASTInterpreter:
271
291
  elif isinstance(op, ast.Mod):
272
292
  return left % right
273
293
  elif isinstance(op, ast.Pow):
274
- return left ** right
294
+ return left**right
275
295
  elif isinstance(op, ast.LShift):
276
296
  return left << right
277
297
  elif isinstance(op, ast.RShift):
@@ -286,6 +306,7 @@ class ASTInterpreter:
286
306
  raise Exception("Unsupported binary operator: " + str(op))
287
307
 
288
308
  def visit_UnaryOp(self, node: ast.UnaryOp) -> Any:
309
+ """Evaluate unary operations."""
289
310
  operand: Any = self.visit(node.operand)
290
311
  op = node.op
291
312
  if isinstance(op, ast.UAdd):
@@ -300,11 +321,13 @@ class ASTInterpreter:
300
321
  raise Exception("Unsupported unary operator: " + str(op))
301
322
 
302
323
  def visit_Assign(self, node: ast.Assign) -> None:
324
+ """Handle assignment statements."""
303
325
  value: Any = self.visit(node.value)
304
326
  for target in node.targets:
305
327
  self.assign(target, value)
306
328
 
307
329
  def visit_AugAssign(self, node: ast.AugAssign) -> Any:
330
+ """Handle augmented assignments (e.g., +=)."""
308
331
  # If target is a Name, get its current value from the environment.
309
332
  if isinstance(node.target, ast.Name):
310
333
  current_val: Any = self.get_variable(node.target.id)
@@ -325,7 +348,7 @@ class ASTInterpreter:
325
348
  elif isinstance(op, ast.Mod):
326
349
  result = current_val % right_val
327
350
  elif isinstance(op, ast.Pow):
328
- result = current_val ** right_val
351
+ result = current_val**right_val
329
352
  elif isinstance(op, ast.BitAnd):
330
353
  result = current_val & right_val
331
354
  elif isinstance(op, ast.BitOr):
@@ -342,6 +365,7 @@ class ASTInterpreter:
342
365
  return result
343
366
 
344
367
  def visit_Compare(self, node: ast.Compare) -> bool:
368
+ """Evaluate comparison operations."""
345
369
  left: Any = self.visit(node.left)
346
370
  for op, comparator in zip(node.ops, node.comparators):
347
371
  right: Any = self.visit(comparator)
@@ -364,13 +388,13 @@ class ASTInterpreter:
364
388
  if not (left >= right):
365
389
  return False
366
390
  elif isinstance(op, ast.Is):
367
- if not (left is right):
391
+ if left is not right:
368
392
  return False
369
393
  elif isinstance(op, ast.IsNot):
370
394
  if not (left is not right):
371
395
  return False
372
396
  elif isinstance(op, ast.In):
373
- if not (left in right):
397
+ if left not in right:
374
398
  return False
375
399
  elif isinstance(op, ast.NotIn):
376
400
  if not (left not in right):
@@ -381,6 +405,7 @@ class ASTInterpreter:
381
405
  return True
382
406
 
383
407
  def visit_BoolOp(self, node: ast.BoolOp) -> bool:
408
+ """Evaluate boolean operations (and/or)."""
384
409
  if isinstance(node.op, ast.And):
385
410
  for value in node.values:
386
411
  if not self.visit(value):
@@ -395,6 +420,7 @@ class ASTInterpreter:
395
420
  raise Exception("Unsupported boolean operator: " + str(node.op))
396
421
 
397
422
  def visit_If(self, node: ast.If) -> Any:
423
+ """Handle if statements."""
398
424
  if self.visit(node.test):
399
425
  branch = node.body
400
426
  else:
@@ -409,6 +435,7 @@ class ASTInterpreter:
409
435
  return result
410
436
 
411
437
  def visit_While(self, node: ast.While) -> None:
438
+ """Handle while loops."""
412
439
  while self.visit(node.test):
413
440
  try:
414
441
  for stmt in node.body:
@@ -421,6 +448,7 @@ class ASTInterpreter:
421
448
  self.visit(stmt)
422
449
 
423
450
  def visit_For(self, node: ast.For) -> None:
451
+ """Handle for loops."""
424
452
  iter_obj: Any = self.visit(node.iter)
425
453
  for item in iter_obj:
426
454
  self.assign(node.target, item)
@@ -435,53 +463,66 @@ class ASTInterpreter:
435
463
  self.visit(stmt)
436
464
 
437
465
  def visit_Break(self, node: ast.Break) -> None:
466
+ """Handle break statements."""
438
467
  raise BreakException()
439
468
 
440
469
  def visit_Continue(self, node: ast.Continue) -> None:
470
+ """Handle continue statements."""
441
471
  raise ContinueException()
442
472
 
443
473
  def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
474
+ """Define a function and store it in the environment."""
444
475
  # Capture the current env_stack for a closure without copying inner dicts.
445
- closure: List[Dict[str, Any]] = self.env_stack[:] # <-- changed here
476
+ closure: List[Dict[str, Any]] = self.env_stack[:]
446
477
  func = Function(node, closure, self)
447
478
  self.set_variable(node.name, func)
448
479
 
449
480
  def visit_Call(self, node: ast.Call) -> Any:
481
+ """Handle function calls."""
450
482
  func = self.visit(node.func)
451
483
  args: List[Any] = [self.visit(arg) for arg in node.args]
452
484
  kwargs: Dict[str, Any] = {kw.arg: self.visit(kw.value) for kw in node.keywords}
453
485
  return func(*args, **kwargs)
454
486
 
455
487
  def visit_Return(self, node: ast.Return) -> None:
488
+ """Handle return statements."""
456
489
  value: Any = self.visit(node.value) if node.value is not None else None
457
490
  raise ReturnException(value)
458
491
 
459
492
  def visit_Lambda(self, node: ast.Lambda) -> Any:
460
- closure: List[Dict[str, Any]] = self.env_stack[:] # <-- changed here
493
+ """Define a lambda function."""
494
+ closure: List[Dict[str, Any]] = self.env_stack[:]
461
495
  return LambdaFunction(node, closure, self)
462
496
 
463
497
  def visit_List(self, node: ast.List) -> List[Any]:
498
+ """Evaluate list literals."""
464
499
  return [self.visit(elt) for elt in node.elts]
465
500
 
466
501
  def visit_Tuple(self, node: ast.Tuple) -> Tuple[Any, ...]:
502
+ """Evaluate tuple literals."""
467
503
  return tuple(self.visit(elt) for elt in node.elts)
468
504
 
469
505
  def visit_Dict(self, node: ast.Dict) -> Dict[Any, Any]:
506
+ """Evaluate dictionary literals."""
470
507
  return {self.visit(k): self.visit(v) for k, v in zip(node.keys, node.values)}
471
508
 
472
509
  def visit_Set(self, node: ast.Set) -> set:
510
+ """Evaluate set literals."""
473
511
  return set(self.visit(elt) for elt in node.elts)
474
512
 
475
513
  def visit_Attribute(self, node: ast.Attribute) -> Any:
514
+ """Handle attribute access."""
476
515
  value: Any = self.visit(node.value)
477
516
  return getattr(value, node.attr)
478
517
 
479
518
  def visit_Subscript(self, node: ast.Subscript) -> Any:
519
+ """Handle subscript operations (e.g., list indexing)."""
480
520
  value: Any = self.visit(node.value)
481
521
  slice_val: Any = self.visit(node.slice)
482
522
  return value[slice_val]
483
523
 
484
524
  def visit_Slice(self, node: ast.Slice) -> slice:
525
+ """Evaluate slice objects."""
485
526
  lower: Any = self.visit(node.lower) if node.lower else None
486
527
  upper: Any = self.visit(node.upper) if node.upper else None
487
528
  step: Any = self.visit(node.step) if node.step else None
@@ -489,20 +530,23 @@ class ASTInterpreter:
489
530
 
490
531
  # For compatibility with older AST versions.
491
532
  def visit_Index(self, node: ast.Index) -> Any:
533
+ """Handle index nodes for older AST compatibility."""
492
534
  return self.visit(node.value)
493
535
 
494
536
  # Visitor for Pass nodes.
495
537
  def visit_Pass(self, node: ast.Pass) -> None:
496
- # Simply ignore 'pass' statements.
538
+ """Handle pass statements (do nothing)."""
497
539
  return None
498
540
 
499
541
  def visit_TypeIgnore(self, node: ast.TypeIgnore) -> None:
542
+ """Handle type ignore statements (do nothing)."""
500
543
  pass
501
544
 
502
545
  def visit_Try(self, node: ast.Try) -> Any:
546
+ """Handle try-except blocks."""
503
547
  result: Any = None
504
548
  exc_info: Optional[tuple] = None
505
-
549
+
506
550
  try:
507
551
  for stmt in node.body:
508
552
  result = self.visit(stmt)
@@ -541,23 +585,24 @@ class ASTInterpreter:
541
585
  if exc_info:
542
586
  raise exc_info[1]
543
587
  raise
544
-
588
+
545
589
  return result
546
590
 
547
591
  def visit_Nonlocal(self, node: ast.Nonlocal) -> None:
592
+ """Handle nonlocal statements (minimal support)."""
548
593
  # Minimal support – assume these names exist in an outer frame.
549
594
  return None
550
595
 
551
596
  def visit_JoinedStr(self, node: ast.JoinedStr) -> str:
552
- # Support f-string: concatenate all parts.
597
+ """Handle f-strings by concatenating all parts."""
553
598
  return "".join(self.visit(value) for value in node.values)
554
599
 
555
600
  def visit_FormattedValue(self, node: ast.FormattedValue) -> str:
556
- # Format the embedded expression.
601
+ """Handle formatted values within f-strings."""
557
602
  return str(self.visit(node.value))
558
603
 
559
604
  def visit_GeneratorExp(self, node: ast.GeneratorExp) -> Any:
560
- # Process a generator expression.
605
+ """Handle generator expressions."""
561
606
  def generator():
562
607
  base_frame: Dict[str, Any] = self.env_stack[-1].copy()
563
608
  self.env_stack.append(base_frame)
@@ -575,28 +620,29 @@ class ASTInterpreter:
575
620
  if all(self.visit(if_clause) for if_clause in comp.ifs):
576
621
  yield from rec(gen_idx + 1)
577
622
  self.env_stack.pop()
623
+
578
624
  gen = list(rec(0))
579
625
  self.env_stack.pop()
580
626
  for val in gen:
581
627
  yield val
628
+
582
629
  return generator()
583
630
 
584
631
  def visit_ClassDef(self, node: ast.ClassDef):
632
+ """Handle class definitions."""
585
633
  base_frame = self.env_stack[-1].copy()
586
634
  self.env_stack.append(base_frame)
587
635
  try:
588
636
  for stmt in node.body:
589
637
  self.visit(stmt)
590
- class_dict = {
591
- k: v for k, v in self.env_stack[-1].items()
592
- if k not in ["__builtins__"]
593
- }
638
+ class_dict = {k: v for k, v in self.env_stack[-1].items() if k not in ["__builtins__"]}
594
639
  finally:
595
640
  self.env_stack.pop()
596
641
  new_class = type(node.name, (), class_dict)
597
642
  self.set_variable(node.name, new_class)
598
643
 
599
644
  def visit_With(self, node: ast.With):
645
+ """Handle with statements."""
600
646
  for item in node.items:
601
647
  ctx = self.visit(item.context_expr)
602
648
  val = ctx.__enter__()
@@ -612,21 +658,26 @@ class ASTInterpreter:
612
658
  ctx.__exit__(None, None, None)
613
659
 
614
660
  def visit_Raise(self, node: ast.Raise):
661
+ """Handle raise statements."""
615
662
  exc = self.visit(node.exc) if node.exc else None
616
663
  if exc:
617
664
  raise exc
618
665
  raise Exception("Raise with no exception specified")
619
666
 
620
667
  def visit_Global(self, node: ast.Global):
668
+ """Handle global statements."""
621
669
  self.env_stack[-1].setdefault("__global_names__", set()).update(node.names)
622
670
 
623
671
  def visit_IfExp(self, node: ast.IfExp):
672
+ """Handle ternary if expressions."""
624
673
  return self.visit(node.body) if self.visit(node.test) else self.visit(node.orelse)
625
674
 
626
675
  def visit_DictComp(self, node: ast.DictComp):
676
+ """Handle dictionary comprehensions."""
627
677
  result = {}
628
678
  base_frame = self.env_stack[-1].copy()
629
679
  self.env_stack.append(base_frame)
680
+
630
681
  def rec(gen_idx: int):
631
682
  if gen_idx == len(node.generators):
632
683
  key = self.visit(node.key)
@@ -641,14 +692,17 @@ class ASTInterpreter:
641
692
  if all(self.visit(if_clause) for if_clause in comp.ifs):
642
693
  rec(gen_idx + 1)
643
694
  self.env_stack.pop()
695
+
644
696
  rec(0)
645
697
  self.env_stack.pop()
646
698
  return result
647
699
 
648
700
  def visit_SetComp(self, node: ast.SetComp):
701
+ """Handle set comprehensions."""
649
702
  result = set()
650
703
  base_frame = self.env_stack[-1].copy()
651
704
  self.env_stack.append(base_frame)
705
+
652
706
  def rec(gen_idx: int):
653
707
  if gen_idx == len(node.generators):
654
708
  result.add(self.visit(node.elt))
@@ -661,13 +715,16 @@ class ASTInterpreter:
661
715
  if all(self.visit(if_clause) for if_clause in comp.ifs):
662
716
  rec(gen_idx + 1)
663
717
  self.env_stack.pop()
718
+
664
719
  rec(0)
665
720
  self.env_stack.pop()
666
721
  return result
667
722
 
723
+
668
724
  # Class to represent a user-defined function.
669
725
  class Function:
670
726
  def __init__(self, node: ast.FunctionDef, closure: List[Dict[str, Any]], interpreter: ASTInterpreter) -> None:
727
+ """Initialize a user-defined function."""
671
728
  self.node: ast.FunctionDef = node
672
729
  # Shallow copy to support recursion.
673
730
  self.closure: List[Dict[str, Any]] = self.env_stack_reference(closure)
@@ -675,9 +732,11 @@ class Function:
675
732
 
676
733
  # Helper to simply return the given environment stack (shallow copy of list refs).
677
734
  def env_stack_reference(self, env_stack: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
735
+ """Return a shallow copy of the environment stack."""
678
736
  return env_stack[:] # shallow
679
737
 
680
738
  def __call__(self, *args: Any, **kwargs: Any) -> Any:
739
+ """Execute the function with given arguments."""
681
740
  new_env_stack: List[Dict[str, Any]] = self.closure[:]
682
741
  local_frame: Dict[str, Any] = {}
683
742
  # Bind the function into its own local frame for recursion.
@@ -703,21 +762,26 @@ class Function:
703
762
 
704
763
  # Add __get__ to support method binding.
705
764
  def __get__(self, instance: Any, owner: Any):
765
+ """Support method binding for instance methods."""
706
766
  def method(*args: Any, **kwargs: Any) -> Any:
707
767
  return self(instance, *args, **kwargs)
708
768
  return method
709
769
 
770
+
710
771
  # Class to represent a lambda function.
711
772
  class LambdaFunction:
712
773
  def __init__(self, node: ast.Lambda, closure: List[Dict[str, Any]], interpreter: ASTInterpreter) -> None:
774
+ """Initialize a lambda function."""
713
775
  self.node: ast.Lambda = node
714
776
  self.closure: List[Dict[str, Any]] = self.env_stack_reference(closure)
715
777
  self.interpreter: ASTInterpreter = interpreter
716
778
 
717
779
  def env_stack_reference(self, env_stack: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
780
+ """Return a shallow copy of the environment stack."""
718
781
  return env_stack[:]
719
782
 
720
783
  def __call__(self, *args: Any, **kwargs: Any) -> Any:
784
+ """Execute the lambda function with given arguments."""
721
785
  new_env_stack: List[Dict[str, Any]] = self.closure[:]
722
786
  local_frame: Dict[str, Any] = {}
723
787
  if len(args) < len(self.node.args.args):
@@ -732,9 +796,12 @@ class LambdaFunction:
732
796
  new_interp: ASTInterpreter = self.interpreter.spawn_from_env(new_env_stack)
733
797
  return new_interp.visit(self.node.body)
734
798
 
799
+
735
800
  # The main function to interpret an AST.
736
801
  def interpret_ast(ast_tree: Any, allowed_modules: list[str], source: str = "") -> Any:
802
+ """Interpret an AST with restricted module access."""
737
803
  import ast
804
+
738
805
  # Keep only yield-based nodes in fallback.
739
806
  unsupported = (ast.Yield, ast.YieldFrom)
740
807
  for node in ast.walk(ast_tree):
@@ -756,34 +823,37 @@ def interpret_ast(ast_tree: Any, allowed_modules: list[str], source: str = "") -
756
823
  "float": float,
757
824
  "int": int,
758
825
  "bool": bool,
759
- "Exception": Exception
826
+ "Exception": Exception,
760
827
  }
761
828
  }
762
829
  for mod in allowed_modules:
763
830
  safe_globals[mod] = __import__(mod)
764
831
  local_vars = {}
765
- # ...existing code...
766
832
  exec(compile(ast_tree, "<string>", "exec"), safe_globals, local_vars)
767
833
  return local_vars.get("result", None)
768
834
  # Otherwise, use the custom interpreter.
769
835
  interpreter = ASTInterpreter(allowed_modules=allowed_modules, source=source)
770
836
  return interpreter.visit(ast_tree)
771
837
 
838
+
772
839
  # A helper function which takes a Python code string and a list of allowed module names,
773
840
  # then parses and interprets the code.
774
841
  def interpret_code(source_code: str, allowed_modules: List[str]) -> Any:
775
842
  """
776
843
  Interpret a Python source code string with a restricted set of allowed modules.
777
-
778
- :param source_code: The Python source code to interpret.
779
- :param allowed_modules: A list of module names that are allowed.
780
- :return: The result of interpreting the source code.
844
+
845
+ Args:
846
+ source_code: The Python source code to interpret.
847
+ allowed_modules: A list of module names that are allowed.
848
+ Returns:
849
+ The result of interpreting the source code.
781
850
  """
782
851
  # Dedent the source to normalize its indentation.
783
852
  dedented_source = textwrap.dedent(source_code)
784
853
  tree: ast.AST = ast.parse(dedented_source)
785
854
  return interpret_ast(tree, allowed_modules, source=dedented_source)
786
855
 
856
+
787
857
  if __name__ == "__main__":
788
858
  print("Script is running!")
789
859
  source_code_1: str = """