hypha-debugger 0.1.6__tar.gz → 0.1.7__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.
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/PKG-INFO +1 -1
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/__init__.py +1 -1
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/debugger.py +1 -1
- hypha_debugger-0.1.7/hypha_debugger/services/execute.py +191 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/services/source.py +7 -4
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/PKG-INFO +1 -1
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/pyproject.toml +1 -1
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/tests/test_services.py +44 -4
- hypha_debugger-0.1.6/hypha_debugger/services/execute.py +0 -142
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/README.md +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/__main__.py +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/services/__init__.py +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/services/filesystem.py +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/services/info.py +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/services/inspect_vars.py +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/utils/__init__.py +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/utils/env.py +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/SOURCES.txt +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/dependency_links.txt +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/entry_points.txt +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/requires.txt +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/top_level.txt +0 -0
- {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/setup.cfg +0 -0
|
@@ -60,7 +60,7 @@ def _build_instruction_block(service_url: str, token: str = "") -> str:
|
|
|
60
60
|
"#",
|
|
61
61
|
"# Available functions:",
|
|
62
62
|
"# get_process_info - PID, Python version, CWD, platform, memory",
|
|
63
|
-
"# execute_code - Run
|
|
63
|
+
"# execute_code - Run Python code (persistent REPL, auto-captures last expr)",
|
|
64
64
|
"# get_variable - Inspect a variable by name",
|
|
65
65
|
"# list_variables - List variables in a namespace",
|
|
66
66
|
"# get_stack_trace - Stack traces of all threads",
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Arbitrary code execution service with persistent REPL and timeout support."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import reprlib
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import traceback
|
|
11
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
# Default timeout for code execution (seconds). 0 = no timeout.
|
|
15
|
+
DEFAULT_TIMEOUT = 30
|
|
16
|
+
|
|
17
|
+
# Persistent REPL namespace — survives across calls.
|
|
18
|
+
_repl_globals: Dict[str, Any] = {
|
|
19
|
+
"__name__": "__repl__",
|
|
20
|
+
"__builtins__": __builtins__,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _safe_jsonable(obj: Any) -> Any:
|
|
25
|
+
"""Return obj if it's JSON-serializable, else its repr."""
|
|
26
|
+
try:
|
|
27
|
+
json.dumps(obj)
|
|
28
|
+
return obj
|
|
29
|
+
except Exception:
|
|
30
|
+
return reprlib.repr(obj)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _summarize_namespace(ns: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
34
|
+
"""Summarize namespace variables (skip dunders)."""
|
|
35
|
+
summary = {}
|
|
36
|
+
for k, v in ns.items():
|
|
37
|
+
if k.startswith("__") and k.endswith("__"):
|
|
38
|
+
continue
|
|
39
|
+
summary[k] = {
|
|
40
|
+
"type": type(v).__name__,
|
|
41
|
+
"repr": reprlib.repr(v),
|
|
42
|
+
"jsonable": _safe_jsonable(v),
|
|
43
|
+
}
|
|
44
|
+
return summary
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from pydantic import Field
|
|
49
|
+
from hypha_rpc.utils.schema import schema_function
|
|
50
|
+
|
|
51
|
+
@schema_function
|
|
52
|
+
def execute_code(
|
|
53
|
+
code: str = Field(..., description="Python code to execute."),
|
|
54
|
+
namespace: str = Field(
|
|
55
|
+
default="",
|
|
56
|
+
description=(
|
|
57
|
+
'Namespace to execute in. Default: "" uses the persistent REPL namespace '
|
|
58
|
+
'(variables survive across calls). Use "__main__" for the main module namespace.'
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
timeout: int = Field(
|
|
62
|
+
default=DEFAULT_TIMEOUT,
|
|
63
|
+
description="Timeout in seconds. 0 for no timeout. Default: 30.",
|
|
64
|
+
),
|
|
65
|
+
) -> dict:
|
|
66
|
+
"""Execute Python code in the debugger process and return stdout, stderr, and the result.
|
|
67
|
+
|
|
68
|
+
Uses AST parsing to capture the last expression's value automatically.
|
|
69
|
+
For example, `x = 1\\nx + 1` will return result=2.
|
|
70
|
+
Code runs in a persistent REPL namespace so variables, functions, and imports
|
|
71
|
+
survive across calls. A timeout (default 30s) prevents infinite loops.
|
|
72
|
+
"""
|
|
73
|
+
return _execute_impl(code, namespace, timeout)
|
|
74
|
+
|
|
75
|
+
except ImportError:
|
|
76
|
+
|
|
77
|
+
def execute_code(code: str, namespace: str = "", timeout: int = DEFAULT_TIMEOUT) -> dict:
|
|
78
|
+
"""Execute Python code in the debugger process."""
|
|
79
|
+
return _execute_impl(code, namespace, timeout)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _get_namespace(namespace: str) -> Dict[str, Any]:
|
|
83
|
+
"""Get the target namespace dict."""
|
|
84
|
+
if not namespace:
|
|
85
|
+
return _repl_globals
|
|
86
|
+
if namespace == "__main__":
|
|
87
|
+
mod = sys.modules.get("__main__")
|
|
88
|
+
return vars(mod) if mod else _repl_globals
|
|
89
|
+
mod = sys.modules.get(namespace)
|
|
90
|
+
return vars(mod) if mod else _repl_globals
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _execute_impl(code: str, namespace: str = "", timeout: int = DEFAULT_TIMEOUT) -> dict:
|
|
94
|
+
"""Implementation of code execution with AST-based last-expression capture."""
|
|
95
|
+
ns = _get_namespace(namespace)
|
|
96
|
+
|
|
97
|
+
# Parse the code into an AST
|
|
98
|
+
try:
|
|
99
|
+
tree = ast.parse(code, mode="exec")
|
|
100
|
+
except SyntaxError as e:
|
|
101
|
+
return {
|
|
102
|
+
"ok": False,
|
|
103
|
+
"stdout": "",
|
|
104
|
+
"stderr": "",
|
|
105
|
+
"result": None,
|
|
106
|
+
"result_repr": None,
|
|
107
|
+
"error_type": type(e).__name__,
|
|
108
|
+
"error_message": str(e),
|
|
109
|
+
"traceback": traceback.format_exc(),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Separate the last expression (if any) so we can eval it for its value
|
|
113
|
+
body = tree.body
|
|
114
|
+
last_expr_node = None
|
|
115
|
+
if body and isinstance(body[-1], ast.Expr):
|
|
116
|
+
last_expr_node = body[-1].value
|
|
117
|
+
body = body[:-1]
|
|
118
|
+
|
|
119
|
+
exec_code = compile(
|
|
120
|
+
ast.Module(body=body, type_ignores=[]),
|
|
121
|
+
filename="<debugger-repl>",
|
|
122
|
+
mode="exec",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
eval_code = None
|
|
126
|
+
if last_expr_node is not None:
|
|
127
|
+
eval_code = compile(
|
|
128
|
+
ast.Expression(last_expr_node),
|
|
129
|
+
filename="<debugger-repl>",
|
|
130
|
+
mode="eval",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
stdout_buf = io.StringIO()
|
|
134
|
+
stderr_buf = io.StringIO()
|
|
135
|
+
result = None
|
|
136
|
+
error_type = None
|
|
137
|
+
error_message = None
|
|
138
|
+
error_tb = None
|
|
139
|
+
timed_out = False
|
|
140
|
+
|
|
141
|
+
def _run():
|
|
142
|
+
nonlocal result
|
|
143
|
+
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
|
144
|
+
exec(exec_code, ns, ns)
|
|
145
|
+
if eval_code is not None:
|
|
146
|
+
result = eval(eval_code, ns, ns)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
if timeout > 0 and threading.current_thread() is threading.main_thread():
|
|
150
|
+
def _timeout_handler(signum, frame):
|
|
151
|
+
raise TimeoutError(f"Code execution timed out after {timeout}s")
|
|
152
|
+
|
|
153
|
+
old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
|
|
154
|
+
signal.alarm(timeout)
|
|
155
|
+
try:
|
|
156
|
+
_run()
|
|
157
|
+
except TimeoutError as e:
|
|
158
|
+
error_type = "TimeoutError"
|
|
159
|
+
error_message = str(e)
|
|
160
|
+
error_tb = traceback.format_exc()
|
|
161
|
+
timed_out = True
|
|
162
|
+
finally:
|
|
163
|
+
signal.alarm(0)
|
|
164
|
+
signal.signal(signal.SIGALRM, old_handler)
|
|
165
|
+
else:
|
|
166
|
+
_run()
|
|
167
|
+
except Exception as e:
|
|
168
|
+
error_type = type(e).__name__
|
|
169
|
+
error_message = str(e)
|
|
170
|
+
error_tb = traceback.format_exc()
|
|
171
|
+
|
|
172
|
+
response = {
|
|
173
|
+
"ok": error_type is None,
|
|
174
|
+
"stdout": stdout_buf.getvalue(),
|
|
175
|
+
"stderr": stderr_buf.getvalue(),
|
|
176
|
+
"result": _safe_jsonable(result),
|
|
177
|
+
"result_repr": reprlib.repr(result) if result is not None else None,
|
|
178
|
+
"result_type": type(result).__name__ if result is not None else "None",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if error_type:
|
|
182
|
+
response["error_type"] = error_type
|
|
183
|
+
response["error_message"] = error_message
|
|
184
|
+
response["traceback"] = error_tb
|
|
185
|
+
# Keep backward-compat "error" key
|
|
186
|
+
response["error"] = error_tb or error_message
|
|
187
|
+
|
|
188
|
+
if timed_out:
|
|
189
|
+
response["timed_out"] = True
|
|
190
|
+
|
|
191
|
+
return response
|
|
@@ -98,12 +98,15 @@ Get information about the current Python process.
|
|
|
98
98
|
- **Example**: `curl "$SERVICE_URL/get_process_info"`
|
|
99
99
|
|
|
100
100
|
### execute_code(code, namespace?, timeout?)
|
|
101
|
-
Execute arbitrary Python code in the process.
|
|
101
|
+
Execute arbitrary Python code in the process. Uses AST parsing to automatically
|
|
102
|
+
capture the last expression's value. Variables, functions, and imports persist
|
|
103
|
+
across calls in the default REPL namespace.
|
|
102
104
|
- **code** (string, required): Python code to execute.
|
|
103
|
-
- **namespace** (string, default `"
|
|
105
|
+
- **namespace** (string, default `""`): Namespace. Empty = persistent REPL. `"__main__"` = main module.
|
|
104
106
|
- **timeout** (int, default `30`): Timeout in seconds. 0 for no timeout.
|
|
105
|
-
- **Returns**: `{stdout, stderr, result, result_type, error?, timed_out?}`
|
|
106
|
-
-
|
|
107
|
+
- **Returns**: `{ok, stdout, stderr, result, result_repr, result_type, error?, error_type?, error_message?, traceback?, timed_out?}`
|
|
108
|
+
- The last expression in the code block is automatically captured as `result`.
|
|
109
|
+
E.g. `"x = 1\\nx + 1"` returns `result=2`.
|
|
107
110
|
- **Example**:
|
|
108
111
|
```bash
|
|
109
112
|
curl -X POST "$SERVICE_URL/execute_code" \\
|
|
@@ -38,6 +38,7 @@ def test_get_installed_packages_filter():
|
|
|
38
38
|
|
|
39
39
|
def test_execute_code_expression():
|
|
40
40
|
result = execute_code("1 + 2")
|
|
41
|
+
assert result["ok"] is True
|
|
41
42
|
assert result["result"] == 3
|
|
42
43
|
assert result["result_type"] == "int"
|
|
43
44
|
assert result.get("error") is None
|
|
@@ -45,48 +46,87 @@ def test_execute_code_expression():
|
|
|
45
46
|
|
|
46
47
|
def test_execute_code_statement():
|
|
47
48
|
result = execute_code("x = 42\nprint(x)")
|
|
49
|
+
assert result["ok"] is True
|
|
48
50
|
assert "42" in result["stdout"]
|
|
49
51
|
assert result.get("error") is None
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
def test_execute_code_error():
|
|
53
55
|
result = execute_code("1 / 0")
|
|
56
|
+
assert result["ok"] is False
|
|
54
57
|
assert "error" in result
|
|
55
58
|
assert "ZeroDivisionError" in result["error"]
|
|
59
|
+
assert result["error_type"] == "ZeroDivisionError"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_execute_code_syntax_error():
|
|
63
|
+
result = execute_code("def")
|
|
64
|
+
assert result["ok"] is False
|
|
65
|
+
assert result["error_type"] == "SyntaxError"
|
|
56
66
|
|
|
57
67
|
|
|
58
68
|
def test_execute_code_import():
|
|
59
|
-
# Multi-statement uses exec (no return value), so test via stdout
|
|
60
69
|
result = execute_code("import json; print(json.dumps({'a': 1}))")
|
|
61
70
|
assert '{"a": 1}' in result["stdout"]
|
|
62
71
|
assert result.get("error") is None
|
|
63
72
|
|
|
64
73
|
|
|
65
74
|
def test_execute_code_multiline():
|
|
75
|
+
"""AST parsing captures the last expression's value."""
|
|
66
76
|
code = """
|
|
67
77
|
def greet(name):
|
|
68
78
|
return f"Hello, {name}!"
|
|
69
79
|
greet("World")
|
|
70
80
|
"""
|
|
71
81
|
result = execute_code(code)
|
|
72
|
-
assert result
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
assert result["ok"] is True
|
|
83
|
+
assert result["result"] == "Hello, World!"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_execute_code_multiline_no_trailing_expr():
|
|
87
|
+
"""Multi-statement code with no trailing expression returns None."""
|
|
88
|
+
code = "a = 1\nb = 2"
|
|
89
|
+
result = execute_code(code)
|
|
90
|
+
assert result["ok"] is True
|
|
91
|
+
assert result["result"] is None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_execute_code_persistent_namespace():
|
|
95
|
+
"""Variables persist across calls in the default REPL namespace."""
|
|
96
|
+
execute_code("repl_test_var = 42")
|
|
97
|
+
result = execute_code("repl_test_var + 8")
|
|
98
|
+
assert result["ok"] is True
|
|
99
|
+
assert result["result"] == 50
|
|
75
100
|
|
|
76
101
|
|
|
77
102
|
def test_execute_code_timeout():
|
|
78
103
|
"""Test that timeout parameter works (execution should not hang)."""
|
|
79
104
|
result = execute_code("'done'", timeout=5)
|
|
105
|
+
assert result["ok"] is True
|
|
80
106
|
assert result["result"] == "done"
|
|
81
107
|
assert result.get("error") is None
|
|
82
108
|
|
|
83
109
|
|
|
84
110
|
def test_execute_code_stdout_capture():
|
|
85
111
|
result = execute_code("print('hello'); print('world')")
|
|
112
|
+
assert result["ok"] is True
|
|
86
113
|
assert "hello" in result["stdout"]
|
|
87
114
|
assert "world" in result["stdout"]
|
|
88
115
|
|
|
89
116
|
|
|
117
|
+
def test_execute_code_stderr_capture():
|
|
118
|
+
result = execute_code("import sys; print('err', file=sys.stderr)")
|
|
119
|
+
assert result["ok"] is True
|
|
120
|
+
assert "err" in result["stderr"]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_execute_code_result_repr():
|
|
124
|
+
result = execute_code("[1, 2, 3]")
|
|
125
|
+
assert result["ok"] is True
|
|
126
|
+
assert result["result"] == [1, 2, 3]
|
|
127
|
+
assert result["result_repr"] is not None
|
|
128
|
+
|
|
129
|
+
|
|
90
130
|
# --- inspect_vars ---
|
|
91
131
|
|
|
92
132
|
def test_get_variable():
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
"""Arbitrary code execution service with timeout support."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
import io
|
|
5
|
-
import signal
|
|
6
|
-
import traceback
|
|
7
|
-
import threading
|
|
8
|
-
|
|
9
|
-
# Default timeout for code execution (seconds). 0 = no timeout.
|
|
10
|
-
DEFAULT_TIMEOUT = 30
|
|
11
|
-
|
|
12
|
-
try:
|
|
13
|
-
from pydantic import Field
|
|
14
|
-
from hypha_rpc.utils.schema import schema_function
|
|
15
|
-
|
|
16
|
-
@schema_function
|
|
17
|
-
def execute_code(
|
|
18
|
-
code: str = Field(..., description="Python code to execute."),
|
|
19
|
-
namespace: str = Field(
|
|
20
|
-
default="__main__",
|
|
21
|
-
description='Namespace to execute in. Default: "__main__" (the main module namespace).',
|
|
22
|
-
),
|
|
23
|
-
timeout: int = Field(
|
|
24
|
-
default=DEFAULT_TIMEOUT,
|
|
25
|
-
description="Timeout in seconds. 0 for no timeout. Default: 30.",
|
|
26
|
-
),
|
|
27
|
-
) -> dict:
|
|
28
|
-
"""Execute Python code in the debugger process and return stdout, stderr, and the result.
|
|
29
|
-
|
|
30
|
-
Supports both expressions (returns the value) and statements.
|
|
31
|
-
Code runs in the target namespace so you can define functions, import modules, etc.
|
|
32
|
-
A timeout (default 30s) prevents infinite loops from hanging the debugger.
|
|
33
|
-
"""
|
|
34
|
-
return _execute_impl(code, namespace, timeout)
|
|
35
|
-
|
|
36
|
-
except ImportError:
|
|
37
|
-
|
|
38
|
-
def execute_code(code: str, namespace: str = "__main__", timeout: int = DEFAULT_TIMEOUT) -> dict:
|
|
39
|
-
"""Execute Python code in the debugger process."""
|
|
40
|
-
return _execute_impl(code, namespace, timeout)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _execute_impl(code: str, namespace: str = "__main__", timeout: int = DEFAULT_TIMEOUT) -> dict:
|
|
44
|
-
"""Implementation of code execution with optional timeout."""
|
|
45
|
-
# Get the target namespace
|
|
46
|
-
if namespace == "__main__":
|
|
47
|
-
ns = vars(sys.modules.get("__main__", {}))
|
|
48
|
-
else:
|
|
49
|
-
mod = sys.modules.get(namespace)
|
|
50
|
-
ns = vars(mod) if mod else {}
|
|
51
|
-
|
|
52
|
-
stdout_capture = io.StringIO()
|
|
53
|
-
stderr_capture = io.StringIO()
|
|
54
|
-
old_stdout = sys.stdout
|
|
55
|
-
old_stderr = sys.stderr
|
|
56
|
-
|
|
57
|
-
result = None
|
|
58
|
-
error = None
|
|
59
|
-
timed_out = False
|
|
60
|
-
|
|
61
|
-
try:
|
|
62
|
-
sys.stdout = stdout_capture
|
|
63
|
-
sys.stderr = stderr_capture
|
|
64
|
-
|
|
65
|
-
if timeout > 0 and threading.current_thread() is threading.main_thread():
|
|
66
|
-
# Use SIGALRM for timeout on the main thread (Unix only)
|
|
67
|
-
def _timeout_handler(signum, frame):
|
|
68
|
-
raise TimeoutError(f"Code execution timed out after {timeout}s")
|
|
69
|
-
|
|
70
|
-
old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
|
|
71
|
-
signal.alarm(timeout)
|
|
72
|
-
try:
|
|
73
|
-
result = _run_code(code, ns)
|
|
74
|
-
except TimeoutError as e:
|
|
75
|
-
error = str(e)
|
|
76
|
-
timed_out = True
|
|
77
|
-
finally:
|
|
78
|
-
signal.alarm(0)
|
|
79
|
-
signal.signal(signal.SIGALRM, old_handler)
|
|
80
|
-
else:
|
|
81
|
-
# No timeout or not on main thread — run directly
|
|
82
|
-
result = _run_code(code, ns)
|
|
83
|
-
except Exception:
|
|
84
|
-
error = traceback.format_exc()
|
|
85
|
-
finally:
|
|
86
|
-
sys.stdout = old_stdout
|
|
87
|
-
sys.stderr = old_stderr
|
|
88
|
-
|
|
89
|
-
stdout_str = stdout_capture.getvalue()
|
|
90
|
-
stderr_str = stderr_capture.getvalue()
|
|
91
|
-
|
|
92
|
-
# Serialize result safely
|
|
93
|
-
serialized_result = _safe_serialize(result)
|
|
94
|
-
|
|
95
|
-
response = {
|
|
96
|
-
"stdout": stdout_str,
|
|
97
|
-
"stderr": stderr_str,
|
|
98
|
-
"result": serialized_result,
|
|
99
|
-
"result_type": type(result).__name__ if result is not None else "None",
|
|
100
|
-
}
|
|
101
|
-
if error:
|
|
102
|
-
response["error"] = error
|
|
103
|
-
if timed_out:
|
|
104
|
-
response["timed_out"] = True
|
|
105
|
-
|
|
106
|
-
return response
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _run_code(code: str, ns: dict):
|
|
110
|
-
"""Try as expression first (to capture return value), fall back to exec."""
|
|
111
|
-
try:
|
|
112
|
-
return eval(code, ns)
|
|
113
|
-
except SyntaxError:
|
|
114
|
-
exec(code, ns)
|
|
115
|
-
return None
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _safe_serialize(obj, depth=0, max_depth=3):
|
|
119
|
-
"""Safely serialize an object for RPC transport."""
|
|
120
|
-
if depth > max_depth:
|
|
121
|
-
return repr(obj)
|
|
122
|
-
if obj is None:
|
|
123
|
-
return None
|
|
124
|
-
if isinstance(obj, (str, int, float, bool)):
|
|
125
|
-
return obj
|
|
126
|
-
if isinstance(obj, bytes):
|
|
127
|
-
return f"<bytes len={len(obj)}>"
|
|
128
|
-
if isinstance(obj, (list, tuple)):
|
|
129
|
-
return [_safe_serialize(v, depth + 1, max_depth) for v in obj[:100]]
|
|
130
|
-
if isinstance(obj, dict):
|
|
131
|
-
return {
|
|
132
|
-
str(k): _safe_serialize(v, depth + 1, max_depth)
|
|
133
|
-
for k, v in list(obj.items())[:50]
|
|
134
|
-
}
|
|
135
|
-
if isinstance(obj, set):
|
|
136
|
-
return [_safe_serialize(v, depth + 1, max_depth) for v in list(obj)[:100]]
|
|
137
|
-
# Try repr for everything else
|
|
138
|
-
try:
|
|
139
|
-
r = repr(obj)
|
|
140
|
-
return r if len(r) < 1000 else r[:1000] + "..."
|
|
141
|
-
except Exception:
|
|
142
|
-
return f"<{type(obj).__name__}>"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|