more-compute 0.2.6__py3-none-any.whl → 0.3.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.
- frontend/app/globals.css +38 -133
- frontend/app/layout.tsx +54 -5
- frontend/components/Notebook.tsx +9 -1
- frontend/components/cell/CellButton.tsx +2 -2
- frontend/components/cell/MonacoCell.tsx +1 -15
- frontend/components/output/CellOutput.tsx +77 -17
- frontend/components/output/ErrorDisplay.tsx +3 -28
- frontend/components/popups/MetricsPopup.tsx +42 -7
- frontend/components/popups/PackagesPopup.tsx +2 -1
- frontend/lib/api.ts +6 -2
- frontend/lib/settings.ts +7 -0
- frontend/lib/websocket-native.ts +3 -0
- frontend/styling_README.md +15 -2
- {more_compute-0.2.6.dist-info → more_compute-0.3.0.dist-info}/METADATA +1 -1
- {more_compute-0.2.6.dist-info → more_compute-0.3.0.dist-info}/RECORD +27 -25
- morecompute/__version__.py +1 -1
- morecompute/execution/executor.py +12 -5
- morecompute/execution/worker.py +93 -1
- morecompute/server.py +4 -0
- morecompute/utils/cell_magics.py +713 -0
- morecompute/utils/line_magics.py +949 -0
- morecompute/utils/shell_utils.py +68 -0
- morecompute/utils/special_commands.py +106 -173
- frontend/components/Cell.tsx +0 -383
- {more_compute-0.2.6.dist-info → more_compute-0.3.0.dist-info}/WHEEL +0 -0
- {more_compute-0.2.6.dist-info → more_compute-0.3.0.dist-info}/entry_points.txt +0 -0
- {more_compute-0.2.6.dist-info → more_compute-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {more_compute-0.2.6.dist-info → more_compute-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import io
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import asyncio
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
import platform
|
|
9
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
10
|
+
from typing import Dict, Any, Optional
|
|
11
|
+
from fastapi import WebSocket
|
|
12
|
+
|
|
13
|
+
from .shell_utils import prepare_shell_command, prepare_shell_environment
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CellMagicHandlers:
|
|
17
|
+
"""Handlers for IPython cell magic commands (%%magic)"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, globals_dict: dict, special_handler):
|
|
20
|
+
self.globals_dict = globals_dict
|
|
21
|
+
self.special_handler = special_handler # Reference to parent AsyncSpecialCommandHandler
|
|
22
|
+
self.captured_outputs = {}
|
|
23
|
+
|
|
24
|
+
async def execute_cell_content(self, cell_content: str, result: Dict[str, Any],
|
|
25
|
+
execution_count: int, websocket: Optional[WebSocket] = None,
|
|
26
|
+
capture_stdout: bool = False, capture_stderr: bool = False) -> tuple:
|
|
27
|
+
"""
|
|
28
|
+
Execute cell content that may contain shell commands (!cmd) mixed with Python code.
|
|
29
|
+
|
|
30
|
+
Uses preprocessing to transform shell commands into Python function calls.
|
|
31
|
+
This approach properly handles shell commands inside control structures.
|
|
32
|
+
|
|
33
|
+
Supported:
|
|
34
|
+
- Shell commands on their own lines (can be indented): `!pip install pandas`
|
|
35
|
+
- Shell commands in if/else blocks: `if x:\n !ls`
|
|
36
|
+
- Mixed Python and shell code in same cell
|
|
37
|
+
- Async execution (non-blocking)
|
|
38
|
+
- Cross-platform (Windows/macOS/Linux)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
(stdout_output, stderr_output) if capturing, otherwise (None, None)
|
|
42
|
+
"""
|
|
43
|
+
stdout_capture = io.StringIO() if capture_stdout else None
|
|
44
|
+
stderr_capture = io.StringIO() if capture_stderr else None
|
|
45
|
+
|
|
46
|
+
# Store websocket reference for shell command execution
|
|
47
|
+
self._current_websocket = websocket
|
|
48
|
+
self._capture_mode = (capture_stdout or capture_stderr)
|
|
49
|
+
|
|
50
|
+
# Use preprocessing approach for all cases
|
|
51
|
+
# This transforms !cmd into function calls, preserving Python syntax
|
|
52
|
+
transformed_code = await self._preprocess_cell_content(cell_content, capture_mode=(capture_stdout or capture_stderr))
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
# Compile the code
|
|
56
|
+
compiled_code = compile(transformed_code, '<cell>', 'exec')
|
|
57
|
+
|
|
58
|
+
if capture_stdout or capture_stderr:
|
|
59
|
+
# Execute with output capture in a thread pool to avoid blocking
|
|
60
|
+
# This allows us to send heartbeats while long-running commands execute
|
|
61
|
+
loop = asyncio.get_event_loop()
|
|
62
|
+
|
|
63
|
+
def _exec_with_capture():
|
|
64
|
+
with redirect_stdout(stdout_capture or sys.stdout), \
|
|
65
|
+
redirect_stderr(stderr_capture or sys.stderr):
|
|
66
|
+
exec(compiled_code, self.globals_dict)
|
|
67
|
+
|
|
68
|
+
# Run in thread pool
|
|
69
|
+
exec_future = loop.run_in_executor(None, _exec_with_capture)
|
|
70
|
+
|
|
71
|
+
# Send heartbeats while waiting
|
|
72
|
+
heartbeat_count = 0
|
|
73
|
+
while not exec_future.done():
|
|
74
|
+
heartbeat_count += 1
|
|
75
|
+
if websocket and heartbeat_count % 2 == 0: # Every 2 seconds
|
|
76
|
+
try:
|
|
77
|
+
await websocket.send_json({
|
|
78
|
+
"type": "heartbeat",
|
|
79
|
+
"data": {"status": "executing", "message": "Executing cell..."}
|
|
80
|
+
})
|
|
81
|
+
except:
|
|
82
|
+
pass # WebSocket might be closed
|
|
83
|
+
await asyncio.sleep(1)
|
|
84
|
+
|
|
85
|
+
# Wait for completion
|
|
86
|
+
await exec_future
|
|
87
|
+
else:
|
|
88
|
+
# Execute normally
|
|
89
|
+
exec(compiled_code, self.globals_dict)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
raise e
|
|
92
|
+
finally:
|
|
93
|
+
# Clean up websocket reference
|
|
94
|
+
self._current_websocket = None
|
|
95
|
+
self._capture_mode = False
|
|
96
|
+
|
|
97
|
+
# Return captured outputs
|
|
98
|
+
return (
|
|
99
|
+
stdout_capture.getvalue() if stdout_capture else None,
|
|
100
|
+
stderr_capture.getvalue() if stderr_capture else None
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def _execute_cell_with_async_shell(self, cell_content: str,
|
|
104
|
+
stdout_capture: Optional[io.StringIO],
|
|
105
|
+
stderr_capture: Optional[io.StringIO],
|
|
106
|
+
websocket: Optional[WebSocket]) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Execute cell content line-by-line with async shell command execution.
|
|
109
|
+
Used for %%capture mode to avoid blocking on long pip installs.
|
|
110
|
+
"""
|
|
111
|
+
import re
|
|
112
|
+
|
|
113
|
+
lines = cell_content.split('\n')
|
|
114
|
+
python_lines = []
|
|
115
|
+
|
|
116
|
+
for line in lines:
|
|
117
|
+
# Check if this line is a shell command
|
|
118
|
+
shell_match = re.match(r'^(\s*)!(.+)$', line)
|
|
119
|
+
|
|
120
|
+
if shell_match:
|
|
121
|
+
# Execute accumulated Python code first
|
|
122
|
+
if python_lines:
|
|
123
|
+
python_code = '\n'.join(python_lines)
|
|
124
|
+
if stdout_capture or stderr_capture:
|
|
125
|
+
with redirect_stdout(stdout_capture or sys.stdout), \
|
|
126
|
+
redirect_stderr(stderr_capture or sys.stderr):
|
|
127
|
+
exec(compile(python_code, '<cell>', 'exec'), self.globals_dict)
|
|
128
|
+
else:
|
|
129
|
+
exec(compile(python_code, '<cell>', 'exec'), self.globals_dict)
|
|
130
|
+
python_lines = []
|
|
131
|
+
|
|
132
|
+
# Execute shell command asynchronously
|
|
133
|
+
shell_cmd = shell_match.group(2).strip()
|
|
134
|
+
await self._run_shell_non_blocking(
|
|
135
|
+
shell_cmd, stdout_capture, stderr_capture, websocket
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
# Accumulate Python line
|
|
139
|
+
python_lines.append(line)
|
|
140
|
+
|
|
141
|
+
# Execute remaining Python code
|
|
142
|
+
if python_lines:
|
|
143
|
+
python_code = '\n'.join(python_lines)
|
|
144
|
+
if stdout_capture or stderr_capture:
|
|
145
|
+
with redirect_stdout(stdout_capture or sys.stdout), \
|
|
146
|
+
redirect_stderr(stderr_capture or sys.stderr):
|
|
147
|
+
exec(compile(python_code, '<cell>', 'exec'), self.globals_dict)
|
|
148
|
+
else:
|
|
149
|
+
exec(compile(python_code, '<cell>', 'exec'), self.globals_dict)
|
|
150
|
+
|
|
151
|
+
async def _run_shell_non_blocking(self, cmd: str,
|
|
152
|
+
stdout_capture: Optional[io.StringIO],
|
|
153
|
+
stderr_capture: Optional[io.StringIO],
|
|
154
|
+
websocket: Optional[WebSocket]) -> int:
|
|
155
|
+
"""
|
|
156
|
+
Run shell command in thread pool executor (non-blocking).
|
|
157
|
+
Sends periodic heartbeats to keep frontend alive during long operations.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Return code from subprocess
|
|
161
|
+
"""
|
|
162
|
+
loop = asyncio.get_event_loop()
|
|
163
|
+
|
|
164
|
+
# Prepare platform-specific shell command
|
|
165
|
+
system = platform.system()
|
|
166
|
+
if system == 'Windows':
|
|
167
|
+
shell_cmd = ['cmd', '/c', cmd]
|
|
168
|
+
else:
|
|
169
|
+
shell_cmd = ['/bin/bash', '-c', cmd]
|
|
170
|
+
|
|
171
|
+
# Run blocking subprocess in thread pool
|
|
172
|
+
shell_future = loop.run_in_executor(
|
|
173
|
+
None, # Uses default ThreadPoolExecutor
|
|
174
|
+
lambda: subprocess.run(
|
|
175
|
+
shell_cmd,
|
|
176
|
+
stdout=subprocess.PIPE,
|
|
177
|
+
stderr=subprocess.PIPE,
|
|
178
|
+
text=True,
|
|
179
|
+
env=os.environ.copy(),
|
|
180
|
+
timeout=1800 # 30 minute timeout
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Send heartbeats while waiting
|
|
185
|
+
heartbeat_count = 0
|
|
186
|
+
while not shell_future.done():
|
|
187
|
+
heartbeat_count += 1
|
|
188
|
+
if websocket and heartbeat_count % 2 == 0: # Every 2 seconds
|
|
189
|
+
try:
|
|
190
|
+
await websocket.send_json({
|
|
191
|
+
"type": "heartbeat",
|
|
192
|
+
"data": {"status": "executing", "message": "Running shell command..."}
|
|
193
|
+
})
|
|
194
|
+
except:
|
|
195
|
+
pass # WebSocket might be closed, continue anyway
|
|
196
|
+
await asyncio.sleep(1)
|
|
197
|
+
|
|
198
|
+
# Get result
|
|
199
|
+
proc = await shell_future
|
|
200
|
+
|
|
201
|
+
# Write captured output
|
|
202
|
+
if stdout_capture and proc.stdout:
|
|
203
|
+
stdout_capture.write(proc.stdout)
|
|
204
|
+
if stderr_capture and proc.stderr:
|
|
205
|
+
stderr_capture.write(proc.stderr)
|
|
206
|
+
|
|
207
|
+
return proc.returncode
|
|
208
|
+
|
|
209
|
+
async def _preprocess_cell_content(self, cell_content: str, capture_mode: bool = False) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Preprocess cell content to transform shell commands into Python code.
|
|
212
|
+
This is how IPython handles !commands - it transforms them before execution.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
cell_content: The cell content to preprocess
|
|
216
|
+
capture_mode: Whether this is being called from %%capture (unused for now, but available for future use)
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
!pip install pandas -> _run_shell_command('pip install pandas')
|
|
220
|
+
"""
|
|
221
|
+
import re
|
|
222
|
+
|
|
223
|
+
lines = cell_content.split('\n')
|
|
224
|
+
transformed_lines = []
|
|
225
|
+
|
|
226
|
+
# Create a shell command executor function in globals if not exists
|
|
227
|
+
# Use a closure to capture self reference for capture mode detection
|
|
228
|
+
cell_magic_handler = self # Capture self in closure
|
|
229
|
+
|
|
230
|
+
if '_run_shell_command' not in self.globals_dict:
|
|
231
|
+
def _run_shell_command(cmd: str):
|
|
232
|
+
"""Execute a shell command synchronously with streaming output (injected by cell magic handler)"""
|
|
233
|
+
import subprocess
|
|
234
|
+
import threading
|
|
235
|
+
|
|
236
|
+
# Prepare command and environment (using shared utilities)
|
|
237
|
+
shell_cmd = prepare_shell_command(cmd)
|
|
238
|
+
env = prepare_shell_environment(cmd)
|
|
239
|
+
|
|
240
|
+
# Stream output in real-time (like regular !commands)
|
|
241
|
+
# Use Popen instead of run to get real-time output
|
|
242
|
+
process = subprocess.Popen(
|
|
243
|
+
shell_cmd,
|
|
244
|
+
stdout=subprocess.PIPE,
|
|
245
|
+
stderr=subprocess.PIPE,
|
|
246
|
+
text=True,
|
|
247
|
+
bufsize=1, # Line buffered
|
|
248
|
+
env=env
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Read and print output line by line (real-time streaming)
|
|
252
|
+
def read_stream(stream, output_type):
|
|
253
|
+
"""Read stream line by line and print immediately"""
|
|
254
|
+
try:
|
|
255
|
+
for line in iter(stream.readline, ''):
|
|
256
|
+
if not line:
|
|
257
|
+
break
|
|
258
|
+
# Print immediately (unless in capture mode)
|
|
259
|
+
if not getattr(cell_magic_handler, '_capture_mode', False):
|
|
260
|
+
if output_type == 'stdout':
|
|
261
|
+
print(line, end='')
|
|
262
|
+
sys.stdout.flush() # Force immediate flush for streaming
|
|
263
|
+
else:
|
|
264
|
+
print(line, end='', file=sys.stderr)
|
|
265
|
+
sys.stderr.flush() # Force immediate flush for streaming
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
finally:
|
|
269
|
+
stream.close()
|
|
270
|
+
|
|
271
|
+
# Start threads to read stdout and stderr concurrently
|
|
272
|
+
stdout_thread = threading.Thread(target=read_stream, args=(process.stdout, 'stdout'))
|
|
273
|
+
stderr_thread = threading.Thread(target=read_stream, args=(process.stderr, 'stderr'))
|
|
274
|
+
stdout_thread.daemon = True
|
|
275
|
+
stderr_thread.daemon = True
|
|
276
|
+
stdout_thread.start()
|
|
277
|
+
stderr_thread.start()
|
|
278
|
+
|
|
279
|
+
# Wait for process to complete
|
|
280
|
+
return_code = process.wait()
|
|
281
|
+
|
|
282
|
+
# Wait for output threads to finish
|
|
283
|
+
stdout_thread.join()
|
|
284
|
+
stderr_thread.join()
|
|
285
|
+
|
|
286
|
+
return return_code
|
|
287
|
+
|
|
288
|
+
self.globals_dict['_run_shell_command'] = _run_shell_command
|
|
289
|
+
|
|
290
|
+
# Transform each line
|
|
291
|
+
for line in lines:
|
|
292
|
+
# Match shell commands: " !pip install pandas"
|
|
293
|
+
shell_match = re.match(r'^(\s*)!(.+)$', line)
|
|
294
|
+
|
|
295
|
+
if shell_match:
|
|
296
|
+
indent = shell_match.group(1)
|
|
297
|
+
shell_cmd = shell_match.group(2).strip()
|
|
298
|
+
|
|
299
|
+
# Use repr() for proper escaping of quotes and backslashes
|
|
300
|
+
# This handles all edge cases correctly
|
|
301
|
+
shell_cmd_repr = repr(shell_cmd)
|
|
302
|
+
|
|
303
|
+
# Transform to synchronous function call
|
|
304
|
+
# This preserves Python syntax (shell command becomes valid Python)
|
|
305
|
+
transformed = f"{indent}_run_shell_command({shell_cmd_repr})"
|
|
306
|
+
transformed_lines.append(transformed)
|
|
307
|
+
else:
|
|
308
|
+
# Regular Python line
|
|
309
|
+
transformed_lines.append(line)
|
|
310
|
+
|
|
311
|
+
return '\n'.join(transformed_lines)
|
|
312
|
+
|
|
313
|
+
async def handle_capture(self, args: list, cell_content: str, result: Dict[str, Any],
|
|
314
|
+
start_time: float, execution_count: int,
|
|
315
|
+
websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
316
|
+
"""
|
|
317
|
+
Handle %%capture magic - capture stdout/stderr without displaying.
|
|
318
|
+
Usage: %%capture [output_var] [--no-stdout] [--no-stderr] [--no-display]
|
|
319
|
+
"""
|
|
320
|
+
output_var = None
|
|
321
|
+
no_stdout = "--no-stdout" in args
|
|
322
|
+
no_stderr = "--no-stderr" in args
|
|
323
|
+
|
|
324
|
+
# First non-flag argument is the output variable name
|
|
325
|
+
for arg in args:
|
|
326
|
+
if not arg.startswith('--'):
|
|
327
|
+
output_var = arg
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
capture_stdout = not no_stdout
|
|
331
|
+
capture_stderr = not no_stderr
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
stdout_text, stderr_text = await self.execute_cell_content(
|
|
335
|
+
cell_content, result, execution_count, websocket,
|
|
336
|
+
capture_stdout, capture_stderr
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Store captured output in a variable if specified
|
|
340
|
+
if output_var:
|
|
341
|
+
captured_data = {
|
|
342
|
+
'stdout': stdout_text or '',
|
|
343
|
+
'stderr': stderr_text or ''
|
|
344
|
+
}
|
|
345
|
+
self.globals_dict[output_var] = captured_data
|
|
346
|
+
self.captured_outputs[output_var] = captured_data
|
|
347
|
+
|
|
348
|
+
# Check if there were any errors in stderr (pip failures, etc.)
|
|
349
|
+
if stderr_text and stderr_text.strip():
|
|
350
|
+
# Check for common error indicators
|
|
351
|
+
stderr_lower = stderr_text.lower()
|
|
352
|
+
if any(err in stderr_lower for err in ['error:', 'failed', 'could not', 'unable to']):
|
|
353
|
+
# Add a warning output (not a full error, just FYI)
|
|
354
|
+
result["outputs"].append({
|
|
355
|
+
"output_type": "stream",
|
|
356
|
+
"name": "stderr",
|
|
357
|
+
"text": f" Captured stderr (may contain errors):\n{stderr_text[:500]}{'...' if len(stderr_text) > 500 else ''}\n"
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
# Don't add outputs to result (they're captured, not displayed)
|
|
361
|
+
result["status"] = "ok"
|
|
362
|
+
|
|
363
|
+
# Check if pip install/uninstall occurred and notify
|
|
364
|
+
if websocket and ('pip install' in cell_content or 'pip uninstall' in cell_content):
|
|
365
|
+
try:
|
|
366
|
+
await websocket.send_json({
|
|
367
|
+
"type": "packages_updated",
|
|
368
|
+
"data": {"action": "pip"}
|
|
369
|
+
})
|
|
370
|
+
except Exception:
|
|
371
|
+
pass # Ignore websocket errors
|
|
372
|
+
|
|
373
|
+
if websocket:
|
|
374
|
+
await websocket.send_json({
|
|
375
|
+
"type": "execution_complete",
|
|
376
|
+
"data": {
|
|
377
|
+
"status": "ok",
|
|
378
|
+
"message": "Output captured" + (f" in variable '{output_var}'" if output_var else "")
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
except Exception as e:
|
|
383
|
+
result["status"] = "error"
|
|
384
|
+
result["error"] = {
|
|
385
|
+
"ename": type(e).__name__,
|
|
386
|
+
"evalue": str(e),
|
|
387
|
+
"traceback": [f"Capture magic error: {str(e)}"]
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if websocket:
|
|
391
|
+
await websocket.send_json({
|
|
392
|
+
"type": "execution_error",
|
|
393
|
+
"data": {"error": result["error"]}
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
return result
|
|
397
|
+
|
|
398
|
+
async def handle_time(self, cell_content: str, result: Dict[str, Any],
|
|
399
|
+
start_time: float, execution_count: int,
|
|
400
|
+
websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
401
|
+
"""
|
|
402
|
+
Handle %%time magic - time cell execution and display results.
|
|
403
|
+
"""
|
|
404
|
+
try:
|
|
405
|
+
# Record start time
|
|
406
|
+
cell_start = time.time()
|
|
407
|
+
cpu_start = time.process_time()
|
|
408
|
+
|
|
409
|
+
# Execute cell content
|
|
410
|
+
await self.execute_cell_content(
|
|
411
|
+
cell_content, result, execution_count, websocket
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Calculate timing
|
|
415
|
+
wall_time = time.time() - cell_start
|
|
416
|
+
cpu_time = time.process_time() - cpu_start
|
|
417
|
+
|
|
418
|
+
# Format timing output
|
|
419
|
+
timing_output = f"CPU times: user {cpu_time:.2f} s, sys: 0 s, total: {cpu_time:.2f} s\n"
|
|
420
|
+
timing_output += f"Wall time: {wall_time:.2f} s"
|
|
421
|
+
|
|
422
|
+
# Add timing as stream output
|
|
423
|
+
result["outputs"].append({
|
|
424
|
+
"output_type": "stream",
|
|
425
|
+
"name": "stdout",
|
|
426
|
+
"text": timing_output + "\n"
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
if websocket:
|
|
430
|
+
await websocket.send_json({
|
|
431
|
+
"type": "stream_output",
|
|
432
|
+
"data": {
|
|
433
|
+
"stream": "stdout",
|
|
434
|
+
"text": timing_output + "\n"
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
result["status"] = "error"
|
|
440
|
+
result["error"] = {
|
|
441
|
+
"ename": type(e).__name__,
|
|
442
|
+
"evalue": str(e),
|
|
443
|
+
"traceback": [f"Time magic error: {str(e)}"]
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if websocket:
|
|
447
|
+
await websocket.send_json({
|
|
448
|
+
"type": "execution_error",
|
|
449
|
+
"data": {"error": result["error"]}
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
return result
|
|
453
|
+
|
|
454
|
+
async def handle_timeit(self, args: list, cell_content: str, result: Dict[str, Any],
|
|
455
|
+
start_time: float, execution_count: int,
|
|
456
|
+
websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
457
|
+
"""
|
|
458
|
+
Handle %%timeit magic - time cell execution using timeit module.
|
|
459
|
+
Options: -n<N> (iterations), -r<R> (repeats), -q (quiet)
|
|
460
|
+
"""
|
|
461
|
+
import timeit
|
|
462
|
+
|
|
463
|
+
# Parse arguments
|
|
464
|
+
number = None
|
|
465
|
+
repeat = 7
|
|
466
|
+
quiet = '-q' in args
|
|
467
|
+
|
|
468
|
+
for arg in args:
|
|
469
|
+
if arg.startswith('-n'):
|
|
470
|
+
number = int(arg[2:])
|
|
471
|
+
elif arg.startswith('-r'):
|
|
472
|
+
repeat = int(arg[2:])
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
# Create a timer
|
|
476
|
+
timer = timeit.Timer(
|
|
477
|
+
stmt=cell_content,
|
|
478
|
+
globals=self.globals_dict
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Auto-determine number of iterations if not specified
|
|
482
|
+
if number is None:
|
|
483
|
+
# Determine number of loops automatically
|
|
484
|
+
for i in range(1, 10):
|
|
485
|
+
n = 10 ** i
|
|
486
|
+
try:
|
|
487
|
+
t = timer.timeit(n)
|
|
488
|
+
if t >= 0.2:
|
|
489
|
+
number = n
|
|
490
|
+
break
|
|
491
|
+
except:
|
|
492
|
+
number = 1
|
|
493
|
+
break
|
|
494
|
+
if number is None:
|
|
495
|
+
number = 10 ** 7
|
|
496
|
+
|
|
497
|
+
# Run the timing
|
|
498
|
+
all_runs = timer.repeat(repeat=repeat, number=number)
|
|
499
|
+
best = min(all_runs) / number
|
|
500
|
+
|
|
501
|
+
# Format output
|
|
502
|
+
if best < 1e-6:
|
|
503
|
+
timing_str = f"{best * 1e9:.0f} ns"
|
|
504
|
+
elif best < 1e-3:
|
|
505
|
+
timing_str = f"{best * 1e6:.0f} µs"
|
|
506
|
+
elif best < 1:
|
|
507
|
+
timing_str = f"{best * 1e3:.0f} ms"
|
|
508
|
+
else:
|
|
509
|
+
timing_str = f"{best:.2f} s"
|
|
510
|
+
|
|
511
|
+
output_text = f"{timing_str} ± {(max(all_runs) - min(all_runs)) / number * 1e6:.0f} µs per loop (mean ± std. dev. of {repeat} runs, {number} loops each)\n"
|
|
512
|
+
|
|
513
|
+
if not quiet:
|
|
514
|
+
result["outputs"].append({
|
|
515
|
+
"output_type": "stream",
|
|
516
|
+
"name": "stdout",
|
|
517
|
+
"text": output_text
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
if websocket:
|
|
521
|
+
await websocket.send_json({
|
|
522
|
+
"type": "stream_output",
|
|
523
|
+
"data": {"stream": "stdout", "text": output_text}
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
except Exception as e:
|
|
527
|
+
result["status"] = "error"
|
|
528
|
+
result["error"] = {
|
|
529
|
+
"ename": type(e).__name__,
|
|
530
|
+
"evalue": str(e),
|
|
531
|
+
"traceback": [f"Timeit magic error: {str(e)}"]
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if websocket:
|
|
535
|
+
await websocket.send_json({
|
|
536
|
+
"type": "execution_error",
|
|
537
|
+
"data": {"error": result["error"]}
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
return result
|
|
541
|
+
|
|
542
|
+
async def handle_writefile(self, args: list, cell_content: str, result: Dict[str, Any],
|
|
543
|
+
start_time: float, websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
544
|
+
"""
|
|
545
|
+
Handle %%writefile magic - write cell contents to file.
|
|
546
|
+
Usage: %%writefile [-a/--append] filename
|
|
547
|
+
"""
|
|
548
|
+
if not args:
|
|
549
|
+
result["status"] = "error"
|
|
550
|
+
result["error"] = {
|
|
551
|
+
"ename": "UsageError",
|
|
552
|
+
"evalue": "%%writefile requires a filename",
|
|
553
|
+
"traceback": ["Usage: %%writefile [-a/--append] filename"]
|
|
554
|
+
}
|
|
555
|
+
return result
|
|
556
|
+
|
|
557
|
+
append_mode = '-a' in args or '--append' in args
|
|
558
|
+
filename = args[-1] # Last argument is the filename
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
mode = 'a' if append_mode else 'w'
|
|
562
|
+
with open(filename, mode) as f:
|
|
563
|
+
f.write(cell_content)
|
|
564
|
+
if not cell_content.endswith('\n'):
|
|
565
|
+
f.write('\n')
|
|
566
|
+
|
|
567
|
+
action = "Appending to" if append_mode else "Writing"
|
|
568
|
+
output_text = f"{action} {filename}\n"
|
|
569
|
+
|
|
570
|
+
result["outputs"].append({
|
|
571
|
+
"output_type": "stream",
|
|
572
|
+
"name": "stdout",
|
|
573
|
+
"text": output_text
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
if websocket:
|
|
577
|
+
await websocket.send_json({
|
|
578
|
+
"type": "stream_output",
|
|
579
|
+
"data": {"stream": "stdout", "text": output_text}
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
except Exception as e:
|
|
583
|
+
result["status"] = "error"
|
|
584
|
+
result["error"] = {
|
|
585
|
+
"ename": type(e).__name__,
|
|
586
|
+
"evalue": str(e),
|
|
587
|
+
"traceback": [f"Writefile magic error: {str(e)}"]
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if websocket:
|
|
591
|
+
await websocket.send_json({
|
|
592
|
+
"type": "execution_error",
|
|
593
|
+
"data": {"error": result["error"]}
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
return result
|
|
597
|
+
|
|
598
|
+
async def handle_bash(self, cell_content: str, result: Dict[str, Any],
|
|
599
|
+
start_time: float, websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
600
|
+
"""
|
|
601
|
+
Handle %%bash / %%sh magic - run cell as bash script.
|
|
602
|
+
"""
|
|
603
|
+
try:
|
|
604
|
+
# Execute as shell script
|
|
605
|
+
shell_result = {
|
|
606
|
+
"outputs": [],
|
|
607
|
+
"status": "ok"
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
# Write cell content to temporary script and execute
|
|
611
|
+
import tempfile
|
|
612
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
|
|
613
|
+
f.write(cell_content)
|
|
614
|
+
script_path = f.name
|
|
615
|
+
|
|
616
|
+
try:
|
|
617
|
+
await self.special_handler._execute_shell_command(
|
|
618
|
+
f"bash {script_path}", shell_result, start_time, websocket
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Merge results
|
|
622
|
+
result["outputs"].extend(shell_result.get("outputs", []))
|
|
623
|
+
if shell_result.get("status") == "error":
|
|
624
|
+
result["status"] = "error"
|
|
625
|
+
if "error" in shell_result:
|
|
626
|
+
result["error"] = shell_result["error"]
|
|
627
|
+
finally:
|
|
628
|
+
# Clean up temp file
|
|
629
|
+
os.unlink(script_path)
|
|
630
|
+
|
|
631
|
+
except Exception as e:
|
|
632
|
+
result["status"] = "error"
|
|
633
|
+
result["error"] = {
|
|
634
|
+
"ename": type(e).__name__,
|
|
635
|
+
"evalue": str(e),
|
|
636
|
+
"traceback": [f"Bash magic error: {str(e)}"]
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if websocket:
|
|
640
|
+
await websocket.send_json({
|
|
641
|
+
"type": "execution_error",
|
|
642
|
+
"data": {"error": result["error"]}
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
return result
|
|
646
|
+
|
|
647
|
+
async def handle_html(self, cell_content: str, result: Dict[str, Any],
|
|
648
|
+
start_time: float, websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
649
|
+
"""
|
|
650
|
+
Handle %%html magic - render cell as HTML.
|
|
651
|
+
"""
|
|
652
|
+
try:
|
|
653
|
+
result["outputs"].append({
|
|
654
|
+
"output_type": "display_data",
|
|
655
|
+
"data": {
|
|
656
|
+
"text/html": cell_content,
|
|
657
|
+
"text/plain": f"<IPython.core.display.HTML object>"
|
|
658
|
+
}
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
if websocket:
|
|
662
|
+
await websocket.send_json({
|
|
663
|
+
"type": "display_data",
|
|
664
|
+
"data": {
|
|
665
|
+
"data": {
|
|
666
|
+
"text/html": cell_content
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
except Exception as e:
|
|
672
|
+
result["status"] = "error"
|
|
673
|
+
result["error"] = {
|
|
674
|
+
"ename": type(e).__name__,
|
|
675
|
+
"evalue": str(e),
|
|
676
|
+
"traceback": [f"HTML magic error: {str(e)}"]
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return result
|
|
680
|
+
|
|
681
|
+
async def handle_markdown(self, cell_content: str, result: Dict[str, Any],
|
|
682
|
+
start_time: float, websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
|
|
683
|
+
"""
|
|
684
|
+
Handle %%markdown magic - render cell as Markdown.
|
|
685
|
+
"""
|
|
686
|
+
try:
|
|
687
|
+
result["outputs"].append({
|
|
688
|
+
"output_type": "display_data",
|
|
689
|
+
"data": {
|
|
690
|
+
"text/markdown": cell_content,
|
|
691
|
+
"text/plain": f"<IPython.core.display.Markdown object>"
|
|
692
|
+
}
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
if websocket:
|
|
696
|
+
await websocket.send_json({
|
|
697
|
+
"type": "display_data",
|
|
698
|
+
"data": {
|
|
699
|
+
"data": {
|
|
700
|
+
"text/markdown": cell_content
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
except Exception as e:
|
|
706
|
+
result["status"] = "error"
|
|
707
|
+
result["error"] = {
|
|
708
|
+
"ename": type(e).__name__,
|
|
709
|
+
"evalue": str(e),
|
|
710
|
+
"traceback": [f"Markdown magic error: {str(e)}"]
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return result
|