diff --git a/index.ts b/index.ts index 9569354..9204936 100644 --- a/index.ts +++ b/index.ts @@ -77,6 +77,18 @@ export class PythonShellErrorWithLogs extends PythonShellError { logs: any[]; } +export class PythonShellParseError extends PythonShellError { + data: string; + originalError: Error; + + constructor(data: string, originalError: Error) { + super('Error parsing PythonShell output: ' + originalError.message); + this.name = 'PythonShellParseError'; + this.data = data; + this.originalError = originalError; + } +} + /** * Takes in a string stream and emits batches seperated by newlines */ @@ -169,6 +181,7 @@ export class PythonShell extends EventEmitter { let self = this; let errorData = ''; + let parseError: PythonShellParseError; EventEmitter.call(this); options = extend({}, PythonShell.defaultOptions, options); @@ -205,7 +218,14 @@ export class PythonShell extends EventEmitter { // note that setting the encoding turns the chunk into a string stdoutSplitter.setEncoding(options.encoding || 'utf8'); this.stdout.pipe(stdoutSplitter).on('data', (chunk: string) => { - this.emit('message', self.parser(chunk)); + let parsedChunk; + try { + parsedChunk = self.parser(chunk); + } catch (err) { + emitParseError(err, chunk); + return; + } + this.emit('message', parsedChunk); }); } @@ -249,6 +269,15 @@ export class PythonShell extends EventEmitter { terminateIfNeeded(); }); + function emitParseError(err: unknown, data: string) { + let originalError = err instanceof Error ? err : new Error('' + err); + let outputError = new PythonShellParseError(data, originalError); + if (!parseError) { + parseError = outputError; + } + self.emit('parseError', outputError); + } + function terminateIfNeeded() { if ( !self.stderrHasEnded || @@ -277,6 +306,8 @@ export class PythonShell extends EventEmitter { if (self.listeners('pythonError').length || !self._endCallback) { self.emit('pythonError', err); } + } else if (parseError) { + err = parseError; } self.terminated = true; @@ -532,4 +563,26 @@ export interface PythonShell { event: 'pythonError', listener: (error: PythonShellError) => void, ): this; + + addListener( + event: 'parseError', + listener: (error: PythonShellParseError) => void, + ): this; + emit(event: 'parseError', error: PythonShellParseError): boolean; + on( + event: 'parseError', + listener: (error: PythonShellParseError) => void, + ): this; + once( + event: 'parseError', + listener: (error: PythonShellParseError) => void, + ): this; + prependListener( + event: 'parseError', + listener: (error: PythonShellParseError) => void, + ): this; + prependOnceListener( + event: 'parseError', + listener: (error: PythonShellParseError) => void, + ): this; } diff --git a/test/python/invalid_json.py b/test/python/invalid_json.py new file mode 100644 index 0000000..78274bd --- /dev/null +++ b/test/python/invalid_json.py @@ -0,0 +1 @@ +print("not json") diff --git a/test/test-python-shell.ts b/test/test-python-shell.ts index 3d5de81..1960c7b 100644 --- a/test/test-python-shell.ts +++ b/test/test-python-shell.ts @@ -415,6 +415,30 @@ describe('PythonShell', function () { }) .end(done); }); + it('should emit parseError when JSON output cannot be parsed', function (done) { + let pyshell = new PythonShell('invalid_json.py', { + mode: 'json', + }); + pyshell + .on('parseError', function (err) { + err.name.should.be.exactly('PythonShellParseError'); + err.data.should.be.exactly('not json'); + err.originalError.should.be.instanceOf(SyntaxError); + }) + .end(function (err) { + err.name.should.be.exactly('PythonShellParseError'); + done(); + }); + }); + it('should reject run() when JSON output cannot be parsed', async function () { + try { + await PythonShell.run('invalid_json.py', { mode: 'json' }); + throw new Error('Expected run() to reject invalid JSON output'); + } catch (err) { + err.name.should.be.exactly('PythonShellParseError'); + err.data.should.be.exactly('not json'); + } + }); it('should properly buffer partial messages', function (done) { // echo_text_with_newline_control echoes text with $'s replaced with newlines let pyshell = new PythonShell('echo_text_with_newline_control.py', {