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.
@@ -0,0 +1,68 @@
1
+ """
2
+ Shared utilities for shell command execution.
3
+ Used by both special_commands.py and cell_magics.py to avoid duplication.
4
+ """
5
+ import os
6
+ import shlex
7
+ import platform
8
+ from typing import List, Dict
9
+
10
+
11
+ def prepare_shell_command(cmd: str) -> List[str]:
12
+ """
13
+ Convert shell command to subprocess-compatible format.
14
+ Handles pip routing, python unbuffering, and platform-specific shells.
15
+
16
+ Args:
17
+ cmd: Shell command string (e.g., "pip install pandas")
18
+
19
+ Returns:
20
+ List of command arguments for subprocess
21
+ """
22
+ if cmd.startswith('pip '):
23
+ # Route pip through Python module for better control
24
+ parts = ['python', '-m'] + shlex.split(cmd)
25
+ # Add progress bar control for pip
26
+ if 'install' in cmd and '--progress-bar' not in cmd:
27
+ parts.extend(['--progress-bar', 'off'])
28
+ return parts
29
+
30
+ elif cmd.startswith('python '):
31
+ # Add unbuffered flag to python commands
32
+ parts = shlex.split(cmd)
33
+ parts.insert(1, '-u') # Add -u after 'python'
34
+ return parts
35
+
36
+ else:
37
+ # For other shell commands, use platform-appropriate shell
38
+ system = platform.system()
39
+ if system == 'Windows':
40
+ return ['cmd', '/c', cmd]
41
+ elif system == 'Darwin':
42
+ return ['/bin/bash', '-c', cmd]
43
+ else:
44
+ return ['/bin/bash', '-c', cmd]
45
+
46
+
47
+ def prepare_shell_environment(cmd: str) -> Dict[str, str]:
48
+ """
49
+ Prepare environment variables for shell command execution.
50
+
51
+ Args:
52
+ cmd: Shell command string
53
+
54
+ Returns:
55
+ Environment dictionary
56
+ """
57
+ env = os.environ.copy()
58
+
59
+ # Always set unbuffered Python
60
+ env['PYTHONUNBUFFERED'] = '1'
61
+ env['PYTHONDONTWRITEBYTECODE'] = '1'
62
+
63
+ # Additional settings for pip commands
64
+ if 'pip install' in cmd:
65
+ env['PIP_DISABLE_PIP_VERSION_CHECK'] = '1'
66
+ env['PIP_NO_CACHE_DIR'] = '1'
67
+
68
+ return env
@@ -5,10 +5,15 @@ import asyncio
5
5
  import subprocess
6
6
  import time
7
7
  import shlex
8
+ import platform
8
9
  from contextlib import redirect_stdout, redirect_stderr
9
10
  from typing import Dict, Any, Optional, Tuple, Union
10
11
  from fastapi import WebSocket
11
12
 
13
+ from .cell_magics import CellMagicHandlers
14
+ from .line_magics import LineMagicHandlers
15
+ from .shell_utils import prepare_shell_command, prepare_shell_environment
16
+
12
17
 
13
18
  # this file is not tested that all functions work, need to write a test file / manually check
14
19
  # to-do
@@ -19,14 +24,28 @@ class AsyncSpecialCommandHandler:
19
24
  def __init__(self, globals_dict: dict):
20
25
  self.globals_dict = globals_dict
21
26
  self.captured_outputs = {} # Store captured outputs from %%capture
27
+ self.cell_magic_handlers = CellMagicHandlers(globals_dict, self)
28
+ self.line_magic_handlers = LineMagicHandlers(globals_dict)
22
29
 
23
30
  def is_special_command(self, source_code: Union[str, list, tuple]) -> bool:
24
- """Check if the source code is a special command"""
31
+ """Check if the source code is a special command or contains shell commands"""
25
32
  text = self._coerce_source_to_text(source_code)
26
33
  stripped = text.strip()
27
- return (stripped.startswith('!') or
28
- stripped.startswith('%%') or
29
- stripped.startswith('%'))
34
+
35
+ # Check if starts with magic or shell command
36
+ if (stripped.startswith('!') or
37
+ stripped.startswith('%%') or
38
+ stripped.startswith('%')):
39
+ return True
40
+
41
+ # Check if ANY line contains a shell command (like Jupyter/Colab)
42
+ # This allows mixing Python code with !commands
43
+ lines = text.split('\n')
44
+ for line in lines:
45
+ if line.strip().startswith('!'):
46
+ return True
47
+
48
+ return False
30
49
 
31
50
  async def execute_special_command(self, source_code: Union[str, list, tuple], result: Dict[str, Any],
32
51
  start_time: float, execution_count: int,
@@ -42,8 +61,27 @@ class AsyncSpecialCommandHandler:
42
61
  return await self._execute_cell_magic(text, result, start_time, execution_count, websocket)
43
62
  elif stripped.startswith('%'):
44
63
  return await self._execute_line_magic(stripped[1:], result, start_time, websocket)
45
- else:
46
- raise ValueError("Not a special command")
64
+
65
+ # Cell contains shell commands mixed with Python code
66
+ # Treat it like regular code execution but preprocess shell commands
67
+ # This reuses the preprocessing logic from cell_magics.py
68
+ try:
69
+ stdout_text, stderr_text = await self.cell_magic_handlers.execute_cell_content(
70
+ text, result, execution_count, websocket,
71
+ capture_stdout=False, capture_stderr=False
72
+ )
73
+ result["status"] = "ok"
74
+ except Exception as e:
75
+ result["status"] = "error"
76
+ result["error"] = {
77
+ "ename": type(e).__name__,
78
+ "evalue": str(e),
79
+ "traceback": [f"Error executing cell: {str(e)}"]
80
+ }
81
+
82
+ # Calculate execution time
83
+ result["execution_time"] = f"{(time.time() - start_time) * 1000:.1f}ms"
84
+ return result
47
85
 
48
86
  def _coerce_source_to_text(self, source_code: Union[str, list, tuple]) -> str:
49
87
  """Normalize incoming source to a single text string"""
@@ -61,9 +99,9 @@ class AsyncSpecialCommandHandler:
61
99
  cell_index: Optional[int] = None) -> Dict[str, Any]:
62
100
  """Execute a shell command with real-time streaming output"""
63
101
  try:
64
- # Prepare environment and command for streaming
65
- env = self._prepare_streaming_environment(command)
66
- cmd_parts = self._prepare_command_parts(command)
102
+ # Prepare environment and command for streaming (using shared utilities)
103
+ env = prepare_shell_environment(command)
104
+ cmd_parts = prepare_shell_command(command)
67
105
 
68
106
  # Send execution start notification
69
107
  if websocket:
@@ -112,6 +150,8 @@ class AsyncSpecialCommandHandler:
112
150
  # If pip install/uninstall occurred, notify clients to refresh packages
113
151
  try:
114
152
  if websocket and (command.startswith('pip install') or command.startswith('pip uninstall') or 'pip install' in command or 'pip uninstall' in command):
153
+ # Small delay to ensure pip finishes writing metadata to disk
154
+ await asyncio.sleep(0.5)
115
155
  await websocket.send_json({
116
156
  "type": "packages_updated",
117
157
  "data": {"action": "pip"}
@@ -158,57 +198,21 @@ class AsyncSpecialCommandHandler:
158
198
  # Placeholder for future process-based interruption logic
159
199
  return
160
200
 
161
- def _prepare_streaming_environment(self, shell_cmd: str) -> dict:
162
- """Prepare environment variables for unbuffered output"""
163
- env = os.environ.copy()
164
-
165
- # Always set unbuffered Python
166
- env['PYTHONUNBUFFERED'] = '1'
167
- env['PYTHONDONTWRITEBYTECODE'] = '1'
168
-
169
- # Additional settings for specific commands
170
- if 'pip install' in shell_cmd:
171
- env['PIP_DISABLE_PIP_VERSION_CHECK'] = '1'
172
- env['PIP_NO_CACHE_DIR'] = '1'
173
-
174
- return env
175
-
176
- def _prepare_command_parts(self, shell_cmd: str) -> list:
177
- """Convert shell command to subprocess-compatible format"""
178
-
179
- if shell_cmd.startswith('pip '):
180
- # Route pip through Python module for better control
181
- parts = ['python', '-m'] + shlex.split(shell_cmd)
182
- # Add progress bar control for pip
183
- if 'install' in shell_cmd and '--progress-bar' not in shell_cmd:
184
- parts.extend(['--progress-bar', 'off'])
185
- return parts
186
-
187
- elif shell_cmd.startswith('python '):
188
- # Add unbuffered flag to python commands
189
- parts = shlex.split(shell_cmd)
190
- parts.insert(1, '-u') # Add -u after 'python'
191
- return parts
192
-
193
- else:
194
- # For other shell commands, use shell execution
195
- return ['/bin/zsh', '-c', shell_cmd] # macOS with zsh
196
-
197
201
  async def _stream_output(self, stream, stream_type: str, result: Dict[str, Any],
198
202
  websocket: Optional[WebSocket] = None,
199
203
  cell_index: Optional[int] = None):
200
204
  """Read from a stream and send to websocket, while capturing the output."""
201
-
205
+
202
206
  output_text = ""
203
207
  while True:
204
208
  try:
205
209
  line = await stream.readline()
206
210
  if not line:
207
211
  break
208
-
212
+
209
213
  decoded_line = line.decode('utf-8')
210
214
  output_text += decoded_line
211
-
215
+
212
216
  if websocket:
213
217
  await websocket.send_json({
214
218
  "type": "stream_output",
@@ -234,7 +238,7 @@ class AsyncSpecialCommandHandler:
234
238
  }
235
239
  })
236
240
  break
237
-
241
+
238
242
  # Add the captured text to the final result object
239
243
  if output_text:
240
244
  # Look for an existing stream output of the same type to append to
@@ -261,13 +265,37 @@ class AsyncSpecialCommandHandler:
261
265
  magic_name = magic_parts[0][2:] # Remove %%
262
266
  magic_args = magic_parts[1:] if len(magic_parts) > 1 else []
263
267
 
268
+ # Map magic names to handler methods
269
+ magic_handlers = {
270
+ "capture": lambda: self.cell_magic_handlers.handle_capture(
271
+ magic_args, cell_content, result, start_time, execution_count, websocket
272
+ ),
273
+ "time": lambda: self.cell_magic_handlers.handle_time(
274
+ cell_content, result, start_time, execution_count, websocket
275
+ ),
276
+ "timeit": lambda: self.cell_magic_handlers.handle_timeit(
277
+ magic_args, cell_content, result, start_time, execution_count, websocket
278
+ ),
279
+ "writefile": lambda: self.cell_magic_handlers.handle_writefile(
280
+ magic_args, cell_content, result, start_time, websocket
281
+ ),
282
+ "bash": lambda: self.cell_magic_handlers.handle_bash(
283
+ cell_content, result, start_time, websocket
284
+ ),
285
+ "sh": lambda: self.cell_magic_handlers.handle_bash(
286
+ cell_content, result, start_time, websocket
287
+ ),
288
+ "html": lambda: self.cell_magic_handlers.handle_html(
289
+ cell_content, result, start_time, websocket
290
+ ),
291
+ "markdown": lambda: self.cell_magic_handlers.handle_markdown(
292
+ cell_content, result, start_time, websocket
293
+ ),
294
+ }
295
+
264
296
  try:
265
- if magic_name == "capture":
266
- return await self._handle_capture_magic(magic_args, cell_content, result, start_time, execution_count, websocket)
267
- elif magic_name == "time":
268
- return await self._handle_time_magic(cell_content, result, start_time, execution_count, websocket)
269
- elif magic_name == "writefile":
270
- return await self._handle_writefile_magic(magic_args, cell_content, result, start_time, websocket)
297
+ if magic_name in magic_handlers:
298
+ return await magic_handlers[magic_name]()
271
299
  else:
272
300
  result["status"] = "error"
273
301
  result["error"] = {
@@ -310,19 +338,30 @@ class AsyncSpecialCommandHandler:
310
338
  magic_name = parts[0]
311
339
  magic_args = parts[1:] if len(parts) > 1 else []
312
340
 
341
+ # Map magic names to handler methods
342
+ magic_handlers = {
343
+ "pwd": lambda: self.line_magic_handlers.handle_pwd(magic_args, result, websocket),
344
+ "cd": lambda: self.line_magic_handlers.handle_cd(magic_args, result, websocket),
345
+ "ls": lambda: self.line_magic_handlers.handle_ls(magic_args, result, websocket),
346
+ "env": lambda: self.line_magic_handlers.handle_env(magic_args, result, websocket),
347
+ "who": lambda: self.line_magic_handlers.handle_who(magic_args, result, websocket),
348
+ "whos": lambda: self.line_magic_handlers.handle_whos(magic_args, result, websocket),
349
+ "time": lambda: self.line_magic_handlers.handle_time(magic_args, result, websocket),
350
+ "timeit": lambda: self.line_magic_handlers.handle_timeit(magic_args, result, websocket),
351
+ "pip": lambda: self.line_magic_handlers.handle_pip(magic_args, result, self, websocket),
352
+ "load": lambda: self.line_magic_handlers.handle_load(magic_args, result, websocket),
353
+ "reset": lambda: self.line_magic_handlers.handle_reset(magic_args, result, websocket),
354
+ "lsmagic": lambda: self.line_magic_handlers.handle_lsmagic(magic_args, result, websocket),
355
+ "matplotlib": lambda: self.line_magic_handlers.handle_matplotlib(magic_args, result, websocket),
356
+ "load_ext": lambda: self.line_magic_handlers.handle_load_ext(magic_args, result, websocket),
357
+ "reload_ext": lambda: self.line_magic_handlers.handle_reload_ext(magic_args, result, websocket),
358
+ "unload_ext": lambda: self.line_magic_handlers.handle_unload_ext(magic_args, result, websocket),
359
+ "run": lambda: self.line_magic_handlers.handle_run(magic_args, result, websocket),
360
+ }
361
+
313
362
  try:
314
- if magic_name == "pwd":
315
- return await self._handle_pwd_magic(result, start_time, websocket)
316
- elif magic_name == "cd":
317
- return await self._handle_cd_magic(magic_args, result, start_time, websocket)
318
- elif magic_name == "ls":
319
- return await self._handle_ls_magic(magic_args, result, start_time, websocket)
320
- elif magic_name == "env":
321
- return await self._handle_env_magic(magic_args, result, start_time, websocket)
322
- elif magic_name == "who":
323
- return await self._handle_who_magic(result, start_time, websocket)
324
- elif magic_name == "whos":
325
- return await self._handle_whos_magic(result, start_time, websocket)
363
+ if magic_name in magic_handlers:
364
+ return await magic_handlers[magic_name]()
326
365
  else:
327
366
  result["status"] = "error"
328
367
  result["error"] = {
@@ -356,109 +395,3 @@ class AsyncSpecialCommandHandler:
356
395
 
357
396
  result["execution_time"] = f"{(time.time() - start_time) * 1000:.1f}ms"
358
397
  return result
359
-
360
- # Cell Magic Implementations
361
-
362
- async def _handle_capture_magic(self, args: list, cell_content: str, result: Dict[str, Any],
363
- start_time: float, execution_count: int,
364
- websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
365
- """Handle %%capture magic - capture stdout/stderr without displaying"""
366
- output_var = args[0] if args else None
367
- no_stdout = "--no-stdout" in args
368
- no_stderr = "--no-stderr" in args
369
-
370
- # Capture outputs
371
- stdout_capture = None if no_stdout else io.StringIO()
372
- stderr_capture = None if no_stderr else io.StringIO()
373
-
374
- try:
375
- # Execute the cell content with output capture
376
- if stdout_capture or stderr_capture:
377
- with redirect_stdout(stdout_capture or sys.stdout), \
378
- redirect_stderr(stderr_capture or sys.stderr):
379
- compiled_code = compile(cell_content, '<cell>', 'exec')
380
- exec(compiled_code, self.globals_dict)
381
- else:
382
- compiled_code = compile(cell_content, '<cell>', 'exec')
383
- exec(compiled_code, self.globals_dict)
384
-
385
- # Store captured output in a variable if specified
386
- if output_var:
387
- captured_data = {
388
- 'stdout': stdout_capture.getvalue() if stdout_capture else '',
389
- 'stderr': stderr_capture.getvalue() if stderr_capture else ''
390
- }
391
- self.globals_dict[output_var] = captured_data
392
- self.captured_outputs[output_var] = captured_data
393
-
394
- # Don't add outputs to result (they're captured, not displayed)
395
- if websocket:
396
- await websocket.send_json({
397
- "type": "execution_complete",
398
- "data": {
399
- "status": "ok",
400
- "message": "Output captured" + (f" in variable '{output_var}'" if output_var else "")
401
- }
402
- })
403
-
404
- except Exception as e:
405
- result["status"] = "error"
406
- result["error"] = {
407
- "ename": type(e).__name__,
408
- "evalue": str(e),
409
- "traceback": [f"Capture magic error: {str(e)}"]
410
- }
411
-
412
- if websocket:
413
- await websocket.send_json({
414
- "type": "execution_error",
415
- "data": {
416
- "error": result["error"]
417
- }
418
- })
419
-
420
- return result
421
-
422
- # Add other magic method implementations here...
423
- # (Time magic, writefile magic, line magics like pwd, cd, ls, etc.)
424
- # I'll implement a few key ones to keep this focused:
425
-
426
- async def _handle_pwd_magic(self, result: Dict[str, Any], start_time: float,
427
- websocket: Optional[WebSocket] = None) -> Dict[str, Any]:
428
- """Handle %pwd magic - print working directory"""
429
- try:
430
- pwd = os.getcwd()
431
- output_data = {
432
- "output_type": "execute_result",
433
- "execution_count": None,
434
- "data": {
435
- "text/plain": f"'{pwd}'"
436
- }
437
- }
438
- result["outputs"].append(output_data)
439
-
440
- if websocket:
441
- await websocket.send_json({
442
- "type": "execute_result",
443
- "data": {
444
- "data": output_data["data"]
445
- }
446
- })
447
-
448
- except Exception as e:
449
- result["status"] = "error"
450
- result["error"] = {
451
- "ename": type(e).__name__,
452
- "evalue": str(e),
453
- "traceback": [f"PWD magic error: {str(e)}"]
454
- }
455
-
456
- if websocket:
457
- await websocket.send_json({
458
- "type": "execution_error",
459
- "data": {
460
- "error": result["error"]
461
- }
462
- })
463
-
464
- return result