quantalogic 0.35.0__py3-none-any.whl → 0.50.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.
- quantalogic/__init__.py +0 -4
- quantalogic/agent.py +603 -363
- quantalogic/agent_config.py +233 -46
- quantalogic/agent_factory.py +34 -22
- quantalogic/coding_agent.py +16 -14
- quantalogic/config.py +2 -1
- quantalogic/console_print_events.py +4 -8
- quantalogic/console_print_token.py +2 -2
- quantalogic/docs_cli.py +15 -10
- quantalogic/event_emitter.py +258 -83
- quantalogic/flow/__init__.py +23 -0
- quantalogic/flow/flow.py +595 -0
- quantalogic/flow/flow_extractor.py +672 -0
- quantalogic/flow/flow_generator.py +89 -0
- quantalogic/flow/flow_manager.py +407 -0
- quantalogic/flow/flow_manager_schema.py +169 -0
- quantalogic/flow/flow_yaml.md +419 -0
- quantalogic/generative_model.py +109 -77
- quantalogic/get_model_info.py +5 -5
- quantalogic/interactive_text_editor.py +100 -73
- quantalogic/main.py +17 -21
- quantalogic/model_info_list.py +3 -3
- quantalogic/model_info_litellm.py +14 -14
- quantalogic/prompts.py +2 -1
- quantalogic/{llm.py → quantlitellm.py} +29 -39
- quantalogic/search_agent.py +4 -4
- quantalogic/server/models.py +4 -1
- quantalogic/task_file_reader.py +5 -5
- quantalogic/task_runner.py +20 -20
- quantalogic/tool_manager.py +10 -21
- quantalogic/tools/__init__.py +98 -68
- quantalogic/tools/composio/composio.py +416 -0
- quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
- quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
- quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
- quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
- quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
- quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
- quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
- quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
- quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
- quantalogic/tools/duckduckgo_search_tool.py +2 -4
- quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
- quantalogic/tools/finance/ccxt_tool.py +373 -0
- quantalogic/tools/finance/finance_llm_tool.py +387 -0
- quantalogic/tools/finance/google_finance.py +192 -0
- quantalogic/tools/finance/market_intelligence_tool.py +520 -0
- quantalogic/tools/finance/technical_analysis_tool.py +491 -0
- quantalogic/tools/finance/tradingview_tool.py +336 -0
- quantalogic/tools/finance/yahoo_finance.py +236 -0
- quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
- quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
- quantalogic/tools/git/clone_repo_tool.py +189 -0
- quantalogic/tools/git/git_operations_tool.py +532 -0
- quantalogic/tools/google_packages/google_news_tool.py +480 -0
- quantalogic/tools/grep_app_tool.py +123 -186
- quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
- quantalogic/tools/jinja_tool.py +6 -10
- quantalogic/tools/language_handlers/__init__.py +22 -9
- quantalogic/tools/list_directory_tool.py +131 -42
- quantalogic/tools/llm_tool.py +45 -15
- quantalogic/tools/llm_vision_tool.py +59 -7
- quantalogic/tools/markitdown_tool.py +17 -5
- quantalogic/tools/nasa_packages/models.py +47 -0
- quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
- quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
- quantalogic/tools/nasa_packages/services.py +82 -0
- quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
- quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
- quantalogic/tools/product_hunt/services.py +63 -0
- quantalogic/tools/rag_tool/__init__.py +48 -0
- quantalogic/tools/rag_tool/document_metadata.py +15 -0
- quantalogic/tools/rag_tool/query_response.py +20 -0
- quantalogic/tools/rag_tool/rag_tool.py +566 -0
- quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
- quantalogic/tools/read_html_tool.py +24 -38
- quantalogic/tools/replace_in_file_tool.py +10 -10
- quantalogic/tools/safe_python_interpreter_tool.py +10 -24
- quantalogic/tools/search_definition_names.py +2 -2
- quantalogic/tools/sequence_tool.py +14 -23
- quantalogic/tools/sql_query_tool.py +17 -19
- quantalogic/tools/tool.py +39 -15
- quantalogic/tools/unified_diff_tool.py +1 -1
- quantalogic/tools/utilities/csv_processor_tool.py +234 -0
- quantalogic/tools/utilities/download_file_tool.py +179 -0
- quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
- quantalogic/tools/utils/__init__.py +1 -4
- quantalogic/tools/utils/create_sample_database.py +24 -38
- quantalogic/tools/utils/generate_database_report.py +74 -82
- quantalogic/tools/wikipedia_search_tool.py +17 -21
- quantalogic/utils/ask_user_validation.py +1 -1
- quantalogic/utils/async_utils.py +35 -0
- quantalogic/utils/check_version.py +3 -5
- quantalogic/utils/get_all_models.py +2 -1
- quantalogic/utils/git_ls.py +21 -7
- quantalogic/utils/lm_studio_model_info.py +9 -7
- quantalogic/utils/python_interpreter.py +113 -43
- quantalogic/utils/xml_utility.py +178 -0
- quantalogic/version_check.py +1 -1
- quantalogic/welcome_message.py +7 -7
- quantalogic/xml_parser.py +0 -1
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/METADATA +40 -1
- quantalogic-0.50.0.dist-info/RECORD +148 -0
- quantalogic-0.35.0.dist-info/RECORD +0 -102
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.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,
|
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
|
-
#
|
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(
|
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(
|
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
|
-
|
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
|
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
|
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
|
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
|
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[:]
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
:
|
779
|
-
|
780
|
-
|
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 = """
|