| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,7 +5,8 @@ A simple way to run Python scripts from Node.js with basic but efficient inter-p | |||
| 5 | 5 | ## Features | |
| 6 | 6 | ||
| 7 | 7 | + Reliably spawn Python scripts in a child process | |
| 8 | - + Text, JSON and binary modes | ||
| 8 | + + Built-in text, JSON and binary modes | ||
| 9 | + + Custom parsers and formatters | ||
| 9 | 10 | + Simple and efficient data transfers through stdin and stdout streams | |
| 10 | 11 | + Extended stack traces when an error is thrown | |
| 11 | 12 | ||
@@ -78,6 +79,12 @@ pyshell.end(function (err) { | |||
| 78 | 79 | ||
| 79 | 80 | Use `.send(message)` to send a message to the Python script. Attach the `message` event to listen to messages emitted from the Python script. | |
| 80 | 81 | ||
| 82 | + Use `options.mode` to quickly setup how data is sent and received between your Node and Python applications. | ||
| 83 | + | ||
| 84 | + * use `text` mode for exchanging lines of text | ||
| 85 | + * use `json` mode for exchanging JSON fragments | ||
| 86 | + * use `binary` mode for anything else (data is sent and received as-is) | ||
| 87 | + | ||
| 81 | 88 | For more details and examples including Python source code, take a look at the tests. | |
| 82 | 89 | ||
| 83 | 90 | ### Error Handling and extended stack traces | |
@@ -129,6 +136,9 @@ Creates an instance of `PythonShell` and starts the Python process | |||
| 129 | 136 | * `text`: each line of data (ending with "\n") is emitted as a message (default) | |
| 130 | 137 | * `json`: each line of data (ending with "\n") is parsed as JSON and emitted as a message | |
| 131 | 138 | * `binary`: data is streamed as-is through `stdout` and `stdin` | |
| 139 | + * `formatter`: each message to send is transformed using this method, then appended with "\n" | ||
| 140 | + * `parser`: each line of data (ending with "\n") is parsed with this function and its result is emitted as a message | ||
| 141 | + * `encoding`: the text encoding to apply on the child process streams (default: "utf8") | ||
| 132 | 142 | * `pythonPath`: The path where to locate the "python" executable. Default: "python" | |
| 133 | 143 | * `pythonOptions`: Array of option switches to pass to "python" | |
| 134 | 144 | * `scriptPath`: The default path where to look for scripts. Default: "./python" | |
@@ -178,9 +188,7 @@ PythonShell.run('script.py', function (err, results) { | |||
| 178 | 188 | ||
| 179 | 189 | #### `.send(message)` | |
| 180 | 190 | ||
| 181 | - Sends a message to the Python script via stdin. The data is formatted according to the selected mode (text or JSON). This method can be overridden in order to format the data in some other way. | ||
| 182 | - | ||
| 183 | - This method should not be used in binary mode. | ||
| 191 | + 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. | ||
| 184 | 192 | ||
| 185 | 193 | Example: | |
| 186 | 194 | ```js | |
@@ -195,15 +203,15 @@ shell.send({ command: "do_stuff", args: [1, 2, 3] }); | |||
| 195 | 203 | ||
| 196 | 204 | #### `.receive(data)` | |
| 197 | 205 | ||
| 198 | - Parses incoming data from the Python script written via stdout and emits `message` events. The data is parsed as JSON if mode has been set to "json". This method is called automatically as data is being received from stdout and can be overridden to parse the data differently. | ||
| 206 | + 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. | ||
| 199 | 207 | ||
| 200 | 208 | #### `.end(callback)` | |
| 201 | 209 | ||
| 202 | 210 | Closes the stdin stream, allowing the Python script to finish and exit. The optional callback is invoked when the process is terminated. | |
| 203 | 211 | ||
| 204 | 212 | #### event: `message` | |
| 205 | 213 | ||
| 206 | - Fires when a chunk of data is parsed from the stdout stream via the `receive` method. This event is not emitted in binary mode. | ||
| 214 | + 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. | ||
| 207 | 215 | ||
| 208 | 216 | Example: | |
| 209 | 217 | ```js | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -30,6 +30,17 @@ function extend(obj) { | |||
| 30 | 30 | * @constructor | |
| 31 | 31 | */ | |
| 32 | 32 | 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 | + | ||
| 33 | 44 | var self = this; | |
| 34 | 45 | var errorData = ''; | |
| 35 | 46 | EventEmitter.call(this); | |
@@ -42,18 +53,20 @@ var PythonShell = function (script, options) { | |||
| 42 | 53 | this.script = path.join(options.scriptPath || './python', script); | |
| 43 | 54 | this.command = pythonOptions.concat(this.script, scriptArgs); | |
| 44 | 55 | this.mode = options.mode || 'text'; | |
| 56 | + this.formatter = resolve('format', options.formatter || this.mode); | ||
| 57 | + this.parser = resolve('parse', options.parser || this.mode); | ||
| 45 | 58 | this.terminated = false; | |
| 46 | 59 | this.childProcess = spawn(pythonPath, this.command, options); | |
| 47 | 60 | ||
| 48 | 61 | ['stdout', 'stdin', 'stderr'].forEach(function (name) { | |
| 49 | 62 | self[name] = self.childProcess[name]; | |
| 50 | - self.mode !== 'binary' && self[name].setEncoding('utf8'); | ||
| 63 | + self.parser && self[name].setEncoding(options.encoding || 'utf8'); | ||
| 51 | 64 | }); | |
| 52 | 65 | ||
| 53 | - // listen for incoming data on stdout | ||
| 54 | - this.stdout.on('data', function (data) { | ||
| 55 | - self.mode !== 'binary' && self.receive(data); | ||
| 56 | - }); | ||
| 66 | + // parse incoming data on stdout | ||
| 67 | + if (this.parser) { | ||
| 68 | + this.stdout.on('data', PythonShell.prototype.receive.bind(this)); | ||
| 69 | + } | ||
| 57 | 70 | ||
| 58 | 71 | // listen to stderr and emit errors for incoming data | |
| 59 | 72 | this.stderr.on('data', function (data) { | |
@@ -91,6 +104,28 @@ util.inherits(PythonShell, EventEmitter); | |||
| 91 | 104 | // allow global overrides for options | |
| 92 | 105 | PythonShell.defaultOptions = {}; | |
| 93 | 106 | ||
| 107 | + // built-in formatters | ||
| 108 | + PythonShell.format = { | ||
| 109 | + text: function toText(data) { | ||
| 110 | + if (!data) return ''; | ||
| 111 | + else if (typeof data !== 'string') return data.toString(); | ||
| 112 | + return data; | ||
| 113 | + }, | ||
| 114 | + json: function toJson(data) { | ||
| 115 | + return JSON.stringify(data); | ||
| 116 | + } | ||
| 117 | + }; | ||
| 118 | + | ||
| 119 | + // built-in parsers | ||
| 120 | + PythonShell.parse = { | ||
| 121 | + text: function asText(data) { | ||
| 122 | + return data; | ||
| 123 | + }, | ||
| 124 | + json: function asJson(data) { | ||
| 125 | + return JSON.parse(data); | ||
| 126 | + } | ||
| 127 | + }; | ||
| 128 | + | ||
| 94 | 129 | /** | |
| 95 | 130 | * Runs a Python script and returns collected messages | |
| 96 | 131 | * @param {string} script The script to execute | |
@@ -143,22 +178,14 @@ PythonShell.prototype.parseError = function (data) { | |||
| 143 | 178 | ||
| 144 | 179 | /** | |
| 145 | 180 | * Sends a message to the Python shell through stdin | |
| 146 | - * This method | ||
| 147 | 181 | * Override this method to format data to be sent to the Python process | |
| 148 | 182 | * @param {string|Object} data The message to send | |
| 149 | 183 | * @returns {PythonShell} The same instance for chaining calls | |
| 150 | 184 | */ | |
| 151 | 185 | PythonShell.prototype.send = function (message) { | |
| 152 | - if (this.mode === 'binary') { | ||
| 153 | - throw new Error('cannot send a message in binary mode, use stdin directly instead'); | ||
| 154 | - } else if (this.mode === 'json') { | ||
| 155 | - // write a JSON formatted message | ||
| 156 | - this.stdin.write(JSON.stringify(message) + '\n'); | ||
| 157 | - } else { | ||
| 158 | - // write text-based message (default) | ||
| 159 | - if (typeof message !== 'string') message = message.toString(); | ||
| 160 | - this.stdin.write(message + '\n'); | ||
| 161 | - } | ||
| 186 | + var data = this.formatter ? this.formatter(message) : message; | ||
| 187 | + if (this.mode !== 'binary') data += '\n'; | ||
| 188 | + this.stdin.write(data); | ||
| 162 | 189 | return this; | |
| 163 | 190 | }; | |
| 164 | 191 | ||
@@ -170,32 +197,28 @@ PythonShell.prototype.send = function (message) { | |||
| 170 | 197 | */ | |
| 171 | 198 | PythonShell.prototype.receive = function (data) { | |
| 172 | 199 | var self = this; | |
| 173 | - var lines = (''+data).split(/\n/g); | ||
| 200 | + var parts = (''+data).split(/\n/g); | ||
| 174 | 201 | ||
| 175 | - if (lines.length === 1) { | ||
| 202 | + if (parts.length === 1) { | ||
| 176 | 203 | // an incomplete record, keep buffering | |
| 177 | - this._remaining = (this._remaining || '') + lines[0]; | ||
| 204 | + this._remaining = (this._remaining || '') + parts[0]; | ||
| 178 | 205 | return this; | |
| 179 | 206 | } | |
| 180 | 207 | ||
| 181 | - var lastLine = lines.pop(); | ||
| 208 | + var lastLine = parts.pop(); | ||
| 182 | 209 | // fix the first line with the remaining from the previous iteration of 'receive' | |
| 183 | - lines[0] = (this._remaining || '') + lines[0]; | ||
| 210 | + parts[0] = (this._remaining || '') + parts[0]; | ||
| 184 | 211 | // keep the remaining for the next iteration of 'receive' | |
| 185 | 212 | this._remaining = lastLine; | |
| 186 | 213 | ||
| 187 | - lines.forEach(function (line) { | ||
| 188 | - if (self.mode === 'json') { | ||
| 189 | - try { | ||
| 190 | - self.emit('message', JSON.parse(line)); | ||
| 191 | - } catch (err) { | ||
| 192 | - self.emit('error', extend( | ||
| 193 | - new Error('invalid JSON message: ' + data + ' >> ' + err), | ||
| 194 | - { inner: err, data: line} | ||
| 195 | - )); | ||
| 196 | - } | ||
| 197 | - } else { | ||
| 198 | - self.emit('message', line); | ||
| 214 | + parts.forEach(function (part) { | ||
| 215 | + try { | ||
| 216 | + self.emit('message', self.parser(part)); | ||
| 217 | + } catch(err) { | ||
| 218 | + self.emit('error', extend( | ||
| 219 | + new Error('invalid message: ' + data + ' >> ' + err), | ||
| 220 | + { inner: err, data: part} | ||
| 221 | + )); | ||
| 199 | 222 | } | |
| 200 | 223 | }); | |
| 201 | 224 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,8 +10,8 @@ | |||
| 10 | 10 | }, | |
| 11 | 11 | "dependencies": {}, | |
| 12 | 12 | "devDependencies": { | |
| 13 | - "should": "^3.1.3", | ||
| 14 | - "mocha": "^1.17.1" | ||
| 13 | + "should": "^6.0.0", | ||
| 14 | + "mocha": "^2.2.5" | ||
| 15 | 15 | }, | |
| 16 | 16 | "repository": { | |
| 17 | 17 | "type": "git", | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,4 @@ | |||
| 1 | + import sys | ||
| 2 | + | ||
| 3 | + # simple binary echo script | ||
| 4 | + sys.stdout.write(sys.stdin.read()) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -91,14 +91,35 @@ describe('PythonShell', function () { | |||
| 91 | 91 | done(); | |
| 92 | 92 | }); | |
| 93 | 93 | }); | |
| 94 | - it('should throw when mode is "binary"', function (done) { | ||
| 94 | + it('should use a custom formatter', function (done) { | ||
| 95 | 95 | 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', { | ||
| 96 | 112 | mode: 'binary' | |
| 97 | 113 | }); | |
| 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 | + }); | ||
| 102 | 123 | }); | |
| 103 | 124 | }); | |
| 104 | 125 | ||
@@ -150,6 +171,22 @@ describe('PythonShell', function () { | |||
| 150 | 171 | }; | |
| 151 | 172 | pyshell.end(done); | |
| 152 | 173 | }); | |
| 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 | + }); | ||
| 153 | 190 | }); | |
| 154 | 191 | ||
| 155 | 192 | describe('.end(callback)', function () { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments