@@ -7,8 +7,12 @@ import { loadPyodide } from "pyodide";
77import { version as pyodideVersion } from "pyodide/package.json" ;
88import type { PyCallable } from "pyodide/ffi" ;
99import type { WorkerAPI , WorkerCapabilities } from "./runtime" ;
10- import type { Diagnostic , ReplOutput , UpdatedFile } from "../interface" ;
11- import { parsePythonTraceback } from "../diagnostics/python" ;
10+ import type {
11+ Diagnostic ,
12+ DiagnosticFrame ,
13+ ReplOutput ,
14+ UpdatedFile ,
15+ } from "../interface" ;
1216
1317import execfile_py from "./pyodide/execfile.py?raw" ;
1418import check_syntax_py from "./pyodide/check_syntax.py?raw" ;
@@ -176,7 +180,7 @@ async function runFile(
176180 . trim ( ) ,
177181 } ) ;
178182 if ( onDiagnostic ) {
179- const diagnostics = parsePythonTraceback ( e . message , HOME ) ;
183+ const diagnostics = parsePythonTraceback ( e . message ) ;
180184 for ( const diag of diagnostics ) {
181185 await onDiagnostic ( diag ) ;
182186 }
@@ -227,6 +231,95 @@ async function restoreState(): Promise<object> {
227231 throw new Error ( "not implemented" ) ;
228232}
229233
234+ /**
235+ * Parses Python error/traceback string into a single Diagnostic with multiple frames.
236+ *
237+ * @param traceback - The traceback string or error message from Python
238+ * @returns Array of Diagnostic objects (at most 1 per error)
239+ */
240+ export function parsePythonTraceback ( traceback : string ) : Diagnostic [ ] {
241+ if ( ! traceback ) return [ ] ;
242+
243+ const lines = traceback . trim ( ) . split ( "\n" ) ;
244+ if ( lines . length === 0 ) return [ ] ;
245+
246+ // Extract the last error message line (e.g., "Exception: This is a test error" or "SyntaxError: ...")
247+ let errorMessage = "" ;
248+ for ( let i = lines . length - 1 ; i >= 0 ; i -- ) {
249+ const line = lines [ i ] . trim ( ) ;
250+ if (
251+ line &&
252+ ! line . startsWith ( "^" ) &&
253+ ! line . startsWith ( 'File "' ) &&
254+ ! line . startsWith ( "Traceback" )
255+ ) {
256+ errorMessage = line ;
257+ break ;
258+ }
259+ }
260+
261+ const frames : DiagnosticFrame [ ] = [ ] ;
262+ const fileLineRegex = / F i l e " ( [ ^ " ] + ) " , l i n e ( \d + ) (?: , i n ( .+ ) ) ? / ;
263+
264+ for ( let i = 0 ; i < lines . length ; i ++ ) {
265+ const match = fileLineRegex . exec ( lines [ i ] ) ;
266+ if ( match ) {
267+ let rawFilename = match [ 1 ] ;
268+ const lineNum = parseInt ( match [ 2 ] , 10 ) ;
269+
270+ // Normalize filename by removing homePrefix or leading slashes
271+ if ( rawFilename . startsWith ( HOME ) ) {
272+ rawFilename = rawFilename . slice ( HOME . length ) ;
273+ }
274+ if ( rawFilename . startsWith ( "/" ) ) {
275+ rawFilename = rawFilename . replace ( / ^ \/ + / , "" ) ;
276+ }
277+
278+ // Ignore internal names like <exec>, <string> if not matching normal files
279+ if ( rawFilename === "<exec>" || rawFilename === "<string>" ) {
280+ continue ;
281+ }
282+
283+ // Check if there is a column indicator on subsequent lines (e.g. for SyntaxError with ^)
284+ let startColumn : number | undefined = undefined ;
285+ let endColumn : number | undefined = undefined ;
286+ for ( let j = i + 1 ; j < Math . min ( i + 4 , lines . length ) ; j ++ ) {
287+ const nextLine = lines [ j ] ;
288+ if ( fileLineRegex . test ( nextLine ) ) break ;
289+ const caretIndex = nextLine . indexOf ( "^" ) ;
290+ if ( caretIndex !== - 1 ) {
291+ // In Python SyntaxError output, caret points to character (1-indexed)
292+ startColumn = caretIndex + 1 ;
293+ const caretEnd = nextLine . lastIndexOf ( "^" ) ;
294+ if ( caretEnd > caretIndex ) {
295+ endColumn = caretEnd + 2 ;
296+ }
297+ break ;
298+ }
299+ }
300+
301+ frames . push ( {
302+ filename : rawFilename ,
303+ startLineNumber : lineNum ,
304+ startColumn,
305+ endLineNumber : lineNum ,
306+ endColumn,
307+ } ) ;
308+ }
309+ }
310+
311+ if ( frames . length === 0 ) return [ ] ;
312+
313+ // Return a single Diagnostic with all frames (1 error = 1 Diagnostic)
314+ return [
315+ {
316+ frames,
317+ message : errorMessage ,
318+ severity : "error" ,
319+ } ,
320+ ] ;
321+ }
322+
230323const api : WorkerAPI = {
231324 init,
232325 runCode,
0 commit comments