more-compute 0.3.2__py3-none-any.whl → 0.4.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,190 @@
1
+ """Parser for py:percent format (.py files with # %% cell markers)."""
2
+
3
+ import re
4
+ from typing import List, Dict, Optional
5
+
6
+
7
+ def parse_py_percent(content: str) -> Dict:
8
+ """
9
+ Parse py:percent format Python file into notebook structure.
10
+
11
+ Format:
12
+ # %%
13
+ code cell content
14
+
15
+ # %% [markdown]
16
+ # markdown content
17
+
18
+ Args:
19
+ content: Raw .py file content
20
+
21
+ Returns:
22
+ Dict with 'cells' and 'metadata' (compatible with Notebook class)
23
+ """
24
+ cells = []
25
+
26
+ # Split by cell markers (# %%)
27
+ # Keep the marker in the split to determine cell type
28
+ parts = re.split(r'(# %%.*?\n)', content)
29
+
30
+ # First part before any cell marker (usually imports/metadata)
31
+ if parts[0].strip():
32
+ # Check if it's UV inline script metadata
33
+ first_part = parts[0].strip()
34
+ if not first_part.startswith('# /// script'):
35
+ # It's code, add as first cell
36
+ cells.append({
37
+ 'cell_type': 'code',
38
+ 'source': parts[0],
39
+ 'metadata': {},
40
+ 'execution_count': None,
41
+ 'outputs': []
42
+ })
43
+
44
+ # Process remaining parts
45
+ i = 1
46
+ while i < len(parts):
47
+ if i >= len(parts):
48
+ break
49
+
50
+ marker = parts[i] if i < len(parts) else ''
51
+ cell_content = parts[i + 1] if i + 1 < len(parts) else ''
52
+
53
+ # Determine cell type from marker
54
+ if '[markdown]' in marker.lower():
55
+ cell_type = 'markdown'
56
+ # Remove leading # from markdown lines
57
+ lines = cell_content.split('\n')
58
+ cleaned_lines = []
59
+ for line in lines:
60
+ if line.strip().startswith('#'):
61
+ # Remove first # and any space after it
62
+ cleaned_lines.append(line.strip()[1:].lstrip())
63
+ else:
64
+ cleaned_lines.append(line)
65
+ cell_content = '\n'.join(cleaned_lines)
66
+ else:
67
+ cell_type = 'code'
68
+
69
+ # Only add non-empty cells
70
+ if cell_content.strip():
71
+ cell = {
72
+ 'cell_type': cell_type,
73
+ 'source': cell_content.strip(),
74
+ 'metadata': {}
75
+ }
76
+
77
+ if cell_type == 'code':
78
+ cell['execution_count'] = None
79
+ cell['outputs'] = []
80
+
81
+ cells.append(cell)
82
+
83
+ i += 2
84
+
85
+ return {
86
+ 'cells': cells,
87
+ 'metadata': {
88
+ 'kernelspec': {
89
+ 'display_name': 'Python 3',
90
+ 'language': 'python',
91
+ 'name': 'python3'
92
+ },
93
+ 'language_info': {
94
+ 'name': 'python',
95
+ 'version': '3.8.0'
96
+ }
97
+ },
98
+ 'nbformat': 4,
99
+ 'nbformat_minor': 4
100
+ }
101
+
102
+
103
+ def extract_uv_dependencies(content: str) -> Optional[List[str]]:
104
+ """
105
+ Extract UV inline script dependencies from .py file.
106
+
107
+ Format:
108
+ # /// script
109
+ # dependencies = [
110
+ # "package1",
111
+ # "package2>=1.0.0",
112
+ # ]
113
+ # ///
114
+
115
+ Args:
116
+ content: Raw .py file content
117
+
118
+ Returns:
119
+ List of dependency strings, or None if no dependencies found
120
+ """
121
+ # Match UV inline script metadata block
122
+ pattern = r'# /// script\n(.*?)# ///\n'
123
+ match = re.search(pattern, content, re.DOTALL)
124
+
125
+ if not match:
126
+ return None
127
+
128
+ metadata_block = match.group(1)
129
+
130
+ # Extract dependencies list
131
+ deps_pattern = r'# dependencies = \[(.*?)\]'
132
+ deps_match = re.search(deps_pattern, metadata_block, re.DOTALL)
133
+
134
+ if not deps_match:
135
+ return None
136
+
137
+ deps_str = deps_match.group(1)
138
+
139
+ # Parse individual dependencies
140
+ dependencies = []
141
+ for line in deps_str.split('\n'):
142
+ line = line.strip()
143
+ if line.startswith('#'):
144
+ line = line[1:].strip()
145
+ # Remove quotes and trailing comma
146
+ line = line.strip('"\'').strip(',').strip()
147
+ if line:
148
+ dependencies.append(line)
149
+
150
+ return dependencies if dependencies else None
151
+
152
+
153
+ def generate_py_percent(cells: List[Dict]) -> str:
154
+ """
155
+ Generate py:percent format from cell list.
156
+
157
+ Args:
158
+ cells: List of cell dicts with 'cell_type' and 'source'
159
+
160
+ Returns:
161
+ String in py:percent format
162
+ """
163
+ lines = []
164
+
165
+ for i, cell in enumerate(cells):
166
+ cell_type = cell.get('cell_type', 'code')
167
+ source = cell.get('source', '')
168
+
169
+ # Ensure source is string
170
+ if isinstance(source, list):
171
+ source = ''.join(source)
172
+
173
+ # Add cell marker
174
+ if cell_type == 'markdown':
175
+ lines.append('# %% [markdown]')
176
+ # Add # prefix to each line of markdown
177
+ for line in source.split('\n'):
178
+ if line.strip():
179
+ lines.append(f'# {line}')
180
+ else:
181
+ lines.append('#')
182
+ else:
183
+ lines.append('# %%')
184
+ lines.append(source)
185
+
186
+ # Add blank line between cells
187
+ if i < len(cells) - 1:
188
+ lines.append('')
189
+
190
+ return '\n'.join(lines)
@@ -26,6 +26,10 @@ class AsyncSpecialCommandHandler:
26
26
  self.captured_outputs = {} # Store captured outputs from %%capture
27
27
  self.cell_magic_handlers = CellMagicHandlers(globals_dict, self)
28
28
  self.line_magic_handlers = LineMagicHandlers(globals_dict)
29
+ self.current_process: Optional[asyncio.subprocess.Process] = None # Track current async subprocess for interrupts
30
+ self.current_process_sync: Optional[subprocess.Popen] = None # Track current sync subprocess for interrupts
31
+ self.sync_interrupted: bool = False # Flag to indicate sync process was interrupted
32
+ self.stream_tasks: list = [] # Track streaming tasks for cancellation
29
33
 
30
34
  def is_special_command(self, source_code: Union[str, list, tuple]) -> bool:
31
35
  """Check if the source code is a special command or contains shell commands"""
@@ -52,6 +56,9 @@ class AsyncSpecialCommandHandler:
52
56
  websocket: Optional[WebSocket] = None,
53
57
  cell_index: Optional[int] = None) -> Dict[str, Any]:
54
58
  """Execute a special command and return the result"""
59
+ # Reset interrupt flag at start of execution
60
+ self.sync_interrupted = False
61
+
55
62
  text = self._coerce_source_to_text(source_code)
56
63
  stripped = text.strip()
57
64
 
@@ -103,15 +110,8 @@ class AsyncSpecialCommandHandler:
103
110
  env = prepare_shell_environment(command)
104
111
  cmd_parts = prepare_shell_command(command)
105
112
 
106
- # Send execution start notification
107
- if websocket:
108
- await websocket.send_json({
109
- "type": "execution_start",
110
- "data": {
111
- "command": f"!{command}",
112
- **({"cell_index": cell_index} if cell_index is not None else {})
113
- }
114
- })
113
+ # Note: execution_start is sent by the executor, not here
114
+ # Sending it twice confuses the frontend
115
115
 
116
116
  # Create subprocess with streaming
117
117
  process = await asyncio.create_subprocess_exec(
@@ -122,34 +122,78 @@ class AsyncSpecialCommandHandler:
122
122
  cwd=os.getcwd()
123
123
  )
124
124
 
125
- # Stream output concurrently
126
- stdout_task = asyncio.create_task(
127
- self._stream_output(process.stdout, "stdout", result, websocket, cell_index)
128
- )
129
- stderr_task = asyncio.create_task(
130
- self._stream_output(process.stderr, "stderr", result, websocket, cell_index)
131
- )
125
+ # Track process for interrupt handling
126
+ self.current_process = process
127
+ print(f"[SPECIAL_CMD] Started subprocess PID={process.pid}", file=sys.stderr, flush=True)
132
128
 
133
- # Wait for both streams to complete
134
- await asyncio.gather(stdout_task, stderr_task)
135
-
136
- # Wait for process completion
137
- return_code = await process.wait()
129
+ try:
130
+ # Stream output concurrently
131
+ stdout_task = asyncio.create_task(
132
+ self._stream_output(process.stdout, "stdout", result, websocket, cell_index)
133
+ )
134
+ stderr_task = asyncio.create_task(
135
+ self._stream_output(process.stderr, "stderr", result, websocket, cell_index)
136
+ )
137
+
138
+ # Track tasks for interruption
139
+ self.stream_tasks = [stdout_task, stderr_task]
140
+
141
+ print(f"[SPECIAL_CMD] Waiting for stream tasks to complete...", file=sys.stderr, flush=True)
142
+ # Wait for both streams to complete
143
+ await asyncio.gather(stdout_task, stderr_task, return_exceptions=True)
144
+
145
+ print(f"[SPECIAL_CMD] Streams complete, waiting for process to exit...", file=sys.stderr, flush=True)
146
+ # Wait for process completion
147
+ return_code = await process.wait()
148
+ print(f"[SPECIAL_CMD] Process exited with code {return_code}", file=sys.stderr, flush=True)
149
+ except asyncio.CancelledError:
150
+ # Task was cancelled - treat as interrupt
151
+ print(f"[SPECIAL_CMD] Task cancelled, treating as interrupt", file=sys.stderr, flush=True)
152
+ return_code = -15 # SIGTERM
153
+ except Exception as e:
154
+ print(f"[SPECIAL_CMD] Exception during execution: {e}", file=sys.stderr, flush=True)
155
+ import traceback
156
+ traceback.print_exc()
157
+ # Set error result
158
+ result["status"] = "error"
159
+ result["error"] = {
160
+ "output_type": "error",
161
+ "ename": type(e).__name__,
162
+ "evalue": str(e),
163
+ "traceback": traceback.format_exc().split('\n')
164
+ }
165
+ return_code = -1
166
+ finally:
167
+ # Clear process reference when done
168
+ self.current_process = None
169
+ self.stream_tasks = []
170
+ print(f"[SPECIAL_CMD] Cleared process reference", file=sys.stderr, flush=True)
171
+
172
+ # Check if process was interrupted (negative return code means killed by signal)
173
+ if return_code < 0:
174
+ print(f"[SPECIAL_CMD] Process was interrupted (return_code={return_code}), setting KeyboardInterrupt error", file=sys.stderr, flush=True)
175
+ result["status"] = "error"
176
+ result["error"] = {
177
+ "output_type": "error",
178
+ "ename": "KeyboardInterrupt",
179
+ "evalue": "Execution interrupted by user",
180
+ "traceback": [] # No traceback needed for user-initiated interrupt
181
+ }
182
+ elif return_code != 0:
183
+ # Non-zero but positive means command error (not interrupt)
184
+ result["status"] = "error"
185
+ result["error"] = {
186
+ "output_type": "error",
187
+ "ename": "ShellCommandError",
188
+ "evalue": f"Command failed with return code {return_code}",
189
+ "traceback": [f"Shell command '{command}' failed"]
190
+ }
138
191
 
139
- # Send completion notification
140
- if websocket:
141
- await websocket.send_json({
142
- "type": "execution_complete",
143
- "data": {
144
- "return_code": return_code,
145
- "status": "error" if return_code != 0 else "ok",
146
- **({"cell_index": cell_index} if cell_index is not None else {})
147
- }
148
- })
192
+ print(f"[SPECIAL_CMD] Returning result: status={result['status']}, return_code={return_code}", file=sys.stderr, flush=True)
149
193
 
150
194
  # If pip install/uninstall occurred, notify clients to refresh packages
151
195
  try:
152
- if websocket and (command.startswith('pip install') or command.startswith('pip uninstall') or 'pip install' in command or 'pip uninstall' in command):
196
+ if websocket and return_code == 0 and (command.startswith('pip install') or command.startswith('pip uninstall') or 'pip install' in command or 'pip uninstall' in command):
153
197
  # Small delay to ensure pip finishes writing metadata to disk
154
198
  await asyncio.sleep(0.5)
155
199
  await websocket.send_json({
@@ -159,21 +203,6 @@ class AsyncSpecialCommandHandler:
159
203
  except Exception:
160
204
  pass
161
205
 
162
- # Check if command failed
163
- if return_code != 0:
164
- result["status"] = "error"
165
- # Only add generic error if we don't already have detailed stderr output
166
- # The detailed stderr is already in result["outputs"] from streaming
167
- has_stderr = any(o.get("name") == "stderr" and o.get("text", "").strip()
168
- for o in result.get("outputs", []))
169
- if not has_stderr:
170
- # No detailed error output, add generic error
171
- result["error"] = {
172
- "ename": "ShellCommandError",
173
- "evalue": f"Command failed with return code {return_code}",
174
- "traceback": [f"Shell command failed: {command}"]
175
- }
176
-
177
206
  except Exception as e:
178
207
  result["status"] = "error"
179
208
  result["error"] = {
@@ -195,8 +224,56 @@ class AsyncSpecialCommandHandler:
195
224
  return result
196
225
 
197
226
  async def interrupt(self):
198
- # Placeholder for future process-based interruption logic
199
- return
227
+ """Interrupt the currently running subprocess"""
228
+ # Cancel stream tasks first
229
+ for task in self.stream_tasks:
230
+ if not task.done():
231
+ print(f"[SPECIAL_CMD] Cancelling stream task", file=sys.stderr, flush=True)
232
+ task.cancel()
233
+
234
+ # Interrupt async subprocess
235
+ if self.current_process:
236
+ try:
237
+ print(f"[SPECIAL_CMD] Interrupting async subprocess PID={self.current_process.pid}", file=sys.stderr, flush=True)
238
+ self.current_process.terminate()
239
+
240
+ # Give it a moment to terminate gracefully
241
+ try:
242
+ await asyncio.wait_for(self.current_process.wait(), timeout=1.0)
243
+ print(f"[SPECIAL_CMD] Async subprocess terminated gracefully", file=sys.stderr, flush=True)
244
+ except asyncio.TimeoutError:
245
+ # Force kill if it doesn't terminate
246
+ print(f"[SPECIAL_CMD] Async subprocess didn't terminate, force killing", file=sys.stderr, flush=True)
247
+ self.current_process.kill()
248
+ await self.current_process.wait()
249
+ print(f"[SPECIAL_CMD] Async subprocess killed", file=sys.stderr, flush=True)
250
+
251
+ except Exception as e:
252
+ print(f"[SPECIAL_CMD] Error interrupting async subprocess: {e}", file=sys.stderr, flush=True)
253
+
254
+ # Interrupt sync subprocess
255
+ if self.current_process_sync:
256
+ try:
257
+ print(f"[SPECIAL_CMD] Interrupting sync subprocess PID={self.current_process_sync.pid}", file=sys.stderr, flush=True)
258
+ self.sync_interrupted = True # Set flag so shell commands know to stop
259
+ self.current_process_sync.terminate()
260
+
261
+ # Give it a moment to terminate gracefully
262
+ try:
263
+ self.current_process_sync.wait(timeout=1.0)
264
+ print(f"[SPECIAL_CMD] Sync subprocess terminated gracefully", file=sys.stderr, flush=True)
265
+ except subprocess.TimeoutExpired:
266
+ # Force kill if it doesn't terminate
267
+ print(f"[SPECIAL_CMD] Sync subprocess didn't terminate, force killing", file=sys.stderr, flush=True)
268
+ self.current_process_sync.kill()
269
+ self.current_process_sync.wait()
270
+ print(f"[SPECIAL_CMD] Sync subprocess killed", file=sys.stderr, flush=True)
271
+
272
+ except Exception as e:
273
+ print(f"[SPECIAL_CMD] Error interrupting sync subprocess: {e}", file=sys.stderr, flush=True)
274
+
275
+ if not self.current_process and not self.current_process_sync:
276
+ print(f"[SPECIAL_CMD] No subprocess to interrupt", file=sys.stderr, flush=True)
200
277
 
201
278
  async def _stream_output(self, stream, stream_type: str, result: Dict[str, Any],
202
279
  websocket: Optional[WebSocket] = None,
frontend/.DS_Store DELETED
Binary file