Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ Fires when the process has been terminated, with an error or not.

#### event: `pythonError`

Fires when the process terminates with a non-zero exit code.
Fires when the process terminates with a non-zero exit code or an output parser throws before normal completion. Parser exceptions are also passed to `.end()` and reject `PythonShell.run()`.

#### event: `error`

Expand Down
27 changes: 25 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export class PythonShell extends EventEmitter {

let self = this;
let errorData = '';
let parserError: Error;
EventEmitter.call(this);

options = <Options>extend({}, PythonShell.defaultOptions, options);
Expand Down Expand Up @@ -205,7 +206,15 @@ 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));
if (parserError) return;
let parsedChunk: any;
try {
parsedChunk = self.parser(chunk);
} catch (err) {
parserError = err instanceof Error ? err : new Error(String(err));
return;
}
this.emit('message', parsedChunk);
});
}

Expand All @@ -215,7 +224,15 @@ export class PythonShell extends EventEmitter {
// note that setting the encoding turns the chunk into a string
stderrSplitter.setEncoding(options.encoding || 'utf8');
this.stderr.pipe(stderrSplitter).on('data', (chunk: string) => {
this.emit('stderr', self.stderrParser(chunk));
if (parserError) return;
let parsedChunk: any;
try {
parsedChunk = self.stderrParser(chunk);
} catch (err) {
parserError = err instanceof Error ? err : new Error(String(err));
return;
}
this.emit('stderr', parsedChunk);
});
}

Expand Down Expand Up @@ -266,6 +283,12 @@ export class PythonShell extends EventEmitter {
'process exited with code ' + self.exitCode,
);
}
} else if (parserError) {
err = new PythonShellError(parserError.message);
err.stack = parserError.stack;
}

if (err) {
err = <PythonShellError>extend(err, {
executable: pythonPath,
options: pythonOptions.length ? pythonOptions : null,
Expand Down
87 changes: 86 additions & 1 deletion test/test-python-shell.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as should from 'should';
import { PythonShell } from '..';
import { NewlineTransformer, PythonShell } from '..';
import { sep, join } from 'path';
import { EOL as newline } from 'os';
import { chdir, cwd } from 'process';
Expand Down Expand Up @@ -415,6 +415,51 @@ describe('PythonShell', function () {
})
.end(done);
});
it('should report JSON parser errors through the end callback', function (done) {
let pyshell = new PythonShell('echo_text.py', {
mode: 'json',
formatter: 'text',
});
pyshell.send('not-json').end(function (err) {
should.exist(err);
err.should.be.an.Error;
err.message.should.match(/JSON|Unexpected token/);
done();
});
});
it('should not treat message listener exceptions as parser errors', function (done) {
let splitter = new NewlineTransformer();
let pyshell = new PythonShell('exit-code.py', { mode: 'text' }, splitter);
let listenerError = new Error('message listener failed');
let thrownError: Error;

pyshell.on('message', function () {
throw listenerError;
});

try {
splitter.emit('data', 'hello');
} catch (err) {
thrownError = err;
}

should.exist(thrownError);
thrownError.should.be.exactly(listenerError);
pyshell.end(function (err) {
should.not.exist(err);
done();
});
});
it('should prefer process errors over parser errors on non-zero exit', function (done) {
PythonShell.run('echo_hi_then_error.py', { mode: 'json' }).then(
() => done('expected the process to reject'),
(err) => {
err.message.should.be.exactly('Exception: fibble-fah');
err.stack.should.containEql('----- Python Traceback -----');
done();
},
);
});
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', {
Expand Down Expand Up @@ -494,6 +539,46 @@ describe('PythonShell', function () {
.send('world')
.end(done);
});
it('should report stderr parser errors through the end callback', function (done) {
let pyshell = new PythonShell('stderrLogging.py', {
stderrParser: function () {
throw new Error('stderr parser failed');
},
});
pyshell.end(function (err) {
should.exist(err);
err.message.should.be.exactly('stderr parser failed');
done();
});
});
it('should not treat stderr listener exceptions as parser errors', function (done) {
let splitter = new NewlineTransformer();
let pyshell = new PythonShell(
'exit-code.py',
{ mode: 'text' },
null,
splitter,
);
let listenerError = new Error('stderr listener failed');
let thrownError: Error;

pyshell.on('stderr', function () {
throw listenerError;
});

try {
splitter.emit('data', 'hello');
} catch (err) {
thrownError = err;
}

should.exist(thrownError);
thrownError.should.be.exactly(listenerError);
pyshell.end(function (err) {
should.not.exist(err);
done();
});
});
it('should not be invoked when mode is "binary"', function (done) {
let pyshell = new PythonShell('stderrLogging.py', {
stderrParser: 'binary',
Expand Down