Skip to content

Commit f9f4698

Browse files
committed
parser & formatter options
new options for formatting and parsing messages with custom functions, inspired from pr #10.
1 parent 8da89c9 commit f9f4698

File tree

3 files changed

+101
-48
lines changed

3 files changed

+101
-48
lines changed

index.js

Lines changed: 55 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ function extend(obj) {
3030
* @constructor
3131
*/
3232
var PythonShell = function (script, options) {
33+
34+
function resolve(type, val) {
35+
if (typeof val === 'string') {
36+
// use a built-in function using its name
37+
return PythonShell[type][val];
38+
} else if (typeof val === 'function') {
39+
// use a custom function
40+
return val;
41+
}
42+
}
43+
3344
var self = this;
3445
var errorData = '';
3546
EventEmitter.call(this);
@@ -42,19 +53,20 @@ var PythonShell = function (script, options) {
4253
this.script = path.join(options.scriptPath || './python', script);
4354
this.command = pythonOptions.concat(this.script, scriptArgs);
4455
this.mode = options.mode || 'text';
45-
this.parser = options.parser;
56+
this.formatter = resolve('format', options.formatter || this.mode);
57+
this.parser = resolve('parse', options.parser || this.mode);
4658
this.terminated = false;
4759
this.childProcess = spawn(pythonPath, this.command, options);
4860

4961
['stdout', 'stdin', 'stderr'].forEach(function (name) {
5062
self[name] = self.childProcess[name];
51-
self.mode !== 'binary' && self[name].setEncoding('utf8');
63+
self.parser && self[name].setEncoding(options.encoding || 'utf8');
5264
});
5365

54-
// listen for incoming data on stdout
55-
this.stdout.on('data', function (data) {
56-
self.mode !== 'binary' && self.receive(data);
57-
});
66+
// parse incoming data on stdout
67+
if (this.parser) {
68+
this.stdout.on('data', PythonShell.prototype.receive.bind(this));
69+
}
5870

5971
// listen to stderr and emit errors for incoming data
6072
this.stderr.on('data', function (data) {
@@ -92,6 +104,27 @@ util.inherits(PythonShell, EventEmitter);
92104
// allow global overrides for options
93105
PythonShell.defaultOptions = {};
94106

107+
// built-in formatters
108+
PythonShell.format = {
109+
text: function toText(data) {
110+
if (typeof data !== 'string') return data.toString();
111+
return data;
112+
},
113+
json: function toJson(data) {
114+
return JSON.stringify(data);
115+
}
116+
};
117+
118+
// built-in parsers
119+
PythonShell.parse = {
120+
text: function asText(data) {
121+
return data;
122+
},
123+
json: function asJson(data) {
124+
return JSON.parse(data);
125+
}
126+
};
127+
95128
/**
96129
* Runs a Python script and returns collected messages
97130
* @param {string} script The script to execute
@@ -144,22 +177,14 @@ PythonShell.prototype.parseError = function (data) {
144177

145178
/**
146179
* Sends a message to the Python shell through stdin
147-
* This method
148180
* Override this method to format data to be sent to the Python process
149181
* @param {string|Object} data The message to send
150182
* @returns {PythonShell} The same instance for chaining calls
151183
*/
152184
PythonShell.prototype.send = function (message) {
153-
if (this.mode === 'binary') {
154-
throw new Error('cannot send a message in binary mode, use stdin directly instead');
155-
} else if (this.mode === 'json') {
156-
// write a JSON formatted message
157-
this.stdin.write(JSON.stringify(message) + '\n');
158-
} else {
159-
// write text-based message (default)
160-
if (typeof message !== 'string') message = message.toString();
161-
this.stdin.write(message + '\n');
162-
}
185+
var data = this.formatter ? this.formatter(message) : message;
186+
if (this.mode !== 'binary') data += '\n';
187+
this.stdin.write(data);
163188
return this;
164189
};
165190

@@ -171,41 +196,28 @@ PythonShell.prototype.send = function (message) {
171196
*/
172197
PythonShell.prototype.receive = function (data) {
173198
var self = this;
174-
var lines = (''+data).split(/\n/g);
199+
var parts = (''+data).split(/\n/g);
175200

176-
if (lines.length === 1) {
201+
if (parts.length === 1) {
177202
// an incomplete record, keep buffering
178-
this._remaining = (this._remaining || '') + lines[0];
203+
this._remaining = (this._remaining || '') + parts[0];
179204
return this;
180205
}
181206

182-
var lastLine = lines.pop();
207+
var lastLine = parts.pop();
183208
// fix the first line with the remaining from the previous iteration of 'receive'
184-
lines[0] = (this._remaining || '') + lines[0];
209+
parts[0] = (this._remaining || '') + parts[0];
185210
// keep the remaining for the next iteration of 'receive'
186211
this._remaining = lastLine;
187212

188-
lines.forEach(function (line) {
189-
if (self.parser) {
190-
try {
191-
self.emit('message', self.parser(line));
192-
} catch(err) {
193-
self.emit('error', extend(
194-
new Error('invalid message: ' + data + ' >> ' + err),
195-
{ inner: err, data: line}
196-
));
197-
}
198-
} else if (self.mode === 'json') {
199-
try {
200-
self.emit('message', JSON.parse(line));
201-
} catch (err) {
202-
self.emit('error', extend(
203-
new Error('invalid JSON message: ' + data + ' >> ' + err),
204-
{ inner: err, data: line}
205-
));
206-
}
207-
} else {
208-
self.emit('message', line);
213+
parts.forEach(function (part) {
214+
try {
215+
self.emit('message', self.parser(part));
216+
} catch(err) {
217+
self.emit('error', extend(
218+
new Error('invalid message: ' + data + ' >> ' + err),
219+
{ inner: err, data: part}
220+
));
209221
}
210222
});
211223

test/python/echo_binary.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import sys
2+
3+
# simple binary echo script
4+
sys.stdout.write(sys.stdin.read())

test/test-python-shell.js

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,35 @@ describe('PythonShell', function () {
9191
done();
9292
});
9393
});
94-
it('should throw when mode is "binary"', function (done) {
94+
it('should use a custom formatter', function (done) {
9595
var pyshell = new PythonShell('echo_text.py', {
96+
formatter: function (message) {
97+
return message.toUpperCase();
98+
}
99+
});
100+
var output = '';
101+
pyshell.stdout.on('data', function (data) {
102+
output += ''+data;
103+
});
104+
pyshell.send('hello').send('world').end(function (err) {
105+
if (err) return done(err);
106+
output.should.be.exactly('HELLO\nWORLD\n');
107+
done();
108+
});
109+
});
110+
it('should write as-is when mode is "binary"', function (done) {
111+
var pyshell = new PythonShell('echo_binary.py', {
96112
mode: 'binary'
97113
});
98-
(function () {
99-
pyshell.send('hello world!');
100-
}).should.throw();
101-
pyshell.end(done);
114+
var output = '';
115+
pyshell.stdout.on('data', function (data) {
116+
output += ''+data;
117+
});
118+
pyshell.send(new Buffer('i am not a string')).end(function (err) {
119+
if (err) return done(err);
120+
output.should.be.exactly('i am not a string');
121+
done();
122+
});
102123
});
103124
});
104125

@@ -150,6 +171,22 @@ describe('PythonShell', function () {
150171
};
151172
pyshell.end(done);
152173
});
174+
it('should use a custom parser function', function (done) {
175+
var pyshell = new PythonShell('echo_text.py', {
176+
mode: 'text',
177+
parser: function (message) {
178+
return message.toUpperCase();
179+
}
180+
});
181+
var count = 0;
182+
pyshell.on('message', function (message) {
183+
count === 0 && message.should.be.exactly('HELLO');
184+
count === 1 && message.should.be.exactly('WORLD!');
185+
count++;
186+
}).on('close', function () {
187+
count.should.be.exactly(2);
188+
}).send('hello').send('world!').end(done);
189+
});
153190
});
154191

155192
describe('.end(callback)', function () {

0 commit comments

Comments
 (0)