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.
Files changed (23) hide show
  1. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/PKG-INFO +1 -1
  2. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/__init__.py +1 -1
  3. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/debugger.py +1 -1
  4. hypha_debugger-0.1.7/hypha_debugger/services/execute.py +191 -0
  5. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/services/source.py +7 -4
  6. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/PKG-INFO +1 -1
  7. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/pyproject.toml +1 -1
  8. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/tests/test_services.py +44 -4
  9. hypha_debugger-0.1.6/hypha_debugger/services/execute.py +0 -142
  10. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/README.md +0 -0
  11. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/__main__.py +0 -0
  12. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/services/__init__.py +0 -0
  13. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/services/filesystem.py +0 -0
  14. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/services/info.py +0 -0
  15. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/services/inspect_vars.py +0 -0
  16. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/utils/__init__.py +0 -0
  17. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger/utils/env.py +0 -0
  18. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/SOURCES.txt +0 -0
  19. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/dependency_links.txt +0 -0
  20. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/entry_points.txt +0 -0
  21. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/requires.txt +0 -0
  22. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/hypha_debugger.egg-info/top_level.txt +0 -0
  23. {hypha_debugger-0.1.6 → hypha_debugger-0.1.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypha-debugger
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Injectable debugger for Python processes and AI agents, powered by Hypha RPC
5
5
  Author: Amun AI AB
6
6
  License: MIT
@@ -12,5 +12,5 @@ Usage (sync):
12
12
 
13
13
  from hypha_debugger.debugger import start_debugger, start_debugger_sync, DebugSession
14
14
 
15
- __version__ = "0.1.6"
15
+ __version__ = "0.1.7"
16
16
  __all__ = ["start_debugger", "start_debugger_sync", "DebugSession", "__version__"]
@@ -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 arbitrary Python code (with timeout)",
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 `"__main__"`): Module namespace to execute in.
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
- - Tries `eval()` first (returns value), falls back to `exec()` (returns None).
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" \\
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypha-debugger
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Injectable debugger for Python processes and AI agents, powered by Hypha RPC
5
5
  Author: Amun AI AB
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hypha-debugger"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  description = "Injectable debugger for Python processes and AI agents, powered by Hypha RPC"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -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.get("error") is None
73
- # eval/exec may or may not capture the last expression depending on implementation
74
- # but at least no error
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