more-compute 0.2.6__py3-none-any.whl → 0.3.1__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.
@@ -0,0 +1,949 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import shlex
5
+ import subprocess
6
+ import importlib
7
+ from typing import Dict, Any, Optional, List
8
+ from fastapi import WebSocket
9
+
10
+
11
+ class LineMagicHandlers:
12
+ """Handlers for IPython line magic commands (%magic)"""
13
+
14
+ def __init__(self, globals_dict: dict):
15
+ self.globals_dict = globals_dict
16
+ self.directory_stack = []
17
+ self.directory_history = []
18
+ self.loaded_extensions = {} # Track loaded extensions
19
+ self.matplotlib_backend = None # Track matplotlib backend
20
+
21
+ async def handle_pwd(self, args: list, result: Dict[str, Any],
22
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
23
+ """Handle %pwd - print working directory"""
24
+ try:
25
+ pwd = os.getcwd()
26
+ output_data = {
27
+ "output_type": "execute_result",
28
+ "execution_count": None,
29
+ "data": {
30
+ "text/plain": f"'{pwd}'"
31
+ }
32
+ }
33
+ result["outputs"].append(output_data)
34
+
35
+ if websocket:
36
+ await websocket.send_json({
37
+ "type": "execute_result",
38
+ "data": {"data": output_data["data"]}
39
+ })
40
+
41
+ except Exception as e:
42
+ result["status"] = "error"
43
+ result["error"] = {
44
+ "ename": type(e).__name__,
45
+ "evalue": str(e),
46
+ "traceback": [f"PWD magic error: {str(e)}"]
47
+ }
48
+
49
+ return result
50
+
51
+ async def handle_cd(self, args: list, result: Dict[str, Any],
52
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
53
+ """Handle %cd - change directory"""
54
+ try:
55
+ if not args:
56
+ # No args - go to home directory
57
+ new_dir = os.path.expanduser('~')
58
+ else:
59
+ new_dir = os.path.expanduser(args[0])
60
+
61
+ old_dir = os.getcwd()
62
+ os.chdir(new_dir)
63
+ self.directory_history.append(new_dir)
64
+
65
+ output_text = os.getcwd() + "\n"
66
+ result["outputs"].append({
67
+ "output_type": "stream",
68
+ "name": "stdout",
69
+ "text": output_text
70
+ })
71
+
72
+ if websocket:
73
+ await websocket.send_json({
74
+ "type": "stream_output",
75
+ "data": {"stream": "stdout", "text": output_text}
76
+ })
77
+
78
+ except Exception as e:
79
+ result["status"] = "error"
80
+ result["error"] = {
81
+ "ename": type(e).__name__,
82
+ "evalue": str(e),
83
+ "traceback": [f"CD magic error: {str(e)}"]
84
+ }
85
+
86
+ return result
87
+
88
+ async def handle_ls(self, args: list, result: Dict[str, Any],
89
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
90
+ """Handle %ls - list directory contents"""
91
+ try:
92
+ path = args[0] if args else '.'
93
+ items = os.listdir(path)
94
+ items.sort()
95
+
96
+ output_text = '\n'.join(items) + "\n"
97
+ result["outputs"].append({
98
+ "output_type": "stream",
99
+ "name": "stdout",
100
+ "text": output_text
101
+ })
102
+
103
+ if websocket:
104
+ await websocket.send_json({
105
+ "type": "stream_output",
106
+ "data": {"stream": "stdout", "text": output_text}
107
+ })
108
+
109
+ except Exception as e:
110
+ result["status"] = "error"
111
+ result["error"] = {
112
+ "ename": type(e).__name__,
113
+ "evalue": str(e),
114
+ "traceback": [f"LS magic error: {str(e)}"]
115
+ }
116
+
117
+ return result
118
+
119
+ async def handle_env(self, args: list, result: Dict[str, Any],
120
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
121
+ """
122
+ Handle %env - get/set environment variables.
123
+ Usage:
124
+ %env - list all env vars
125
+ %env VAR - get value of VAR
126
+ %env VAR=value - set VAR to value
127
+ %env VAR=$OTHER - set VAR to value of OTHER (with expansion)
128
+ """
129
+ try:
130
+ if not args:
131
+ # List all environment variables
132
+ env_text = '\n'.join([f"{k}={v}" for k, v in sorted(os.environ.items())])
133
+ output_text = env_text + "\n"
134
+ elif '=' in args[0]:
135
+ # Set environment variable
136
+ var_assignment = ' '.join(args)
137
+ var_name, var_value = var_assignment.split('=', 1)
138
+
139
+ # Handle $VAR expansion
140
+ if var_value.startswith('$'):
141
+ var_value = os.environ.get(var_value[1:], '')
142
+
143
+ os.environ[var_name] = var_value
144
+ output_text = f"env: {var_name}={var_value}\n"
145
+ else:
146
+ # Get environment variable
147
+ var_name = args[0]
148
+ var_value = os.environ.get(var_name, '')
149
+ output_text = var_value + "\n" if var_value else f"env: {var_name} not set\n"
150
+
151
+ result["outputs"].append({
152
+ "output_type": "stream",
153
+ "name": "stdout",
154
+ "text": output_text
155
+ })
156
+
157
+ if websocket:
158
+ await websocket.send_json({
159
+ "type": "stream_output",
160
+ "data": {"stream": "stdout", "text": output_text}
161
+ })
162
+
163
+ except Exception as e:
164
+ result["status"] = "error"
165
+ result["error"] = {
166
+ "ename": type(e).__name__,
167
+ "evalue": str(e),
168
+ "traceback": [f"ENV magic error: {str(e)}"]
169
+ }
170
+
171
+ return result
172
+
173
+ async def handle_who(self, args: list, result: Dict[str, Any],
174
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
175
+ """
176
+ Handle %who - list interactive variables.
177
+ Usage: %who [type] - optionally filter by type (e.g., %who int, %who str)
178
+ """
179
+ try:
180
+ # Get all user-defined variables (exclude built-ins and private vars)
181
+ user_vars = {k: v for k, v in self.globals_dict.items()
182
+ if not k.startswith('_') and k not in ['In', 'Out']}
183
+
184
+ # Filter by type if specified
185
+ if args:
186
+ type_name = args[0]
187
+ type_map = {
188
+ 'int': int,
189
+ 'float': float,
190
+ 'str': str,
191
+ 'list': list,
192
+ 'dict': dict,
193
+ 'tuple': tuple,
194
+ 'set': set,
195
+ }
196
+ if type_name in type_map:
197
+ user_vars = {k: v for k, v in user_vars.items()
198
+ if isinstance(v, type_map[type_name])}
199
+
200
+ # Format output
201
+ var_names = sorted(user_vars.keys())
202
+ output_text = '\t'.join(var_names) + "\n" if var_names else "Interactive namespace is empty.\n"
203
+
204
+ result["outputs"].append({
205
+ "output_type": "stream",
206
+ "name": "stdout",
207
+ "text": output_text
208
+ })
209
+
210
+ if websocket:
211
+ await websocket.send_json({
212
+ "type": "stream_output",
213
+ "data": {"stream": "stdout", "text": output_text}
214
+ })
215
+
216
+ except Exception as e:
217
+ result["status"] = "error"
218
+ result["error"] = {
219
+ "ename": type(e).__name__,
220
+ "evalue": str(e),
221
+ "traceback": [f"WHO magic error: {str(e)}"]
222
+ }
223
+
224
+ return result
225
+
226
+ async def handle_whos(self, args: list, result: Dict[str, Any],
227
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
228
+ """
229
+ Handle %whos - list variables with type and size information.
230
+ """
231
+ try:
232
+ # Get all user-defined variables
233
+ user_vars = {k: v for k, v in self.globals_dict.items()
234
+ if not k.startswith('_') and k not in ['In', 'Out']}
235
+
236
+ if not user_vars:
237
+ output_text = "Interactive namespace is empty.\n"
238
+ else:
239
+ # Build table header
240
+ lines = []
241
+ lines.append("Variable Type Data/Info")
242
+ lines.append("-" * 50)
243
+
244
+ for name in sorted(user_vars.keys()):
245
+ var = user_vars[name]
246
+ var_type = type(var).__name__
247
+
248
+ # Get size/shape info
249
+ if hasattr(var, 'shape'): # NumPy arrays, pandas DataFrames
250
+ info = f"shape: {var.shape}"
251
+ elif hasattr(var, '__len__'):
252
+ try:
253
+ info = f"n={len(var)}"
254
+ except:
255
+ info = ""
256
+ else:
257
+ info = str(var)[:50] # First 50 chars
258
+
259
+ lines.append(f"{name:<10} {var_type:<11} {info}")
260
+
261
+ output_text = '\n'.join(lines) + "\n"
262
+
263
+ result["outputs"].append({
264
+ "output_type": "stream",
265
+ "name": "stdout",
266
+ "text": output_text
267
+ })
268
+
269
+ if websocket:
270
+ await websocket.send_json({
271
+ "type": "stream_output",
272
+ "data": {"stream": "stdout", "text": output_text}
273
+ })
274
+
275
+ except Exception as e:
276
+ result["status"] = "error"
277
+ result["error"] = {
278
+ "ename": type(e).__name__,
279
+ "evalue": str(e),
280
+ "traceback": [f"WHOS magic error: {str(e)}"]
281
+ }
282
+
283
+ return result
284
+
285
+ async def handle_time(self, args: list, result: Dict[str, Any],
286
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
287
+ """
288
+ Handle %time - time single statement execution.
289
+ Usage: %time statement
290
+ """
291
+ try:
292
+ if not args:
293
+ result["status"] = "error"
294
+ result["error"] = {
295
+ "ename": "UsageError",
296
+ "evalue": "%time requires a statement to time",
297
+ "traceback": ["Usage: %time statement"]
298
+ }
299
+ return result
300
+
301
+ statement = ' '.join(args)
302
+
303
+ # Time the execution
304
+ start_wall = time.time()
305
+ start_cpu = time.process_time()
306
+
307
+ # Execute the statement
308
+ exec(statement, self.globals_dict)
309
+
310
+ wall_time = time.time() - start_wall
311
+ cpu_time = time.process_time() - start_cpu
312
+
313
+ # Format output
314
+ output_text = f"CPU times: user {cpu_time:.2f} s, sys: 0 s, total: {cpu_time:.2f} s\n"
315
+ output_text += f"Wall time: {wall_time:.2f} s\n"
316
+
317
+ result["outputs"].append({
318
+ "output_type": "stream",
319
+ "name": "stdout",
320
+ "text": output_text
321
+ })
322
+
323
+ if websocket:
324
+ await websocket.send_json({
325
+ "type": "stream_output",
326
+ "data": {"stream": "stdout", "text": output_text}
327
+ })
328
+
329
+ except Exception as e:
330
+ result["status"] = "error"
331
+ result["error"] = {
332
+ "ename": type(e).__name__,
333
+ "evalue": str(e),
334
+ "traceback": [f"TIME magic error: {str(e)}"]
335
+ }
336
+
337
+ return result
338
+
339
+ async def handle_pip(self, args: list, result: Dict[str, Any],
340
+ special_handler, websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
341
+ """
342
+ Handle %pip - run pip within the kernel.
343
+ This delegates to the shell command handler.
344
+ """
345
+ pip_command = 'pip ' + ' '.join(args)
346
+ return await special_handler._execute_shell_command(
347
+ pip_command, result, time.time(), websocket
348
+ )
349
+
350
+ async def handle_load(self, args: list, result: Dict[str, Any],
351
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
352
+ """
353
+ Handle %load - load code from file or URL.
354
+ Usage: %load filename
355
+ """
356
+ try:
357
+ if not args:
358
+ result["status"] = "error"
359
+ result["error"] = {
360
+ "ename": "UsageError",
361
+ "evalue": "%load requires a filename",
362
+ "traceback": ["Usage: %load filename"]
363
+ }
364
+ return result
365
+
366
+ source = args[0]
367
+
368
+ # Check if it's a URL
369
+ if source.startswith('http://') or source.startswith('https://'):
370
+ import urllib.request
371
+ with urllib.request.urlopen(source) as response:
372
+ code = response.read().decode('utf-8')
373
+ else:
374
+ # It's a file
375
+ with open(source, 'r') as f:
376
+ code = f.read()
377
+
378
+ # In IPython, %load replaces the cell content with the loaded code
379
+ # For now, we'll just display it
380
+ output_text = f"# Loaded from {source}\n{code}\n"
381
+
382
+ result["outputs"].append({
383
+ "output_type": "stream",
384
+ "name": "stdout",
385
+ "text": output_text
386
+ })
387
+
388
+ if websocket:
389
+ await websocket.send_json({
390
+ "type": "stream_output",
391
+ "data": {"stream": "stdout", "text": output_text}
392
+ })
393
+
394
+ except Exception as e:
395
+ result["status"] = "error"
396
+ result["error"] = {
397
+ "ename": type(e).__name__,
398
+ "evalue": str(e),
399
+ "traceback": [f"LOAD magic error: {str(e)}"]
400
+ }
401
+
402
+ return result
403
+
404
+ async def handle_reset(self, args: list, result: Dict[str, Any],
405
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
406
+ """
407
+ Handle %reset - clear namespace.
408
+ Usage: %reset [-f] [-s]
409
+ -f: force (no confirmation)
410
+ -s: soft (keep internal variables)
411
+ """
412
+ try:
413
+ force = '-f' in args
414
+ soft = '-s' in args
415
+
416
+ # For now, implement force mode only
417
+ if force or True: # Always reset for now
418
+ # Get list of user variables to delete
419
+ user_vars = [k for k in self.globals_dict.keys()
420
+ if not k.startswith('_') and k not in ['In', 'Out']]
421
+
422
+ for var in user_vars:
423
+ del self.globals_dict[var]
424
+
425
+ output_text = "All user variables have been reset.\n"
426
+ else:
427
+ output_text = "Reset cancelled. Use %reset -f to force reset.\n"
428
+
429
+ result["outputs"].append({
430
+ "output_type": "stream",
431
+ "name": "stdout",
432
+ "text": output_text
433
+ })
434
+
435
+ if websocket:
436
+ await websocket.send_json({
437
+ "type": "stream_output",
438
+ "data": {"stream": "stdout", "text": output_text}
439
+ })
440
+
441
+ except Exception as e:
442
+ result["status"] = "error"
443
+ result["error"] = {
444
+ "ename": type(e).__name__,
445
+ "evalue": str(e),
446
+ "traceback": [f"RESET magic error: {str(e)}"]
447
+ }
448
+
449
+ return result
450
+
451
+ async def handle_lsmagic(self, args: list, result: Dict[str, Any],
452
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
453
+ """Handle %lsmagic - list available magic commands"""
454
+ try:
455
+ available_line = [
456
+ 'cd', 'pwd', 'ls', 'env', 'who', 'whos', 'time', 'timeit', 'pip',
457
+ 'load', 'reset', 'lsmagic', 'matplotlib', 'load_ext', 'reload_ext',
458
+ 'unload_ext', 'run'
459
+ ]
460
+ available_cell = [
461
+ 'capture', 'time', 'timeit', 'writefile', 'bash', 'sh',
462
+ 'html', 'markdown'
463
+ ]
464
+
465
+ output_text = "Available line magics:\n"
466
+ output_text += "%" + " %".join(available_line) + "\n\n"
467
+ output_text += "Available cell magics:\n"
468
+ output_text += "%%" + " %%".join(available_cell) + "\n"
469
+
470
+ result["outputs"].append({
471
+ "output_type": "stream",
472
+ "name": "stdout",
473
+ "text": output_text
474
+ })
475
+
476
+ if websocket:
477
+ await websocket.send_json({
478
+ "type": "stream_output",
479
+ "data": {"stream": "stdout", "text": output_text}
480
+ })
481
+
482
+ except Exception as e:
483
+ result["status"] = "error"
484
+ result["error"] = {
485
+ "ename": type(e).__name__,
486
+ "evalue": str(e),
487
+ "traceback": [f"LSMAGIC magic error: {str(e)}"]
488
+ }
489
+
490
+ return result
491
+
492
+ async def handle_matplotlib(self, args: list, result: Dict[str, Any],
493
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
494
+ """
495
+ Handle %matplotlib - setup matplotlib for interactive use.
496
+ Usage: %matplotlib [backend]
497
+ Common backends: inline, notebook, widget, qt, tk
498
+ """
499
+ try:
500
+ backend = args[0] if args else 'inline'
501
+
502
+ # Import matplotlib
503
+ import matplotlib
504
+ import matplotlib.pyplot as plt
505
+
506
+ # Configure based on backend
507
+ if backend == 'inline':
508
+ # For inline backend, configure matplotlib
509
+ matplotlib.use('Agg') # Non-interactive backend
510
+ self.matplotlib_backend = 'inline'
511
+
512
+ # Configure for better inline display
513
+ try:
514
+ from IPython.display import set_matplotlib_formats
515
+ set_matplotlib_formats('retina')
516
+ except:
517
+ pass
518
+
519
+ elif backend == 'notebook':
520
+ # Interactive notebook backend
521
+ try:
522
+ import ipympl
523
+ matplotlib.use('module://ipympl.backend_nbagg')
524
+ self.matplotlib_backend = 'notebook'
525
+ except ImportError:
526
+ output_text = "Warning: ipympl not installed. Falling back to inline.\n"
527
+ result["outputs"].append({
528
+ "output_type": "stream",
529
+ "name": "stderr",
530
+ "text": output_text
531
+ })
532
+ matplotlib.use('Agg')
533
+ self.matplotlib_backend = 'inline'
534
+
535
+ elif backend in ['qt', 'qt5', 'qt4']:
536
+ matplotlib.use('Qt5Agg')
537
+ self.matplotlib_backend = backend
538
+
539
+ elif backend == 'tk':
540
+ matplotlib.use('TkAgg')
541
+ self.matplotlib_backend = backend
542
+
543
+ elif backend == 'widget':
544
+ # Jupyter widgets backend
545
+ try:
546
+ import ipympl
547
+ matplotlib.use('module://ipympl.backend_nbagg')
548
+ self.matplotlib_backend = 'widget'
549
+ except ImportError:
550
+ output_text = "Warning: ipympl not installed. Install with: pip install ipympl\n"
551
+ result["outputs"].append({
552
+ "output_type": "stream",
553
+ "name": "stderr",
554
+ "text": output_text
555
+ })
556
+ matplotlib.use('Agg')
557
+ self.matplotlib_backend = 'inline'
558
+
559
+ else:
560
+ matplotlib.use(backend)
561
+ self.matplotlib_backend = backend
562
+
563
+ # Store in globals for easy access
564
+ self.globals_dict['plt'] = plt
565
+
566
+ output_text = f"Using matplotlib backend: {self.matplotlib_backend}\n"
567
+ result["outputs"].append({
568
+ "output_type": "stream",
569
+ "name": "stdout",
570
+ "text": output_text
571
+ })
572
+
573
+ if websocket:
574
+ await websocket.send_json({
575
+ "type": "stream_output",
576
+ "data": {"stream": "stdout", "text": output_text}
577
+ })
578
+
579
+ except Exception as e:
580
+ result["status"] = "error"
581
+ result["error"] = {
582
+ "ename": type(e).__name__,
583
+ "evalue": str(e),
584
+ "traceback": [f"MATPLOTLIB magic error: {str(e)}"]
585
+ }
586
+
587
+ if websocket:
588
+ await websocket.send_json({
589
+ "type": "execution_error",
590
+ "data": {"error": result["error"]}
591
+ })
592
+
593
+ return result
594
+
595
+ async def handle_load_ext(self, args: list, result: Dict[str, Any],
596
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
597
+ """
598
+ Handle %load_ext - load IPython extension.
599
+ Usage: %load_ext extension_name
600
+ Common extensions: autoreload, tensorboard
601
+ """
602
+ try:
603
+ if not args:
604
+ result["status"] = "error"
605
+ result["error"] = {
606
+ "ename": "UsageError",
607
+ "evalue": "%load_ext requires an extension name",
608
+ "traceback": ["Usage: %load_ext extension_name"]
609
+ }
610
+ return result
611
+
612
+ ext_name = args[0]
613
+
614
+ # Check if already loaded
615
+ if ext_name in self.loaded_extensions:
616
+ output_text = f"The {ext_name} extension is already loaded.\n"
617
+ result["outputs"].append({
618
+ "output_type": "stream",
619
+ "name": "stdout",
620
+ "text": output_text
621
+ })
622
+ return result
623
+
624
+ # Handle special built-in extensions
625
+ if ext_name == 'autoreload':
626
+ # Basic autoreload implementation
627
+ output_text = "Loaded autoreload extension (basic implementation)\n"
628
+ output_text += "Note: Full autoreload functionality requires IPython kernel\n"
629
+ self.loaded_extensions[ext_name] = {'type': 'builtin', 'config': {}}
630
+
631
+ elif ext_name == 'tensorboard':
632
+ # TensorBoard extension
633
+ try:
634
+ import tensorboard
635
+ output_text = f"Loaded tensorboard extension\n"
636
+ self.loaded_extensions[ext_name] = {'type': 'builtin', 'module': tensorboard}
637
+ except ImportError:
638
+ raise ImportError("TensorBoard not installed. Install with: pip install tensorboard")
639
+
640
+ else:
641
+ # Try to load as a Python module
642
+ try:
643
+ module = importlib.import_module(ext_name)
644
+
645
+ # Look for IPython extension API
646
+ if hasattr(module, 'load_ipython_extension'):
647
+ # Would need IPython shell instance here
648
+ # For now, just load the module
649
+ self.loaded_extensions[ext_name] = {'type': 'module', 'module': module}
650
+ output_text = f"Loaded extension: {ext_name}\n"
651
+ else:
652
+ # Just a regular module
653
+ self.loaded_extensions[ext_name] = {'type': 'module', 'module': module}
654
+ output_text = f"Loaded module: {ext_name} (no IPython extension API found)\n"
655
+
656
+ except ImportError as e:
657
+ raise ImportError(f"Could not load extension {ext_name}: {str(e)}")
658
+
659
+ result["outputs"].append({
660
+ "output_type": "stream",
661
+ "name": "stdout",
662
+ "text": output_text
663
+ })
664
+
665
+ if websocket:
666
+ await websocket.send_json({
667
+ "type": "stream_output",
668
+ "data": {"stream": "stdout", "text": output_text}
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"LOAD_EXT magic error: {str(e)}"]
677
+ }
678
+
679
+ if websocket:
680
+ await websocket.send_json({
681
+ "type": "execution_error",
682
+ "data": {"error": result["error"]}
683
+ })
684
+
685
+ return result
686
+
687
+ async def handle_reload_ext(self, args: list, result: Dict[str, Any],
688
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
689
+ """Handle %reload_ext - reload IPython extension"""
690
+ try:
691
+ if not args:
692
+ result["status"] = "error"
693
+ result["error"] = {
694
+ "ename": "UsageError",
695
+ "evalue": "%reload_ext requires an extension name",
696
+ "traceback": ["Usage: %reload_ext extension_name"]
697
+ }
698
+ return result
699
+
700
+ ext_name = args[0]
701
+
702
+ if ext_name not in self.loaded_extensions:
703
+ output_text = f"Extension {ext_name} is not loaded. Use %load_ext first.\n"
704
+ else:
705
+ # Unload and reload
706
+ ext_info = self.loaded_extensions[ext_name]
707
+ if ext_info['type'] == 'module':
708
+ module = ext_info['module']
709
+ importlib.reload(module)
710
+ output_text = f"Reloaded extension: {ext_name}\n"
711
+ else:
712
+ output_text = f"Reloaded extension: {ext_name}\n"
713
+
714
+ result["outputs"].append({
715
+ "output_type": "stream",
716
+ "name": "stdout",
717
+ "text": output_text
718
+ })
719
+
720
+ if websocket:
721
+ await websocket.send_json({
722
+ "type": "stream_output",
723
+ "data": {"stream": "stdout", "text": output_text}
724
+ })
725
+
726
+ except Exception as e:
727
+ result["status"] = "error"
728
+ result["error"] = {
729
+ "ename": type(e).__name__,
730
+ "evalue": str(e),
731
+ "traceback": [f"RELOAD_EXT magic error: {str(e)}"]
732
+ }
733
+
734
+ return result
735
+
736
+ async def handle_unload_ext(self, args: list, result: Dict[str, Any],
737
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
738
+ """Handle %unload_ext - unload IPython extension"""
739
+ try:
740
+ if not args:
741
+ result["status"] = "error"
742
+ result["error"] = {
743
+ "ename": "UsageError",
744
+ "evalue": "%unload_ext requires an extension name",
745
+ "traceback": ["Usage: %unload_ext extension_name"]
746
+ }
747
+ return result
748
+
749
+ ext_name = args[0]
750
+
751
+ if ext_name in self.loaded_extensions:
752
+ del self.loaded_extensions[ext_name]
753
+ output_text = f"Unloaded extension: {ext_name}\n"
754
+ else:
755
+ output_text = f"Extension {ext_name} is not loaded.\n"
756
+
757
+ result["outputs"].append({
758
+ "output_type": "stream",
759
+ "name": "stdout",
760
+ "text": output_text
761
+ })
762
+
763
+ if websocket:
764
+ await websocket.send_json({
765
+ "type": "stream_output",
766
+ "data": {"stream": "stdout", "text": output_text}
767
+ })
768
+
769
+ except Exception as e:
770
+ result["status"] = "error"
771
+ result["error"] = {
772
+ "ename": type(e).__name__,
773
+ "evalue": str(e),
774
+ "traceback": [f"UNLOAD_EXT magic error: {str(e)}"]
775
+ }
776
+
777
+ return result
778
+
779
+ async def handle_timeit(self, args: list, result: Dict[str, Any],
780
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
781
+ """
782
+ Handle %timeit - time statement execution (line magic version).
783
+ Usage: %timeit [-n<N>] [-r<R>] statement
784
+ """
785
+ import timeit
786
+
787
+ # Parse arguments
788
+ number = None
789
+ repeat = 7
790
+ quiet = '-q' in args
791
+
792
+ # Remove flags and get statement
793
+ statement_parts = []
794
+ for arg in args:
795
+ if arg.startswith('-n'):
796
+ number = int(arg[2:]) if len(arg) > 2 else None
797
+ elif arg.startswith('-r'):
798
+ repeat = int(arg[2:]) if len(arg) > 2 else 7
799
+ elif arg == '-q':
800
+ quiet = True
801
+ else:
802
+ statement_parts.append(arg)
803
+
804
+ statement = ' '.join(statement_parts)
805
+
806
+ if not statement:
807
+ result["status"] = "error"
808
+ result["error"] = {
809
+ "ename": "UsageError",
810
+ "evalue": "%timeit requires a statement to time",
811
+ "traceback": ["Usage: %timeit [-n<N>] [-r<R>] statement"]
812
+ }
813
+ return result
814
+
815
+ try:
816
+ # Create a timer
817
+ timer = timeit.Timer(
818
+ stmt=statement,
819
+ globals=self.globals_dict
820
+ )
821
+
822
+ # Auto-determine number of iterations if not specified
823
+ if number is None:
824
+ for i in range(1, 10):
825
+ n = 10 ** i
826
+ try:
827
+ t = timer.timeit(n)
828
+ if t >= 0.2:
829
+ number = n
830
+ break
831
+ except:
832
+ number = 1
833
+ break
834
+ if number is None:
835
+ number = 10 ** 7
836
+
837
+ # Run the timing
838
+ all_runs = timer.repeat(repeat=repeat, number=number)
839
+ best = min(all_runs) / number
840
+
841
+ # Format output
842
+ if best < 1e-6:
843
+ timing_str = f"{best * 1e9:.0f} ns"
844
+ elif best < 1e-3:
845
+ timing_str = f"{best * 1e6:.0f} µs"
846
+ elif best < 1:
847
+ timing_str = f"{best * 1e3:.0f} ms"
848
+ else:
849
+ timing_str = f"{best:.2f} s"
850
+
851
+ 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"
852
+
853
+ if not quiet:
854
+ result["outputs"].append({
855
+ "output_type": "stream",
856
+ "name": "stdout",
857
+ "text": output_text
858
+ })
859
+
860
+ if websocket:
861
+ await websocket.send_json({
862
+ "type": "stream_output",
863
+ "data": {"stream": "stdout", "text": output_text}
864
+ })
865
+
866
+ except Exception as e:
867
+ result["status"] = "error"
868
+ result["error"] = {
869
+ "ename": type(e).__name__,
870
+ "evalue": str(e),
871
+ "traceback": [f"TIMEIT magic error: {str(e)}"]
872
+ }
873
+
874
+ if websocket:
875
+ await websocket.send_json({
876
+ "type": "execution_error",
877
+ "data": {"error": result["error"]}
878
+ })
879
+
880
+ return result
881
+
882
+ async def handle_run(self, args: list, result: Dict[str, Any],
883
+ websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
884
+ """
885
+ Handle %run - execute Python script or notebook.
886
+ Usage: %run script.py [args]
887
+ """
888
+ try:
889
+ if not args:
890
+ result["status"] = "error"
891
+ result["error"] = {
892
+ "ename": "UsageError",
893
+ "evalue": "%run requires a filename",
894
+ "traceback": ["Usage: %run filename [args]"]
895
+ }
896
+ return result
897
+
898
+ filename = args[0]
899
+ script_args = args[1:] if len(args) > 1 else []
900
+
901
+ # Check file exists
902
+ if not os.path.exists(filename):
903
+ raise FileNotFoundError(f"File not found: {filename}")
904
+
905
+ # Read the file
906
+ with open(filename, 'r') as f:
907
+ code = f.read()
908
+
909
+ # Update sys.argv for the script
910
+ old_argv = sys.argv
911
+ sys.argv = [filename] + script_args
912
+
913
+ try:
914
+ # Compile and execute
915
+ compiled_code = compile(code, filename, 'exec')
916
+ exec(compiled_code, self.globals_dict)
917
+
918
+ output_text = f"Executed: {filename}\n"
919
+ result["outputs"].append({
920
+ "output_type": "stream",
921
+ "name": "stdout",
922
+ "text": output_text
923
+ })
924
+
925
+ if websocket:
926
+ await websocket.send_json({
927
+ "type": "stream_output",
928
+ "data": {"stream": "stdout", "text": output_text}
929
+ })
930
+
931
+ finally:
932
+ # Restore sys.argv
933
+ sys.argv = old_argv
934
+
935
+ except Exception as e:
936
+ result["status"] = "error"
937
+ result["error"] = {
938
+ "ename": type(e).__name__,
939
+ "evalue": str(e),
940
+ "traceback": [f"RUN magic error: {str(e)}"]
941
+ }
942
+
943
+ if websocket:
944
+ await websocket.send_json({
945
+ "type": "execution_error",
946
+ "data": {"error": result["error"]}
947
+ })
948
+
949
+ return result