@oh-my-pi/pi-coding-agent 14.9.3 → 14.9.7
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.
- package/CHANGELOG.md +96 -0
- package/package.json +7 -7
- package/src/async/job-manager.ts +66 -9
- package/src/capability/rule.ts +20 -0
- package/src/cli/setup-cli.ts +14 -161
- package/src/cli/stats-cli.ts +56 -2
- package/src/cli.ts +0 -1
- package/src/config/model-registry.ts +13 -0
- package/src/config/model-resolver.ts +8 -2
- package/src/config/settings-schema.ts +1 -11
- package/src/edit/index.ts +8 -0
- package/src/edit/renderer.ts +6 -1
- package/src/edit/streaming.ts +53 -2
- package/src/eval/eval.lark +30 -10
- package/src/eval/js/context-manager.ts +334 -601
- package/src/eval/js/shared/helpers.ts +237 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -2
- package/src/eval/js/shared/rewrite-imports.ts +211 -0
- package/src/eval/js/shared/runtime.ts +168 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +2 -4
- package/src/eval/js/worker-core.ts +146 -0
- package/src/eval/js/worker-entry.ts +24 -0
- package/src/eval/js/worker-protocol.ts +41 -0
- package/src/eval/parse.ts +218 -49
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +97 -96
- package/src/eval/py/index.ts +2 -2
- package/src/eval/py/kernel.ts +472 -900
- package/src/eval/py/prelude.py +106 -87
- package/src/eval/py/runner.py +879 -0
- package/src/eval/py/runtime.ts +3 -16
- package/src/eval/py/tool-bridge.ts +137 -0
- package/src/export/html/template.css +12 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +113 -7
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/internal-urls/agent-protocol.ts +63 -52
- package/src/internal-urls/artifact-protocol.ts +51 -51
- package/src/internal-urls/docs-index.generated.ts +35 -3
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +49 -7
- package/src/internal-urls/mcp-protocol.ts +2 -8
- package/src/internal-urls/memory-protocol.ts +89 -59
- package/src/internal-urls/router.ts +38 -22
- package/src/internal-urls/rule-protocol.ts +2 -20
- package/src/internal-urls/skill-protocol.ts +4 -27
- package/src/main.ts +1 -1
- package/src/mcp/manager.ts +17 -0
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/components/tree-selector.ts +4 -0
- package/src/modes/controllers/command-controller.ts +0 -23
- package/src/modes/controllers/event-controller.ts +23 -2
- package/src/modes/controllers/mcp-command-controller.ts +7 -10
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/theme/theme.ts +27 -27
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +14 -9
- package/src/prompts/commands/orchestrate.md +1 -0
- package/src/prompts/system/project-prompt.md +10 -2
- package/src/prompts/system/subagent-system-prompt.md +8 -8
- package/src/prompts/system/system-prompt.md +13 -7
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -10
- package/src/prompts/tools/eval.md +15 -30
- package/src/prompts/tools/github.md +6 -5
- package/src/prompts/tools/hashline.md +1 -0
- package/src/prompts/tools/job.md +14 -6
- package/src/prompts/tools/task.md +20 -3
- package/src/registry/agent-registry.ts +2 -1
- package/src/sdk.ts +87 -89
- package/src/session/agent-session.ts +58 -21
- package/src/session/artifacts.ts +7 -4
- package/src/session/history-storage.ts +77 -19
- package/src/session/session-manager.ts +30 -1
- package/src/ssh/connection-manager.ts +32 -16
- package/src/ssh/sshfs-mount.ts +10 -7
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +14 -2
- package/src/task/index.ts +19 -5
- package/src/tool-discovery/tool-index.ts +21 -8
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +3 -2
- package/src/tools/bash.ts +15 -9
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +98 -7
- package/src/tools/browser/tab-worker.ts +104 -58
- package/src/tools/eval.ts +49 -11
- package/src/tools/fetch.ts +1 -1
- package/src/tools/gh.ts +140 -4
- package/src/tools/index.ts +12 -11
- package/src/tools/job.ts +48 -12
- package/src/tools/read.ts +5 -4
- package/src/tools/search.ts +3 -2
- package/src/tools/todo-write.ts +1 -1
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/web/search/index.ts +6 -4
- package/src/cli/jupyter-cli.ts +0 -106
- package/src/commands/jupyter.ts +0 -32
- package/src/eval/py/cancellation.ts +0 -28
- package/src/eval/py/gateway-coordinator.ts +0 -424
- package/src/internal-urls/jobs-protocol.ts +0 -120
- package/src/prompts/system/now-prompt.md +0 -7
- /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
"""OMP Python runner — subprocess wrapper used by the coding-agent host.
|
|
2
|
+
|
|
3
|
+
NDJSON protocol over stdin/stdout. Host writes one JSON object per line;
|
|
4
|
+
wrapper writes typed frames back.
|
|
5
|
+
|
|
6
|
+
Host -> wrapper:
|
|
7
|
+
{"id": str, "code": str, "silent": bool?, "storeHistory": bool?}
|
|
8
|
+
{"type": "exit"} # graceful shutdown
|
|
9
|
+
|
|
10
|
+
Wrapper -> host:
|
|
11
|
+
{"type": "started", "id": ...}
|
|
12
|
+
{"type": "stdout", "id": ..., "data": str}
|
|
13
|
+
{"type": "stderr", "id": ..., "data": str}
|
|
14
|
+
{"type": "display", "id": ..., "bundle": {<mime>: <value>}}
|
|
15
|
+
{"type": "result", "id": ..., "bundle": {<mime>: <value>}}
|
|
16
|
+
{"type": "error", "id": ..., "ename": str, "evalue": str, "traceback": [str]}
|
|
17
|
+
{"type": "done", "id": ..., "status": "ok"|"error",
|
|
18
|
+
"executionCount": int, "cancelled": bool}
|
|
19
|
+
|
|
20
|
+
The runner is intentionally self-contained: no third-party imports, no IPython.
|
|
21
|
+
Magics are translated by a small line-scanner before AST parsing; rich display
|
|
22
|
+
falls back through `_repr_*_` methods so pandas/PIL/plotly etc. still render
|
|
23
|
+
when installed.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import ast
|
|
29
|
+
import base64
|
|
30
|
+
import builtins
|
|
31
|
+
import io
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import runpy
|
|
36
|
+
import shlex
|
|
37
|
+
import signal
|
|
38
|
+
import subprocess
|
|
39
|
+
import sys
|
|
40
|
+
import threading
|
|
41
|
+
import time
|
|
42
|
+
import traceback
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import Any, Callable
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Frame writer
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
_RAW_STDOUT = sys.__stdout__
|
|
51
|
+
_RAW_STDERR = sys.__stderr__
|
|
52
|
+
_OUT_LOCK = threading.Lock()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _json_default(o: Any) -> Any:
|
|
56
|
+
try:
|
|
57
|
+
return repr(o)
|
|
58
|
+
except Exception:
|
|
59
|
+
return f"<unrepr {type(o).__name__}>"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _emit(frame: dict) -> None:
|
|
63
|
+
"""Serialize a frame and write it to the host as a single NDJSON line."""
|
|
64
|
+
line = json.dumps(frame, ensure_ascii=False, default=_json_default)
|
|
65
|
+
with _OUT_LOCK:
|
|
66
|
+
_RAW_STDOUT.write(line)
|
|
67
|
+
_RAW_STDOUT.write("\n")
|
|
68
|
+
_RAW_STDOUT.flush()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# User stdout/stderr proxies
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _StreamProxy(io.TextIOBase):
|
|
77
|
+
"""Emit each ``write()`` as a typed frame tied to the current request."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, kind: str) -> None:
|
|
80
|
+
super().__init__()
|
|
81
|
+
self._kind = kind
|
|
82
|
+
|
|
83
|
+
def writable(self) -> bool: # noqa: D401 - protocol method
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
def isatty(self) -> bool: # noqa: D401 - protocol method
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def write(self, data: Any) -> int: # type: ignore[override]
|
|
90
|
+
if not isinstance(data, str):
|
|
91
|
+
data = str(data)
|
|
92
|
+
if not data:
|
|
93
|
+
return 0
|
|
94
|
+
rid = _STATE.current_id
|
|
95
|
+
if rid is None:
|
|
96
|
+
_RAW_STDERR.write(data)
|
|
97
|
+
_RAW_STDERR.flush()
|
|
98
|
+
return len(data)
|
|
99
|
+
_emit({"type": self._kind, "id": rid, "data": data})
|
|
100
|
+
return len(data)
|
|
101
|
+
|
|
102
|
+
def flush(self) -> None: # noqa: D401 - protocol method
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Runner state
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _RunnerState:
|
|
112
|
+
def __init__(self) -> None:
|
|
113
|
+
self.current_id: str | None = None
|
|
114
|
+
self.execution_count: int = 0
|
|
115
|
+
self.cancel_requested: bool = False
|
|
116
|
+
# User globals — kept across requests when running in session mode.
|
|
117
|
+
self.user_ns: dict[str, Any] = {
|
|
118
|
+
"__name__": "__main__",
|
|
119
|
+
"__doc__": None,
|
|
120
|
+
"__builtins__": builtins,
|
|
121
|
+
}
|
|
122
|
+
self.last_install_marker: int = 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
_STATE = _RunnerState()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Magic source transformer
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
_MAGIC_LINE_RE = re.compile(r"^(?P<indent>[ \t]*)(?P<name>[A-Za-z_][A-Za-z_0-9]*)(?:[ \t]+(?P<args>.*))?$")
|
|
134
|
+
_ASSIGN_LINE_RE = re.compile(
|
|
135
|
+
r"^(?P<indent>[ \t]*)(?P<lhs>[A-Za-z_][A-Za-z_0-9.\[\], ]*?)\s*=\s*(?P<rhs>.+)$"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _fold_continuations(lines: list[str], start: int) -> tuple[str, int]:
|
|
140
|
+
"""Fold trailing backslash continuations starting at ``start``. Returns
|
|
141
|
+
``(folded_text, lines_consumed)``."""
|
|
142
|
+
parts: list[str] = []
|
|
143
|
+
i = start
|
|
144
|
+
while i < len(lines):
|
|
145
|
+
line = lines[i]
|
|
146
|
+
if line.endswith("\\"):
|
|
147
|
+
parts.append(line[:-1])
|
|
148
|
+
i += 1
|
|
149
|
+
continue
|
|
150
|
+
parts.append(line)
|
|
151
|
+
i += 1
|
|
152
|
+
break
|
|
153
|
+
return ("".join(parts), i - start)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _quote_arg(text: str) -> str:
|
|
157
|
+
"""Return a Python string literal that round-trips ``text`` exactly."""
|
|
158
|
+
return json.dumps(text, ensure_ascii=False)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def transform_cell(source: str) -> str:
|
|
162
|
+
"""Translate IPython-style magics + shell escapes into plain Python.
|
|
163
|
+
|
|
164
|
+
Rules
|
|
165
|
+
-----
|
|
166
|
+
* ``%name args`` -> ``__omp_magic("name", "args")``
|
|
167
|
+
* ``var = %name args`` -> ``var = __omp_magic("name", "args")``
|
|
168
|
+
* ``!cmd`` -> ``__omp_shell("cmd")``
|
|
169
|
+
* ``var = !cmd`` -> ``var = __omp_shell("cmd")``
|
|
170
|
+
* ``%%name args\\n<body>`` -> ``__omp_magic_cell("name", "args", "<body>")``
|
|
171
|
+
(cell magic must be the first non-whitespace token of a top-level line and
|
|
172
|
+
consumes the remainder of the cell)
|
|
173
|
+
|
|
174
|
+
Lines inside strings or comments are left alone — we operate on the raw
|
|
175
|
+
text before parsing, but the scanner only fires on the first token of each
|
|
176
|
+
physical line and never touches the body of triple-quoted strings because
|
|
177
|
+
those bodies are never first tokens themselves.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
if "%" not in source and "!" not in source:
|
|
181
|
+
return source
|
|
182
|
+
|
|
183
|
+
lines = source.splitlines()
|
|
184
|
+
out: list[str] = []
|
|
185
|
+
i = 0
|
|
186
|
+
while i < len(lines):
|
|
187
|
+
line = lines[i]
|
|
188
|
+
stripped = line.lstrip()
|
|
189
|
+
indent = line[: len(line) - len(stripped)]
|
|
190
|
+
|
|
191
|
+
# Cell magic — consumes from here to EOF.
|
|
192
|
+
if stripped.startswith("%%"):
|
|
193
|
+
head, _ = _split_magic_head(stripped[2:])
|
|
194
|
+
name, args = head
|
|
195
|
+
body_lines = lines[i + 1 :]
|
|
196
|
+
body = "\n".join(body_lines)
|
|
197
|
+
out.append(
|
|
198
|
+
f"{indent}__omp_magic_cell({_quote_arg(name)}, {_quote_arg(args)}, {_quote_arg(body)})"
|
|
199
|
+
)
|
|
200
|
+
return "\n".join(out)
|
|
201
|
+
|
|
202
|
+
# Line magic / shell at start of line.
|
|
203
|
+
if stripped.startswith("%") and not stripped.startswith("%%"):
|
|
204
|
+
folded, consumed = _fold_continuations(lines, i)
|
|
205
|
+
stripped_folded = folded.lstrip()
|
|
206
|
+
indent = folded[: len(folded) - len(stripped_folded)]
|
|
207
|
+
head, _ = _split_magic_head(stripped_folded[1:])
|
|
208
|
+
name, args = head
|
|
209
|
+
out.append(f"{indent}__omp_magic({_quote_arg(name)}, {_quote_arg(args)})")
|
|
210
|
+
i += consumed
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
if stripped.startswith("!"):
|
|
214
|
+
folded, consumed = _fold_continuations(lines, i)
|
|
215
|
+
stripped_folded = folded.lstrip()
|
|
216
|
+
indent = folded[: len(folded) - len(stripped_folded)]
|
|
217
|
+
cmd = stripped_folded[1:].strip()
|
|
218
|
+
out.append(f"{indent}__omp_shell({_quote_arg(cmd)})")
|
|
219
|
+
i += consumed
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# Assignment forms: var = %magic / var = !cmd
|
|
223
|
+
m = _ASSIGN_LINE_RE.match(line)
|
|
224
|
+
if m:
|
|
225
|
+
rhs = m.group("rhs").strip()
|
|
226
|
+
if rhs.startswith("!"):
|
|
227
|
+
cmd = rhs[1:].strip()
|
|
228
|
+
out.append(f"{m.group('indent')}{m.group('lhs').rstrip()} = __omp_shell({_quote_arg(cmd)})")
|
|
229
|
+
i += 1
|
|
230
|
+
continue
|
|
231
|
+
if rhs.startswith("%") and not rhs.startswith("%%"):
|
|
232
|
+
head, _ = _split_magic_head(rhs[1:])
|
|
233
|
+
name, args = head
|
|
234
|
+
out.append(
|
|
235
|
+
f"{m.group('indent')}{m.group('lhs').rstrip()} = __omp_magic({_quote_arg(name)}, {_quote_arg(args)})"
|
|
236
|
+
)
|
|
237
|
+
i += 1
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
out.append(line)
|
|
241
|
+
i += 1
|
|
242
|
+
|
|
243
|
+
return "\n".join(out)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _split_magic_head(text: str) -> tuple[tuple[str, str], str]:
|
|
247
|
+
"""Split ``"name rest"`` into ``("name", "rest")``."""
|
|
248
|
+
text = text.lstrip()
|
|
249
|
+
if not text:
|
|
250
|
+
return ("", ""), ""
|
|
251
|
+
m = re.match(r"([A-Za-z_][A-Za-z_0-9]*)(?:\s+(.*))?$", text)
|
|
252
|
+
if not m:
|
|
253
|
+
return ("", text), ""
|
|
254
|
+
return (m.group(1), (m.group(2) or "").rstrip()), ""
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
# Magic registry
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
_LINE_MAGICS: dict[str, Callable[[str], Any]] = {}
|
|
263
|
+
_CELL_MAGICS: dict[str, Callable[[str, str], Any]] = {}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def line_magic(name: str) -> Callable[[Callable[[str], Any]], Callable[[str], Any]]:
|
|
267
|
+
def decorator(fn: Callable[[str], Any]) -> Callable[[str], Any]:
|
|
268
|
+
_LINE_MAGICS[name] = fn
|
|
269
|
+
return fn
|
|
270
|
+
|
|
271
|
+
return decorator
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def cell_magic(name: str) -> Callable[[Callable[[str, str], Any]], Callable[[str, str], Any]]:
|
|
275
|
+
def decorator(fn: Callable[[str, str], Any]) -> Callable[[str, str], Any]:
|
|
276
|
+
_CELL_MAGICS[name] = fn
|
|
277
|
+
return fn
|
|
278
|
+
|
|
279
|
+
return decorator
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _emit_status(op: str, **data: Any) -> None:
|
|
283
|
+
bundle = {"application/x-omp-status": {"op": op, **data}}
|
|
284
|
+
rid = _STATE.current_id
|
|
285
|
+
if rid is None:
|
|
286
|
+
return
|
|
287
|
+
_emit({"type": "display", "id": rid, "bundle": bundle})
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@line_magic("pip")
|
|
291
|
+
def _magic_pip(args: str) -> None:
|
|
292
|
+
argv = shlex.split(args) if args else ["--help"]
|
|
293
|
+
cmd = [sys.executable, "-m", "pip", *argv]
|
|
294
|
+
proc = subprocess.Popen(
|
|
295
|
+
cmd,
|
|
296
|
+
stdout=subprocess.PIPE,
|
|
297
|
+
stderr=subprocess.STDOUT,
|
|
298
|
+
text=True,
|
|
299
|
+
bufsize=1,
|
|
300
|
+
)
|
|
301
|
+
installed_packages: list[str] = []
|
|
302
|
+
assert proc.stdout is not None
|
|
303
|
+
for raw_line in proc.stdout:
|
|
304
|
+
sys.stdout.write(raw_line)
|
|
305
|
+
m = re.search(r"Successfully installed\s+(.+)$", raw_line)
|
|
306
|
+
if m:
|
|
307
|
+
for token in m.group(1).split():
|
|
308
|
+
# Token is name-version; drop the version suffix.
|
|
309
|
+
pkg = token.rsplit("-", 1)[0]
|
|
310
|
+
installed_packages.append(pkg.replace("_", "-"))
|
|
311
|
+
proc.wait()
|
|
312
|
+
if installed_packages:
|
|
313
|
+
import importlib
|
|
314
|
+
|
|
315
|
+
importlib.invalidate_caches()
|
|
316
|
+
prefixes = {pkg.lower().replace("-", "_") for pkg in installed_packages}
|
|
317
|
+
for mod_name in list(sys.modules):
|
|
318
|
+
head = mod_name.split(".", 1)[0].lower()
|
|
319
|
+
if head in prefixes:
|
|
320
|
+
sys.modules.pop(mod_name, None)
|
|
321
|
+
_emit_status("pip", args=args, installed=installed_packages, exit_code=proc.returncode)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@line_magic("cd")
|
|
325
|
+
def _magic_cd(args: str) -> str:
|
|
326
|
+
path = os.path.expanduser(args.strip()) or os.path.expanduser("~")
|
|
327
|
+
os.chdir(path)
|
|
328
|
+
cwd = os.getcwd()
|
|
329
|
+
_emit_status("cd", path=cwd)
|
|
330
|
+
return cwd
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@line_magic("pwd")
|
|
334
|
+
def _magic_pwd(_args: str) -> str:
|
|
335
|
+
cwd = os.getcwd()
|
|
336
|
+
_emit_status("pwd", path=cwd)
|
|
337
|
+
return cwd
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@line_magic("ls")
|
|
341
|
+
def _magic_ls(args: str) -> list[str]:
|
|
342
|
+
target = os.path.expanduser(args.strip()) or "."
|
|
343
|
+
entries = sorted(os.listdir(target))
|
|
344
|
+
_emit_status("ls", path=os.path.abspath(target), count=len(entries))
|
|
345
|
+
return entries
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@line_magic("env")
|
|
349
|
+
def _magic_env(args: str) -> Any:
|
|
350
|
+
args = args.strip()
|
|
351
|
+
if not args:
|
|
352
|
+
return dict(sorted(os.environ.items()))
|
|
353
|
+
if "=" in args:
|
|
354
|
+
key, value = args.split("=", 1)
|
|
355
|
+
os.environ[key.strip()] = value.strip()
|
|
356
|
+
return value.strip()
|
|
357
|
+
return os.environ.get(args)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@line_magic("set_env")
|
|
361
|
+
def _magic_set_env(args: str) -> str:
|
|
362
|
+
parts = args.split(None, 1)
|
|
363
|
+
if len(parts) != 2:
|
|
364
|
+
raise ValueError("Usage: %set_env KEY VALUE")
|
|
365
|
+
key, value = parts
|
|
366
|
+
os.environ[key] = value
|
|
367
|
+
return value
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@line_magic("time")
|
|
371
|
+
def _magic_time(args: str) -> Any:
|
|
372
|
+
start = time.perf_counter()
|
|
373
|
+
result = eval(args, _STATE.user_ns)
|
|
374
|
+
elapsed = time.perf_counter() - start
|
|
375
|
+
sys.stdout.write(f"Wall time: {elapsed * 1000:.2f} ms\n")
|
|
376
|
+
_emit_status("time", elapsed_ms=round(elapsed * 1000, 3))
|
|
377
|
+
return result
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@line_magic("timeit")
|
|
381
|
+
def _magic_timeit(args: str) -> None:
|
|
382
|
+
import timeit as _timeit
|
|
383
|
+
|
|
384
|
+
timer = _timeit.Timer(stmt=args, globals=_STATE.user_ns)
|
|
385
|
+
iters, total = timer.autorange()
|
|
386
|
+
per = total / iters
|
|
387
|
+
sys.stdout.write(f"{iters} loops, best of 1: {per * 1e6:.2f} us per loop\n")
|
|
388
|
+
_emit_status("timeit", loops=iters, total_ms=round(total * 1000, 3))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@line_magic("who")
|
|
392
|
+
def _magic_who(_args: str) -> list[str]:
|
|
393
|
+
names = sorted(
|
|
394
|
+
name
|
|
395
|
+
for name, value in _STATE.user_ns.items()
|
|
396
|
+
if not name.startswith("_") and not callable(value) or hasattr(value, "__class__")
|
|
397
|
+
)
|
|
398
|
+
return [n for n in names if not n.startswith("__")]
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@line_magic("whos")
|
|
402
|
+
def _magic_whos(_args: str) -> list[tuple[str, str]]:
|
|
403
|
+
rows = []
|
|
404
|
+
for name in sorted(_STATE.user_ns):
|
|
405
|
+
if name.startswith("__"):
|
|
406
|
+
continue
|
|
407
|
+
value = _STATE.user_ns[name]
|
|
408
|
+
rows.append((name, type(value).__name__))
|
|
409
|
+
return rows
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@line_magic("reset")
|
|
413
|
+
def _magic_reset(_args: str) -> None:
|
|
414
|
+
_STATE.user_ns.clear()
|
|
415
|
+
_STATE.user_ns.update({"__name__": "__main__", "__doc__": None, "__builtins__": builtins})
|
|
416
|
+
_install_builtins(_STATE.user_ns)
|
|
417
|
+
_emit_status("reset")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@line_magic("load")
|
|
421
|
+
def _magic_load(args: str) -> None:
|
|
422
|
+
path = Path(os.path.expanduser(args.strip()))
|
|
423
|
+
source = path.read_text(encoding="utf-8")
|
|
424
|
+
_emit({"type": "display", "id": _STATE.current_id, "bundle": {"text/plain": source}})
|
|
425
|
+
_exec_source(source, _STATE.user_ns)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@line_magic("run")
|
|
429
|
+
def _magic_run(args: str) -> None:
|
|
430
|
+
parts = shlex.split(args) if args else []
|
|
431
|
+
if not parts:
|
|
432
|
+
raise ValueError("Usage: %run <path>")
|
|
433
|
+
target = os.path.expanduser(parts[0])
|
|
434
|
+
saved_argv = sys.argv
|
|
435
|
+
try:
|
|
436
|
+
sys.argv = [target, *parts[1:]]
|
|
437
|
+
result_ns = runpy.run_path(target, run_name="__main__")
|
|
438
|
+
finally:
|
|
439
|
+
sys.argv = saved_argv
|
|
440
|
+
for name, value in result_ns.items():
|
|
441
|
+
if name.startswith("__"):
|
|
442
|
+
continue
|
|
443
|
+
_STATE.user_ns[name] = value
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@cell_magic("bash")
|
|
447
|
+
def _magic_cell_bash(args: str, body: str) -> int:
|
|
448
|
+
return _run_shell_body(body, shell_arg="/bin/bash")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@cell_magic("sh")
|
|
452
|
+
def _magic_cell_sh(args: str, body: str) -> int:
|
|
453
|
+
return _run_shell_body(body, shell_arg="/bin/sh")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@cell_magic("capture")
|
|
457
|
+
def _magic_cell_capture(args: str, body: str) -> str:
|
|
458
|
+
"""Capture stdout/stderr of body; bind to ``args`` (a name) if provided."""
|
|
459
|
+
captured = io.StringIO()
|
|
460
|
+
saved_stdout, saved_stderr = sys.stdout, sys.stderr
|
|
461
|
+
sys.stdout = sys.stderr = captured
|
|
462
|
+
try:
|
|
463
|
+
_exec_source(body, _STATE.user_ns)
|
|
464
|
+
finally:
|
|
465
|
+
sys.stdout, sys.stderr = saved_stdout, saved_stderr
|
|
466
|
+
text = captured.getvalue()
|
|
467
|
+
name = args.strip()
|
|
468
|
+
if name:
|
|
469
|
+
_STATE.user_ns[name] = text
|
|
470
|
+
return text
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@cell_magic("timeit")
|
|
474
|
+
def _magic_cell_timeit(args: str, body: str) -> None:
|
|
475
|
+
import timeit as _timeit
|
|
476
|
+
|
|
477
|
+
timer = _timeit.Timer(stmt=body, globals=_STATE.user_ns)
|
|
478
|
+
iters, total = timer.autorange()
|
|
479
|
+
per = total / iters
|
|
480
|
+
sys.stdout.write(f"{iters} loops, best of 1: {per * 1e6:.2f} us per loop\n")
|
|
481
|
+
_emit_status("timeit", loops=iters, total_ms=round(total * 1000, 3))
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@cell_magic("writefile")
|
|
485
|
+
def _magic_cell_writefile(args: str, body: str) -> str:
|
|
486
|
+
path = Path(os.path.expanduser(args.strip()))
|
|
487
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
488
|
+
path.write_text(body, encoding="utf-8")
|
|
489
|
+
_emit_status("writefile", path=str(path), bytes=len(body))
|
|
490
|
+
return str(path)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _run_shell_body(body: str, *, shell_arg: str) -> int:
|
|
494
|
+
proc = subprocess.Popen(
|
|
495
|
+
[shell_arg, "-c", body],
|
|
496
|
+
stdout=subprocess.PIPE,
|
|
497
|
+
stderr=subprocess.STDOUT,
|
|
498
|
+
text=True,
|
|
499
|
+
bufsize=1,
|
|
500
|
+
)
|
|
501
|
+
assert proc.stdout is not None
|
|
502
|
+
for raw_line in proc.stdout:
|
|
503
|
+
sys.stdout.write(raw_line)
|
|
504
|
+
proc.wait()
|
|
505
|
+
return proc.returncode
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def __omp_magic(name: str, args: str) -> Any:
|
|
509
|
+
fn = _LINE_MAGICS.get(name)
|
|
510
|
+
if fn is None:
|
|
511
|
+
raise NameError(f"UsageError: Line magic function '%{name}' not found.")
|
|
512
|
+
return fn(args)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def __omp_magic_cell(name: str, args: str, body: str) -> Any:
|
|
516
|
+
fn = _CELL_MAGICS.get(name)
|
|
517
|
+
if fn is None:
|
|
518
|
+
raise NameError(f"UsageError: Cell magic function '%%{name}' not found.")
|
|
519
|
+
return fn(args, body)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class _ShellResult(list):
|
|
523
|
+
"""Result of ``!cmd`` — list of stripped output lines."""
|
|
524
|
+
|
|
525
|
+
def __init__(self, lines: list[str], returncode: int) -> None:
|
|
526
|
+
super().__init__(lines)
|
|
527
|
+
self.returncode = returncode
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def n(self) -> str: # IPython compat
|
|
531
|
+
return "\n".join(self)
|
|
532
|
+
|
|
533
|
+
@property
|
|
534
|
+
def s(self) -> str: # IPython compat
|
|
535
|
+
return " ".join(self)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def __omp_shell(cmd: str) -> _ShellResult:
|
|
539
|
+
proc = subprocess.run(
|
|
540
|
+
cmd,
|
|
541
|
+
shell=True,
|
|
542
|
+
stdout=subprocess.PIPE,
|
|
543
|
+
stderr=subprocess.STDOUT,
|
|
544
|
+
text=True,
|
|
545
|
+
)
|
|
546
|
+
if proc.stdout:
|
|
547
|
+
sys.stdout.write(proc.stdout)
|
|
548
|
+
lines = [line for line in (proc.stdout or "").splitlines()]
|
|
549
|
+
return _ShellResult(lines, proc.returncode)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# ---------------------------------------------------------------------------
|
|
553
|
+
# Display dispatch
|
|
554
|
+
# ---------------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
_REPR_MIMES = [
|
|
558
|
+
("_repr_html_", "text/html"),
|
|
559
|
+
("_repr_markdown_", "text/markdown"),
|
|
560
|
+
("_repr_svg_", "image/svg+xml"),
|
|
561
|
+
("_repr_png_", "image/png"),
|
|
562
|
+
("_repr_jpeg_", "image/jpeg"),
|
|
563
|
+
("_repr_json_", "application/json"),
|
|
564
|
+
("_repr_latex_", "text/latex"),
|
|
565
|
+
]
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _coerce_image_bytes(value: Any) -> str:
|
|
569
|
+
if isinstance(value, (bytes, bytearray)):
|
|
570
|
+
return base64.b64encode(bytes(value)).decode("ascii")
|
|
571
|
+
if isinstance(value, str):
|
|
572
|
+
return value
|
|
573
|
+
return base64.b64encode(repr(value).encode("utf-8")).decode("ascii")
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _mime_bundle(value: Any) -> dict:
|
|
577
|
+
"""Build a Jupyter-style MIME bundle for ``value``.
|
|
578
|
+
|
|
579
|
+
Honors ``_repr_mimebundle_`` first, falls back to individual ``_repr_*_``
|
|
580
|
+
accessors, and always provides ``text/plain``.
|
|
581
|
+
"""
|
|
582
|
+
bundle: dict[str, Any] = {}
|
|
583
|
+
|
|
584
|
+
mimebundle = getattr(value, "_repr_mimebundle_", None)
|
|
585
|
+
if callable(mimebundle):
|
|
586
|
+
try:
|
|
587
|
+
data = mimebundle()
|
|
588
|
+
except Exception:
|
|
589
|
+
data = None
|
|
590
|
+
if isinstance(data, tuple):
|
|
591
|
+
data = data[0]
|
|
592
|
+
if isinstance(data, dict):
|
|
593
|
+
bundle.update({str(k): v for k, v in data.items()})
|
|
594
|
+
|
|
595
|
+
for attr, mime in _REPR_MIMES:
|
|
596
|
+
if mime in bundle:
|
|
597
|
+
continue
|
|
598
|
+
repr_fn = getattr(value, attr, None)
|
|
599
|
+
if not callable(repr_fn):
|
|
600
|
+
continue
|
|
601
|
+
try:
|
|
602
|
+
data = repr_fn()
|
|
603
|
+
except Exception:
|
|
604
|
+
continue
|
|
605
|
+
if data is None:
|
|
606
|
+
continue
|
|
607
|
+
if mime in ("image/png", "image/jpeg"):
|
|
608
|
+
bundle[mime] = _coerce_image_bytes(data)
|
|
609
|
+
else:
|
|
610
|
+
bundle[mime] = data
|
|
611
|
+
|
|
612
|
+
if "text/plain" not in bundle:
|
|
613
|
+
try:
|
|
614
|
+
bundle["text/plain"] = repr(value)
|
|
615
|
+
except Exception:
|
|
616
|
+
bundle["text/plain"] = f"<unrepr {type(value).__name__}>"
|
|
617
|
+
|
|
618
|
+
return bundle
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _emit_display(bundle: dict, *, kind: str = "display") -> None:
|
|
622
|
+
rid = _STATE.current_id
|
|
623
|
+
if rid is None:
|
|
624
|
+
return
|
|
625
|
+
_emit({"type": kind, "id": rid, "bundle": bundle})
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def __omp_display(value: Any, *, raw: bool = False, kind: str = "display") -> None:
|
|
629
|
+
if raw:
|
|
630
|
+
if not isinstance(value, dict):
|
|
631
|
+
raise TypeError("display(..., raw=True) requires a MIME bundle dict")
|
|
632
|
+
bundle = {str(k): v for k, v in value.items()}
|
|
633
|
+
if "text/plain" not in bundle:
|
|
634
|
+
bundle["text/plain"] = ""
|
|
635
|
+
_emit_display(bundle, kind=kind)
|
|
636
|
+
return
|
|
637
|
+
_emit_display(_mime_bundle(value), kind=kind)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# ---------------------------------------------------------------------------
|
|
641
|
+
# Matplotlib post-cell flush
|
|
642
|
+
# ---------------------------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _flush_matplotlib_figures() -> None:
|
|
646
|
+
plt = sys.modules.get("matplotlib.pyplot")
|
|
647
|
+
if plt is None:
|
|
648
|
+
return
|
|
649
|
+
try:
|
|
650
|
+
fignums = list(plt.get_fignums())
|
|
651
|
+
except Exception:
|
|
652
|
+
return
|
|
653
|
+
for num in fignums:
|
|
654
|
+
try:
|
|
655
|
+
fig = plt.figure(num)
|
|
656
|
+
buf = io.BytesIO()
|
|
657
|
+
fig.savefig(buf, format="png", bbox_inches="tight")
|
|
658
|
+
data = base64.b64encode(buf.getvalue()).decode("ascii")
|
|
659
|
+
_emit_display({"image/png": data, "text/plain": f"<Figure {num}>"})
|
|
660
|
+
plt.close(fig)
|
|
661
|
+
except Exception:
|
|
662
|
+
continue
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# Force a non-interactive backend before user code imports matplotlib. Set as
|
|
666
|
+
# environ default so the user can still override it explicitly.
|
|
667
|
+
os.environ.setdefault("MPLBACKEND", "Agg")
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
# ---------------------------------------------------------------------------
|
|
671
|
+
# Builtin injection
|
|
672
|
+
# ---------------------------------------------------------------------------
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _install_builtins(ns: dict) -> None:
|
|
676
|
+
ns["display"] = __omp_display
|
|
677
|
+
ns["__omp_display"] = __omp_display
|
|
678
|
+
ns["__omp_magic"] = __omp_magic
|
|
679
|
+
ns["__omp_magic_cell"] = __omp_magic_cell
|
|
680
|
+
ns["__omp_shell"] = __omp_shell
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
_install_builtins(_STATE.user_ns)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# ---------------------------------------------------------------------------
|
|
687
|
+
# Source execution (split last expression for rich display)
|
|
688
|
+
# ---------------------------------------------------------------------------
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _exec_source(source: str, ns: dict) -> None:
|
|
692
|
+
"""Compile + execute ``source``; if the last node is an expression, route
|
|
693
|
+
its value through ``__omp_display`` so dataframes/figures render rich."""
|
|
694
|
+
try:
|
|
695
|
+
module = ast.parse(source, mode="exec")
|
|
696
|
+
except SyntaxError:
|
|
697
|
+
raise
|
|
698
|
+
|
|
699
|
+
if not module.body:
|
|
700
|
+
return
|
|
701
|
+
|
|
702
|
+
last = module.body[-1]
|
|
703
|
+
if isinstance(last, ast.Expr):
|
|
704
|
+
body_module = ast.Module(body=module.body[:-1], type_ignores=[])
|
|
705
|
+
expr_module = ast.Expression(body=last.value)
|
|
706
|
+
ast.copy_location(expr_module, last)
|
|
707
|
+
body_code = compile(body_module, "<cell>", "exec")
|
|
708
|
+
expr_code = compile(expr_module, "<cell>", "eval")
|
|
709
|
+
exec(body_code, ns)
|
|
710
|
+
value = eval(expr_code, ns)
|
|
711
|
+
if value is not None:
|
|
712
|
+
__omp_display(value, kind="result")
|
|
713
|
+
return
|
|
714
|
+
|
|
715
|
+
code = compile(module, "<cell>", "exec")
|
|
716
|
+
exec(code, ns)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
# ---------------------------------------------------------------------------
|
|
720
|
+
# Signal handling
|
|
721
|
+
# ---------------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _install_idle_sigint() -> None:
|
|
725
|
+
try:
|
|
726
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
727
|
+
except (OSError, ValueError):
|
|
728
|
+
# Some platforms (Windows in non-console mode) reject this; fine.
|
|
729
|
+
pass
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _install_exec_sigint() -> None:
|
|
733
|
+
try:
|
|
734
|
+
signal.signal(signal.SIGINT, signal.default_int_handler)
|
|
735
|
+
except (OSError, ValueError):
|
|
736
|
+
pass
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _start_parent_watchdog() -> None:
|
|
740
|
+
"""Self-terminate when the host process dies.
|
|
741
|
+
|
|
742
|
+
The main loop only exits when stdin EOFs, which only happens once user
|
|
743
|
+
code finishes and the next ``readline`` call returns. If the host gets
|
|
744
|
+
SIGKILL mid-execution (or any way that skips graceful shutdown) the
|
|
745
|
+
runner would otherwise outlive its parent and keep holding kernel
|
|
746
|
+
state. Poll ``os.getppid()`` instead and ``os._exit`` the moment we get
|
|
747
|
+
reparented \u2014 covers POSIX hosts. Windows has no reliable ppid
|
|
748
|
+
equivalent; there we still bail out on the next stdin read.
|
|
749
|
+
"""
|
|
750
|
+
if os.name != "posix":
|
|
751
|
+
return
|
|
752
|
+
original_ppid = os.getppid()
|
|
753
|
+
if original_ppid <= 1:
|
|
754
|
+
return
|
|
755
|
+
|
|
756
|
+
def watch() -> None:
|
|
757
|
+
while True:
|
|
758
|
+
try:
|
|
759
|
+
if os.getppid() != original_ppid:
|
|
760
|
+
os._exit(0)
|
|
761
|
+
except Exception:
|
|
762
|
+
return
|
|
763
|
+
time.sleep(10)
|
|
764
|
+
|
|
765
|
+
thread = threading.Thread(target=watch, name="omp-parent-watchdog", daemon=True)
|
|
766
|
+
thread.start()
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
# ---------------------------------------------------------------------------
|
|
770
|
+
# Request dispatch
|
|
771
|
+
# ---------------------------------------------------------------------------
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _handle_request(req: dict) -> None:
|
|
775
|
+
if req.get("type") == "exit":
|
|
776
|
+
sys.exit(0)
|
|
777
|
+
|
|
778
|
+
rid = str(req.get("id"))
|
|
779
|
+
code = req.get("code", "")
|
|
780
|
+
_STATE.current_id = rid
|
|
781
|
+
_STATE.cancel_requested = False
|
|
782
|
+
_STATE.execution_count += 1
|
|
783
|
+
_emit({"type": "started", "id": rid})
|
|
784
|
+
|
|
785
|
+
status: str = "ok"
|
|
786
|
+
cancelled = False
|
|
787
|
+
|
|
788
|
+
try:
|
|
789
|
+
transformed = transform_cell(code)
|
|
790
|
+
except SyntaxError as exc:
|
|
791
|
+
_emit_error(rid, exc)
|
|
792
|
+
_emit({
|
|
793
|
+
"type": "done",
|
|
794
|
+
"id": rid,
|
|
795
|
+
"status": "error",
|
|
796
|
+
"executionCount": _STATE.execution_count,
|
|
797
|
+
"cancelled": False,
|
|
798
|
+
})
|
|
799
|
+
_STATE.current_id = None
|
|
800
|
+
return
|
|
801
|
+
|
|
802
|
+
_install_exec_sigint()
|
|
803
|
+
try:
|
|
804
|
+
_exec_source(transformed, _STATE.user_ns)
|
|
805
|
+
except KeyboardInterrupt:
|
|
806
|
+
cancelled = True
|
|
807
|
+
status = "error"
|
|
808
|
+
_emit_error(rid, KeyboardInterrupt("Execution interrupted"))
|
|
809
|
+
except SystemExit:
|
|
810
|
+
raise
|
|
811
|
+
except BaseException as exc: # noqa: BLE001 - we want to surface every user error
|
|
812
|
+
status = "error"
|
|
813
|
+
_emit_error(rid, exc)
|
|
814
|
+
finally:
|
|
815
|
+
_install_idle_sigint()
|
|
816
|
+
try:
|
|
817
|
+
_flush_matplotlib_figures()
|
|
818
|
+
except Exception:
|
|
819
|
+
pass
|
|
820
|
+
|
|
821
|
+
_emit({
|
|
822
|
+
"type": "done",
|
|
823
|
+
"id": rid,
|
|
824
|
+
"status": status,
|
|
825
|
+
"executionCount": _STATE.execution_count,
|
|
826
|
+
"cancelled": cancelled,
|
|
827
|
+
})
|
|
828
|
+
_STATE.current_id = None
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _emit_error(rid: str, exc: BaseException) -> None:
|
|
832
|
+
tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
833
|
+
_emit({
|
|
834
|
+
"type": "error",
|
|
835
|
+
"id": rid,
|
|
836
|
+
"ename": type(exc).__name__,
|
|
837
|
+
"evalue": str(exc),
|
|
838
|
+
"traceback": [line.rstrip("\n") for line in tb_lines],
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
# ---------------------------------------------------------------------------
|
|
843
|
+
# Main loop
|
|
844
|
+
# ---------------------------------------------------------------------------
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def main() -> None:
|
|
848
|
+
sys.stdout = _StreamProxy("stdout")
|
|
849
|
+
sys.stderr = _StreamProxy("stderr")
|
|
850
|
+
_install_idle_sigint()
|
|
851
|
+
_start_parent_watchdog()
|
|
852
|
+
|
|
853
|
+
stdin = sys.__stdin__
|
|
854
|
+
if stdin is None:
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
for raw_line in stdin:
|
|
858
|
+
line = raw_line.strip()
|
|
859
|
+
if not line:
|
|
860
|
+
continue
|
|
861
|
+
try:
|
|
862
|
+
req = json.loads(line)
|
|
863
|
+
except json.JSONDecodeError as exc:
|
|
864
|
+
_emit({
|
|
865
|
+
"type": "error",
|
|
866
|
+
"id": "",
|
|
867
|
+
"ename": "ProtocolError",
|
|
868
|
+
"evalue": f"Invalid JSON request: {exc}",
|
|
869
|
+
"traceback": [],
|
|
870
|
+
})
|
|
871
|
+
continue
|
|
872
|
+
try:
|
|
873
|
+
_handle_request(req)
|
|
874
|
+
except SystemExit:
|
|
875
|
+
return
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
if __name__ == "__main__":
|
|
879
|
+
main()
|