Skip to content

Commit fd5d400

Browse files
committed
#130 added recieveStderr function
1 parent a8c0da7 commit fd5d400

File tree

4 files changed

+115
-30
lines changed

4 files changed

+115
-30
lines changed

README.md

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ npm test
2525

2626
### Running a Python script:
2727

28-
```js
29-
var PythonShell = require('python-shell');
28+
```typescript
29+
import {PythonShell} from 'python-shell';
3030

3131
PythonShell.run('my_script.py', function (err) {
3232
if (err) throw err;
@@ -38,10 +38,10 @@ If the script writes to stderr or exits with a non-zero code, an error will be t
3838

3939
### Running a Python script with arguments and options:
4040

41-
```js
42-
var PythonShell = require('python-shell');
41+
```typescript
42+
import {PythonShell} from 'python-shell';
4343

44-
var options = {
44+
let options = {
4545
mode: 'text',
4646
pythonPath: 'path/to/python',
4747
pythonOptions: ['-u'], // get print results in real-time
@@ -58,9 +58,9 @@ PythonShell.run('my_script.py', options, function (err, results) {
5858

5959
### Exchanging data between Node and Python:
6060

61-
```js
62-
var PythonShell = require('python-shell');
63-
var pyshell = new PythonShell('my_script.py');
61+
```typescript
62+
import {PythonShell} from 'python-shell';
63+
let pyshell = new PythonShell('my_script.py');
6464

6565
// sends a message to the Python script via stdin
6666
pyshell.send('hello');
@@ -92,9 +92,10 @@ For more details and examples including Python source code, take a look at the t
9292

9393
### Error Handling and extended stack traces
9494

95-
An error will be thrown if the process exits with a non-zero exit code or if data has been written to stderr. Additionally, if "stderr" contains a formatted Python traceback, the error is augmented with Python exception details including a concatenated stack trace.
95+
An error will be thrown if the process exits with a non-zero exit code. Additionally, if "stderr" contains a formatted Python traceback, the error is augmented with Python exception details including a concatenated stack trace.
9696

9797
Sample error with traceback (from test/python/error.py):
98+
9899
```
99100
Traceback (most recent call last):
100101
File "test/python/error.py", line 6, in <module>
@@ -103,8 +104,10 @@ Traceback (most recent call last):
103104
print 1/0
104105
ZeroDivisionError: integer division or modulo by zero
105106
```
107+
106108
would result into the following error:
107-
```js
109+
110+
```typescript
108111
{ [Error: ZeroDivisionError: integer division or modulo by zero]
109112
traceback: 'Traceback (most recent call last):\n File "test/python/error.py", line 6, in <module>\n divide_by_zero()\n File "test/python/error.py", line 4, in divide_by_zero\n print 1/0\nZeroDivisionError: integer division or modulo by zero\n',
110113
executable: 'python',
@@ -113,7 +116,9 @@ would result into the following error:
113116
args: null,
114117
exitCode: 1 }
115118
```
119+
116120
and `err.stack` would look like this:
121+
117122
```
118123
Error: ZeroDivisionError: integer division or modulo by zero
119124
at PythonShell.parseError (python-shell/index.js:131:17)
@@ -141,6 +146,7 @@ Creates an instance of `PythonShell` and starts the Python process
141146
* `binary`: data is streamed as-is through `stdout` and `stdin`
142147
* `formatter`: each message to send is transformed using this method, then appended with "\n"
143148
* `parser`: each line of data (ending with "\n") is parsed with this function and its result is emitted as a message
149+
* `stderrParser`: each line of logs (ending with "\n") is parsed with this function and its result is emitted as a message
144150
* `encoding`: the text encoding to apply on the child process streams (default: "utf8")
145151
* `pythonPath`: The path where to locate the "python" executable. Default: "python"
146152
* `pythonOptions`: Array of option switches to pass to "python"
@@ -154,23 +160,25 @@ PythonShell instances have the following properties:
154160
* `command`: the full command arguments passed to the Python executable
155161
* `stdin`: the Python stdin stream, used to send data to the child process
156162
* `stdout`: the Python stdout stream, used for receiving data from the child process
157-
* `stderr`: the Python stderr stream, used for communicating errors
163+
* `stderr`: the Python stderr stream, used for communicating logs & errors
158164
* `childProcess`: the process instance created via `child_process.spawn`
159165
* `terminated`: boolean indicating whether the process has exited
160166
* `exitCode`: the process exit code, available after the process has ended
161167

162168
Example:
163-
```js
169+
170+
```typescript
164171
// create a new instance
165-
var shell = new PythonShell('script.py', options);
172+
let shell = new PythonShell('script.py', options);
166173
```
167174

168175
#### `#defaultOptions`
169176

170177
Configures default options for all new instances of PythonShell.
171178

172179
Example:
173-
```js
180+
181+
```typescript
174182
// setup a default "scriptPath"
175183
PythonShell.defaultOptions = { scriptPath: '../scripts' };
176184
```
@@ -182,7 +190,8 @@ Runs the Python script and invokes `callback` with the results. The callback con
182190
This method is also returning the `PythonShell` instance.
183191

184192
Example:
185-
```js
193+
194+
```typescript
186195
// run a simple script
187196
PythonShell.run('script.py', function (err, results) {
188197
// script finished
@@ -194,20 +203,25 @@ PythonShell.run('script.py', function (err, results) {
194203
Sends a message to the Python script via stdin. The data is formatted according to the selected mode (text or JSON), or through a custom function when `formatter` is specified.
195204

196205
Example:
197-
```js
206+
207+
```typescript
198208
// send a message in text mode
199-
var shell = new PythonShell('script.py', { mode: 'text '});
209+
let shell = new PythonShell('script.py', { mode: 'text '});
200210
shell.send('hello world!');
201211

202212
// send a message in JSON mode
203-
var shell = new PythonShell('script.py', { mode: 'json '});
213+
let shell = new PythonShell('script.py', { mode: 'json '});
204214
shell.send({ command: "do_stuff", args: [1, 2, 3] });
205215
```
206216

207217
#### `.receive(data)`
208218

209219
Parses incoming data from the Python script written via stdout and emits `message` events. This method is called automatically as data is being received from stdout.
210220

221+
#### `.receiveStderr(data)`
222+
223+
Parses incoming logs from the Python script written via stderr and emits `stderr` events. This method is called automatically as data is being received from stderr.
224+
211225
#### `.end(callback)`
212226

213227
Closes the stdin stream, allowing the Python script to finish and exit. The optional callback is invoked when the process is terminated.
@@ -221,20 +235,35 @@ Terminates the python script, the optional end callback is invoked if specified.
221235
Fires when a chunk of data is parsed from the stdout stream via the `receive` method. If a `parser` method is specified, the result of this function will be the message value. This event is not emitted in binary mode.
222236

223237
Example:
224-
```js
238+
239+
```typescript
225240
// receive a message in text mode
226-
var shell = new PythonShell('script.py', { mode: 'text '});
241+
let shell = new PythonShell('script.py', { mode: 'text '});
227242
shell.on('message', function (message) {
228243
// handle message (a line of text from stdout)
229244
});
230245

231246
// receive a message in JSON mode
232-
var shell = new PythonShell('script.py', { mode: 'json '});
247+
let shell = new PythonShell('script.py', { mode: 'json '});
233248
shell.on('message', function (message) {
234249
// handle message (a line of text from stdout, parsed as JSON)
235250
});
236251
```
237252

253+
#### event: `stderr`
254+
255+
Fires when a chunk of logs is parsed from the stderr stream via the `receiveStderr` method. If a `stderrParser` method is specified, the result of this function will be the message value. This event is not emitted in binary mode.
256+
257+
Example:
258+
259+
```typescript
260+
// receive a message in text mode
261+
let shell = new PythonShell('script.py', { mode: 'text '});
262+
shell.on('stderr', function (stderr) {
263+
// handle stderr (a line of text from stderr)
264+
});
265+
```
266+
238267
#### event: `close`
239268

240269
Fires when the process has been terminated, with an error or not.

index.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface Options extends SpawnOptions{
3232
mode?: 'text'|'json'|'binary'
3333
formatter?: (param:string)=>any
3434
parser?: (param:string)=>any
35+
stderrParser?: (param:string)=>any
3536
encoding?: string
3637
pythonPath?: string
3738
pythonOptions?: string[]
@@ -56,6 +57,7 @@ export class PythonShell extends EventEmitter{
5657
mode:string
5758
formatter:(param:string|Object)=>any
5859
parser:(param:string)=>any
60+
stderrParser:(param:string)=>any
5961
terminated:boolean
6062
childProcess:ChildProcess
6163
stdin: Writable;
@@ -107,6 +109,7 @@ export class PythonShell extends EventEmitter{
107109
this.mode = options.mode || 'text';
108110
this.formatter = resolve('format', options.formatter || this.mode);
109111
this.parser = resolve('parse', options.parser || this.mode);
112+
this.stderrParser = resolve('parse', options.stderrParser || this.mode);
110113
this.terminated = false;
111114
this.childProcess = spawn(pythonPath, this.command, options);
112115

@@ -123,6 +126,7 @@ export class PythonShell extends EventEmitter{
123126
// listen to stderr and emit errors for incoming data
124127
this.stderr.on('data', function (data) {
125128
errorData += ''+data;
129+
self.receiveStderr(data);
126130
});
127131

128132
this.stderr.on('end', function(){
@@ -316,6 +320,20 @@ export class PythonShell extends EventEmitter{
316320
* @param {string|Buffer} data The data to parse into messages
317321
*/
318322
receive(data:string|Buffer) {
323+
return this.recieveInternal(data, 'message');
324+
};
325+
326+
/**
327+
* Parses data received from the Python shell stderr stream and emits "stderr" events
328+
* This method is not used in binary mode
329+
* Override this method to parse incoming logs from the Python process into messages
330+
* @param {string|Buffer} data The data to parse into messages
331+
*/
332+
receiveStderr(data:string|Buffer) {
333+
return this.recieveInternal(data, 'stderr');
334+
};
335+
336+
private recieveInternal(data:string|Buffer, emitType:'message'|'stderr'){
319337
let self = this;
320338
let parts = (''+data).split(new RegExp(newline,'g'));
321339

@@ -332,11 +350,12 @@ export class PythonShell extends EventEmitter{
332350
this._remaining = lastLine;
333351

334352
parts.forEach(function (part) {
335-
self.emit('message', self.parser(part));
353+
if(emitType == 'message') self.emit(emitType, self.parser(part));
354+
else if(emitType == 'stderr') self.emit(emitType, self.stderrParser(part));
336355
});
337356

338357
return this;
339-
};
358+
}
340359

341360
/**
342361
* Closes the stdin stream, which should cause the process to finish its work and close

test/python/stderrLogging.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,15 @@
55

66
# set up logging to file - see previous section for more details
77
logging.basicConfig(level=logging.DEBUG)
8-
# define a Handler which writes INFO messages or higher to the sys.stderr
9-
console = logging.StreamHandler()
10-
console.setLevel(logging.INFO)
11-
# add the handler to the root logger
12-
logging.getLogger('').addHandler(console)
138

149
# Now, we can log to the root logger, or any other logger. First the root...
1510
logging.info('Jackdaws love my big sphinx of quartz.')
1611

1712
# Now, define a couple of other loggers which might represent areas in your
1813
# application:
1914

20-
logger1 = logging.getLogger('myapp.area1')
21-
logger2 = logging.getLogger('myapp.area2')
15+
logger1 = logging.getLogger('log1')
16+
logger2 = logging.getLogger('log2')
2217

2318
logger1.debug('Quick zephyrs blow, vexing daft Jim.')
2419
logger1.info('How quickly daft jumping zebras vex.')

test/test-python-shell.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,48 @@ describe('PythonShell', function () {
275275
});
276276
});
277277

278+
describe('.receiveStderr(data)', function () {
279+
it('should emit stderr logs as strings when mode is "text"', function (done) {
280+
let pyshell = new PythonShell('stderrLogging.py', {
281+
mode: 'text'
282+
});
283+
let count = 0;
284+
pyshell.on('stderr', function (stderr) {
285+
count === 0 && stderr.should.be.exactly('INFO:root:Jackdaws love my big sphinx of quartz.');
286+
count === 1 && stderr.should.be.exactly('DEBUG:log1:Quick zephyrs blow, vexing daft Jim.');
287+
count++;
288+
}).on('close', function () {
289+
count.should.be.exactly(5);
290+
}).send('hello').send('world').end(done);
291+
});
292+
it('should not be invoked when mode is "binary"', function (done) {
293+
let pyshell = new PythonShell('echo_args.py', {
294+
args: ['hello', 'world'],
295+
mode: 'binary'
296+
});
297+
pyshell.receiveStderr = function () {
298+
throw new Error('should not emit stderr in binary mode');
299+
};
300+
pyshell.end(done);
301+
});
302+
it('should use a custom parser function', function (done) {
303+
let pyshell = new PythonShell('stderrLogging.py', {
304+
mode: 'text',
305+
stderrParser: function (stderr) {
306+
return stderr.toUpperCase();
307+
}
308+
});
309+
let count = 0;
310+
pyshell.on('stderr', function (stderr) {
311+
count === 0 && stderr.should.be.exactly('INFO:ROOT:JACKDAWS LOVE MY BIG SPHINX OF QUARTZ.');
312+
count === 1 && stderr.should.be.exactly('DEBUG:LOG1:QUICK ZEPHYRS BLOW, VEXING DAFT JIM.');
313+
count++;
314+
}).on('close', function () {
315+
count.should.be.exactly(5);
316+
}).send('hello').send('world!').end(done);
317+
});
318+
});
319+
278320
describe('.end(callback)', function () {
279321
it('should end normally when exit code is zero', function (done) {
280322
let pyshell = new PythonShell('exit-code.py');

0 commit comments

Comments
 (0)