diff --git a/.gitignore b/.gitignore index 3c3629e..2244430 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,114 @@ node_modules +npm-debug.log.* +.vscode +personal +*.js +*.js.map + +# DEFAULT PYTHON GITIGNORE + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +.static_storage/ +.media/ +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ \ No newline at end of file diff --git a/README.md b/README.md index 66d690c..ddcfb92 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# python-shell +# python-shell [![Build status](https://ci.appveyor.com/api/projects/status/m8e3h53vvxg5wb2q?svg=true)](https://ci.appveyor.com/project/Almenon/python-shell) A simple way to run Python scripts from Node.js with basic but efficient inter-process communication and better error handling. @@ -23,10 +23,23 @@ npm test ## Documentation +### Running python code: + +```typescript +import {PythonShell} from 'python-shell'; + +PythonShell.runString('x=1+1;print(x)', function (err) { + if (err) throw err; + console.log('finished'); +}); +``` + +If the script exits with a non-zero code, an error will be thrown. + ### Running a Python script: -```js -var PythonShell = require('python-shell'); +```typescript +import {PythonShell} from 'python-shell'; PythonShell.run('my_script.py', function (err) { if (err) throw err; @@ -34,17 +47,17 @@ PythonShell.run('my_script.py', function (err) { }); ``` -If the script writes to stderr or exits with a non-zero code, an error will be thrown. +If the script exits with a non-zero code, an error will be thrown. ### Running a Python script with arguments and options: -```js -var PythonShell = require('python-shell'); +```typescript +import {PythonShell} from 'python-shell'; -var options = { +let options = { mode: 'text', pythonPath: 'path/to/python', - pythonOptions: ['-u'], + pythonOptions: ['-u'], // get print results in real-time scriptPath: 'path/to/my/scripts', args: ['value1', 'value2', 'value3'] }; @@ -58,9 +71,9 @@ PythonShell.run('my_script.py', options, function (err, results) { ### Exchanging data between Node and Python: -```js -var PythonShell = require('python-shell'); -var pyshell = new PythonShell('my_script.py'); +```typescript +import {PythonShell} from 'python-shell'; +let pyshell = new PythonShell('my_script.py'); // sends a message to the Python script via stdin pyshell.send('hello'); @@ -92,9 +105,10 @@ For more details and examples including Python source code, take a look at the t ### Error Handling and extended stack traces -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. +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. Sample error with traceback (from test/python/error.py): + ``` Traceback (most recent call last): File "test/python/error.py", line 6, in @@ -103,8 +117,10 @@ Traceback (most recent call last): print 1/0 ZeroDivisionError: integer division or modulo by zero ``` + would result into the following error: -```js + +```typescript { [Error: ZeroDivisionError: integer division or modulo by zero] traceback: 'Traceback (most recent call last):\n File "test/python/error.py", line 6, in \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', executable: 'python', @@ -113,7 +129,9 @@ would result into the following error: args: null, exitCode: 1 } ``` + and `err.stack` would look like this: + ``` Error: ZeroDivisionError: integer division or modulo by zero at PythonShell.parseError (python-shell/index.js:131:17) @@ -141,6 +159,7 @@ Creates an instance of `PythonShell` and starts the Python process * `binary`: data is streamed as-is through `stdout` and `stdin` * `formatter`: each message to send is transformed using this method, then appended with "\n" * `parser`: each line of data (ending with "\n") is parsed with this function and its result is emitted as a message + * `stderrParser`: each line of logs (ending with "\n") is parsed with this function and its result is emitted as a message * `encoding`: the text encoding to apply on the child process streams (default: "utf8") * `pythonPath`: The path where to locate the "python" executable. Default: "python" * `pythonOptions`: Array of option switches to pass to "python" @@ -154,15 +173,16 @@ PythonShell instances have the following properties: * `command`: the full command arguments passed to the Python executable * `stdin`: the Python stdin stream, used to send data to the child process * `stdout`: the Python stdout stream, used for receiving data from the child process -* `stderr`: the Python stderr stream, used for communicating errors +* `stderr`: the Python stderr stream, used for communicating logs & errors * `childProcess`: the process instance created via `child_process.spawn` * `terminated`: boolean indicating whether the process has exited * `exitCode`: the process exit code, available after the process has ended Example: -```js + +```typescript // create a new instance -var shell = new PythonShell('script.py', options); +let shell = new PythonShell('script.py', options); ``` #### `#defaultOptions` @@ -170,7 +190,8 @@ var shell = new PythonShell('script.py', options); Configures default options for all new instances of PythonShell. Example: -```js + +```typescript // setup a default "scriptPath" PythonShell.defaultOptions = { scriptPath: '../scripts' }; ``` @@ -182,25 +203,42 @@ Runs the Python script and invokes `callback` with the results. The callback con This method is also returning the `PythonShell` instance. Example: -```js + +```typescript // run a simple script PythonShell.run('script.py', function (err, results) { // script finished }); ``` +#### `#runString(code, options, callback)` + +Runs the Python code and invokes `callback` with the results. The callback contains the execution error (if any) as well as an array of messages emitted from the Python script. + +This method is also returning the `PythonShell` instance. + +Example: + +```typescript +// run a simple script +PythonShell.run('x=1;print(x)', function (err, results) { + // script finished +}); +``` + #### `.send(message)` 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. Example: -```js + +```typescript // send a message in text mode -var shell = new PythonShell('script.py', { mode: 'text '}); +let shell = new PythonShell('script.py', { mode: 'text '}); shell.send('hello world!'); // send a message in JSON mode -var shell = new PythonShell('script.py', { mode: 'json '}); +let shell = new PythonShell('script.py', { mode: 'json '}); shell.send({ command: "do_stuff", args: [1, 2, 3] }); ``` @@ -208,6 +246,10 @@ shell.send({ command: "do_stuff", args: [1, 2, 3] }); 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. +#### `.receiveStderr(data)` + +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. + #### `.end(callback)` Closes the stdin stream, allowing the Python script to finish and exit. The optional callback is invoked when the process is terminated. @@ -216,25 +258,50 @@ Closes the stdin stream, allowing the Python script to finish and exit. The opti Terminates the python script, the optional end callback is invoked if specified. A kill signal may be provided by `signal`, if `signal` is not specified SIGTERM is sent. +#### `checkSyntax(code:string)` + +Checks the syntax of the code and returns a promise. +Promise is rejected if there is a syntax error. + +#### `checkSyntaxFile(filePath:string)` + +Checks the syntax of the file and returns a promise. +Promise is rejected if there is a syntax error. + #### event: `message` 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. Example: -```js + +```typescript // receive a message in text mode -var shell = new PythonShell('script.py', { mode: 'text '}); +let shell = new PythonShell('script.py', { mode: 'text '}); shell.on('message', function (message) { // handle message (a line of text from stdout) }); // receive a message in JSON mode -var shell = new PythonShell('script.py', { mode: 'json '}); +let shell = new PythonShell('script.py', { mode: 'json '}); shell.on('message', function (message) { // handle message (a line of text from stdout, parsed as JSON) }); ``` +#### event: `stderr` + +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. + +Example: + +```typescript +// receive a message in text mode +let shell = new PythonShell('script.py', { mode: 'text '}); +shell.on('stderr', function (stderr) { + // handle stderr (a line of text from stderr) +}); +``` + #### event: `close` Fires when the process has been terminated, with an error or not. diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..86caeb7 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,24 @@ +# this script based off of https://github.com/tolbertam/mocha-appveyor-reporter/blob/master/appveyor.yml + +install: + # Get the latest stable version of Node.js + - ps: Install-Product node '' + - npm install + +build: off + +test_script: + - npm run appveyorTest + +cache: + - node_modules -> package.json + +notifications: + - provider: Email + to: + - almenon214@gmail.com + on_build_success: false + +skip_commits: + files: + - '**/*.md' \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index ec97180..0000000 --- a/index.js +++ /dev/null @@ -1,259 +0,0 @@ -var EventEmitter = require('events').EventEmitter; -var path = require('path'); -var util = require('util'); -var spawn = require('child_process').spawn; - -function toArray(source) { - if (typeof source === 'undefined' || source === null) { - return []; - } else if (!Array.isArray(source)) { - return [source]; - } - return source; -} - -function extend(obj) { - Array.prototype.slice.call(arguments, 1).forEach(function (source) { - if (source) { - for (var key in source) { - obj[key] = source[key]; - } - } - }); - return obj; -} - -/** - * An interactive Python shell exchanging data through stdio - * @param {string} script The python script to execute - * @param {object} [options] The launch options (also passed to child_process.spawn) - * @constructor - */ -var PythonShell = function (script, options) { - - function resolve(type, val) { - if (typeof val === 'string') { - // use a built-in function using its name - return PythonShell[type][val]; - } else if (typeof val === 'function') { - // use a custom function - return val; - } - } - - var self = this; - var errorData = ''; - EventEmitter.call(this); - - options = extend({}, PythonShell.defaultOptions, options); - var pythonPath = options.pythonPath || 'python'; - var pythonOptions = toArray(options.pythonOptions); - var scriptArgs = toArray(options.args); - - this.script = path.join(options.scriptPath || './', script); - this.command = pythonOptions.concat(this.script, scriptArgs); - this.mode = options.mode || 'text'; - this.formatter = resolve('format', options.formatter || this.mode); - this.parser = resolve('parse', options.parser || this.mode); - this.terminated = false; - this.childProcess = spawn(pythonPath, this.command, options); - - ['stdout', 'stdin', 'stderr'].forEach(function (name) { - self[name] = self.childProcess[name]; - self.parser && self[name].setEncoding(options.encoding || 'utf8'); - }); - - // parse incoming data on stdout - if (this.parser) { - this.stdout.on('data', PythonShell.prototype.receive.bind(this)); - } - - // listen to stderr and emit errors for incoming data - this.stderr.on('data', function (data) { - errorData += ''+data; - }); - - this.stderr.on('end', function(){ - self.stderrHasEnded = true - terminateIfNeeded(); - }) - - this.stdout.on('end', function(){ - self.stdoutHasEnded = true - terminateIfNeeded(); - }) - - this.childProcess.on('exit', function (code,signal) { - self.exitCode = code; - self.exitSignal = signal; - terminateIfNeeded(); - }); - - function terminateIfNeeded() { - if(!self.stderrHasEnded || !self.stdoutHasEnded || (self.exitCode == null && self.exitSignal == null)) return; - - var err; - if (errorData || (self.exitCode && self.exitCode !== 0)) { - if (errorData) { - err = self.parseError(errorData); - } else { - err = new Error('process exited with code ' + self.exitCode); - } - err = extend(err, { - executable: pythonPath, - options: pythonOptions.length ? pythonOptions : null, - script: self.script, - args: scriptArgs.length ? scriptArgs : null, - exitCode: self.exitCode - }); - // do not emit error if only a callback is used - if (self.listeners('error').length || !self._endCallback) { - self.emit('error', err); - } - } - - self.terminated = true; - self.emit('close'); - self._endCallback && self._endCallback(err,self.exitCode,self.exitSignal); - }; -}; -util.inherits(PythonShell, EventEmitter); - -// allow global overrides for options -PythonShell.defaultOptions = {}; - -// built-in formatters -PythonShell.format = { - text: function toText(data) { - if (!data) return ''; - else if (typeof data !== 'string') return data.toString(); - return data; - }, - json: function toJson(data) { - return JSON.stringify(data); - } -}; - -// built-in parsers -PythonShell.parse = { - text: function asText(data) { - return data; - }, - json: function asJson(data) { - return JSON.parse(data); - } -}; - -/** - * Runs a Python script and returns collected messages - * @param {string} script The script to execute - * @param {Object} options The execution options - * @param {Function} callback The callback function to invoke with the script results - * @return {PythonShell} The PythonShell instance - */ -PythonShell.run = function (script, options, callback) { - if (typeof options === 'function') { - callback = options; - options = null; - } - - var pyshell = new PythonShell(script, options); - var output = []; - - return pyshell.on('message', function (message) { - output.push(message); - }).end(function (err) { - if (err) return callback(err); - return callback(null, output.length ? output : null); - }); -}; - -/** - * Parses an error thrown from the Python process through stderr - * @param {string|Buffer} data The stderr contents to parse - * @return {Error} The parsed error with extended stack trace when traceback is available - */ -PythonShell.prototype.parseError = function (data) { - var text = ''+data; - var error; - - if (/^Traceback/.test(text)) { - // traceback data is available - var lines = (''+data).trim().split(/\n/g); - var exception = lines.pop(); - error = new Error(exception); - error.traceback = data; - // extend stack trace - error.stack += '\n ----- Python Traceback -----\n '; - error.stack += lines.slice(1).join('\n '); - } else { - // otherwise, create a simpler error with stderr contents - error = new Error(text); - } - - return error; -}; - -/** - * Sends a message to the Python shell through stdin - * Override this method to format data to be sent to the Python process - * @param {string|Object} data The message to send - * @returns {PythonShell} The same instance for chaining calls - */ -PythonShell.prototype.send = function (message) { - var data = this.formatter ? this.formatter(message) : message; - if (this.mode !== 'binary') data += '\n'; - this.stdin.write(data); - return this; -}; - -/** - * Parses data received from the Python shell stdout stream and emits "message" events - * This method is not used in binary mode - * Override this method to parse incoming data from the Python process into messages - * @param {string|Buffer} data The data to parse into messages - */ -PythonShell.prototype.receive = function (data) { - var self = this; - var parts = (''+data).split(/\n/g); - - if (parts.length === 1) { - // an incomplete record, keep buffering - this._remaining = (this._remaining || '') + parts[0]; - return this; - } - - var lastLine = parts.pop(); - // fix the first line with the remaining from the previous iteration of 'receive' - parts[0] = (this._remaining || '') + parts[0]; - // keep the remaining for the next iteration of 'receive' - this._remaining = lastLine; - - parts.forEach(function (part) { - self.emit('message', self.parser(part)); - }); - - return this; -}; - -/** - * Closes the stdin stream, which should cause the process to finish its work and close - * @returns {PythonShell} The same instance for chaining calls - */ -PythonShell.prototype.end = function (callback) { - this.childProcess.stdin.end(); - this._endCallback = callback; - return this; -}; - -/** - * Closes the stdin stream, which should cause the process to finish its work and close - * @returns {PythonShell} The same instance for chaining calls - */ -PythonShell.prototype.terminate = function (signal) { - this.childProcess.kill(signal); - this.terminated = true; - return this; -}; - -module.exports = PythonShell; diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..01a323e --- /dev/null +++ b/index.ts @@ -0,0 +1,379 @@ +import {EventEmitter} from 'events'; +import { ChildProcess,spawn, SpawnOptions, exec } from 'child_process'; +import {EOL as newline, tmpdir} from 'os'; +import {join, sep} from 'path' +import {Readable,Writable} from 'stream' +import { writeFile, writeFileSync } from 'fs'; + +function toArray(source?:T|T[]):T[] { + if (typeof source === 'undefined' || source === null) { + return []; + } else if (!Array.isArray(source)) { + return [source]; + } + return source; +} + +/** + * adds arguments as properties to obj + */ +function extend(obj:{}, ...args) { + Array.prototype.slice.call(arguments, 1).forEach(function (source) { + if (source) { + for (let key in source) { + obj[key] = source[key]; + } + } + }); + return obj; +} + +export interface Options extends SpawnOptions{ + mode?: 'text'|'json'|'binary' + formatter?: (param:string)=>any + parser?: (param:string)=>any + stderrParser?: (param:string)=>any + encoding?: string + pythonPath?: string + pythonOptions?: string[] + scriptPath?: string + args?: string[] +} + +class PythonShellError extends Error{ + traceback: string | Buffer; + exitCode?:number; +} + +/** + * An interactive Python shell exchanging data through stdio + * @param {string} script The python script to execute + * @param {object} [options] The launch options (also passed to child_process.spawn) + * @constructor + */ +export class PythonShell extends EventEmitter{ + scriptPath:string + command:string[] + mode:string + formatter:(param:string|Object)=>any + parser:(param:string)=>any + stderrParser:(param:string)=>any + terminated:boolean + childProcess:ChildProcess + stdin: Writable; + stdout: Readable; + stderr: Readable; + exitSignal:string; + exitCode:number; + private stderrHasEnded:boolean; + private stdoutHasEnded:boolean; + private _remaining:string + private _endCallback:(err:PythonShellError, exitCode:number, exitSignal:string)=>any + + // starting 2020 python2 is deprecated so we choose 3 as default + // except for windows which just has "python" command + static defaultPythonPath = process.platform != "win32" ? "python3" : "python"; + + static defaultOptions:Options = {}; //allow global overrides for options + + constructor(scriptPath:string, options?:Options) { + super(); + + /** + * returns either pythonshell func (if val string) or custom func (if val Function) + */ + function resolve(type, val:string|Function) { + if (typeof val === 'string') { + // use a built-in function using its name + return PythonShell[type][val]; + } else if (typeof val === 'function') { + // use a custom function + return val; + } + } + + let self = this; + let errorData = ''; + EventEmitter.call(this); + + options = extend({}, PythonShell.defaultOptions, options); + let pythonPath; + if (!options.pythonPath) { + pythonPath = PythonShell.defaultPythonPath; + } else pythonPath = options.pythonPath; + let pythonOptions = toArray(options.pythonOptions); + let scriptArgs = toArray(options.args); + + this.scriptPath = join(options.scriptPath || './', scriptPath); + this.command = pythonOptions.concat(this.scriptPath, scriptArgs); + this.mode = options.mode || 'text'; + this.formatter = resolve('format', options.formatter || this.mode); + this.parser = resolve('parse', options.parser || this.mode); + this.stderrParser = resolve('parse', options.stderrParser || this.mode); + this.terminated = false; + this.childProcess = spawn(pythonPath, this.command, options); + + ['stdout', 'stdin', 'stderr'].forEach(function (name) { + self[name] = self.childProcess[name]; + self.parser && self[name].setEncoding(options.encoding || 'utf8'); + }); + + // parse incoming data on stdout + if (this.parser) { + this.stdout.on('data', this.receive.bind(this)); + } + + // listen to stderr and emit errors for incoming data + this.stderr.on('data', function (data) { + errorData += ''+data; + self.receiveStderr(data); + }); + + this.stderr.on('end', function(){ + self.stderrHasEnded = true + terminateIfNeeded(); + }) + + this.stdout.on('end', function(){ + self.stdoutHasEnded = true + terminateIfNeeded(); + }) + + this.childProcess.on('exit', function (code,signal) { + self.exitCode = code; + self.exitSignal = signal; + terminateIfNeeded(); + }); + + function terminateIfNeeded() { + if(!self.stderrHasEnded || !self.stdoutHasEnded || (self.exitCode == null && self.exitSignal == null)) return; + + let err:PythonShellError; + if (self.exitCode && self.exitCode !== 0) { + if (errorData) { + err = self.parseError(errorData); + } else { + err = new PythonShellError('process exited with code ' + self.exitCode); + } + err = extend(err, { + executable: pythonPath, + options: pythonOptions.length ? pythonOptions : null, + script: self.scriptPath, + args: scriptArgs.length ? scriptArgs : null, + exitCode: self.exitCode + }); + // do not emit error if only a callback is used + if (self.listeners('error').length || !self._endCallback) { + self.emit('error', err); + } + } + + self.terminated = true; + self.emit('close'); + self._endCallback && self._endCallback(err,self.exitCode,self.exitSignal); + }; + } + + // built-in formatters + static format = { + text: function toText(data):string { + if (!data) return ''; + else if (typeof data !== 'string') return data.toString(); + return data; + }, + json: function toJson(data) { + return JSON.stringify(data); + } + }; + + //built-in parsers + static parse = { + text: function asText(data):string { + return data; + }, + json: function asJson(data:string) { + return JSON.parse(data); + } + }; + + /** + * checks syntax without executing code + * @param {string} code + * @returns {Promise} rejects w/ stderr if syntax failure + */ + static async checkSyntax(code:string){ + let randomInt = PythonShell.getRandomInt(); + let filePath = tmpdir + sep + `pythonShellSyntaxCheck${randomInt}.py` + + // todo: replace this with util.promisify (once we no longer support node v7) + return new Promise((resolve, reject) => { + writeFile(filePath, code, (err)=>{ + if (err) reject(err); + resolve(this.checkSyntaxFile(filePath)); + }); + }); + } + + /** + * checks syntax without executing code + * @param {string} filePath + * @returns {Promise} rejects w/ stderr if syntax failure + */ + static async checkSyntaxFile(filePath:string){ + + let compileCommand = `${this.defaultPythonPath} -m py_compile ${filePath}` + + return new Promise((resolve, reject) => { + exec(compileCommand, (error, stdout, stderr) => { + if(error == null) resolve() + else reject(stderr) + }) + }) + } + + /** + * Runs a Python script and returns collected messages + * @param {string} scriptPath The path to the script to execute + * @param {Options} options The execution options + * @param {Function} callback The callback function to invoke with the script results + * @return {PythonShell} The PythonShell instance + */ + static run(scriptPath:string, options?:Options, callback?:(err:PythonShellError, output?:any[])=>any) { + let pyshell = new PythonShell(scriptPath, options); + let output = []; + + return pyshell.on('message', function (message) { + output.push(message); + }).end(function (err) { + if (err) return callback(err); + return callback(null, output.length ? output : null); + }); + }; + + /** + * Runs the inputted string of python code and returns collected messages. DO NOT ALLOW UNTRUSTED USER INPUT HERE! + * @param {string} code The python code to execute + * @param {Options} options The execution options + * @param {Function} callback The callback function to invoke with the script results + * @return {PythonShell} The PythonShell instance + */ + static runString(code:string, options?:Options, callback?:(err:PythonShellError, output?:any[])=>any) { + + // put code in temp file + let randomInt = PythonShell.getRandomInt(); + let filePath = tmpdir + sep + `pythonShellFile${randomInt}.py` + writeFileSync(filePath, code); + + return PythonShell.run(filePath, options, callback); + }; + + /** + * Parses an error thrown from the Python process through stderr + * @param {string|Buffer} data The stderr contents to parse + * @return {Error} The parsed error with extended stack trace when traceback is available + */ + private parseError(data:string|Buffer) { + let text = ''+data; + let error:PythonShellError; + + if (/^Traceback/.test(text)) { + // traceback data is available + let lines = (''+data).trim().split(new RegExp(newline, 'g')); + let exception = lines.pop(); + error = new PythonShellError(exception); + error.traceback = data; + // extend stack trace + error.stack += newline+' ----- Python Traceback -----'+newline+' '; + error.stack += lines.slice(1).join(newline+' '); + } else { + // otherwise, create a simpler error with stderr contents + error = new PythonShellError(text); + } + + return error; + }; + + /** + * gets a random int from 0-10000000000 + */ + private static getRandomInt(){ + return Math.floor(Math.random()*10000000000); + } + + /** + * Sends a message to the Python shell through stdin + * Override this method to format data to be sent to the Python process + * @param {string|Object} data The message to send + * @returns {PythonShell} The same instance for chaining calls + */ + send(message:string|Object) { + let data = this.formatter ? this.formatter(message) : message; + if (this.mode !== 'binary') data += newline; + this.stdin.write(data); + return this; + }; + + /** + * Parses data received from the Python shell stdout stream and emits "message" events + * This method is not used in binary mode + * Override this method to parse incoming data from the Python process into messages + * @param {string|Buffer} data The data to parse into messages + */ + receive(data:string|Buffer) { + return this.recieveInternal(data, 'message'); + }; + + /** + * Parses data received from the Python shell stderr stream and emits "stderr" events + * This method is not used in binary mode + * Override this method to parse incoming logs from the Python process into messages + * @param {string|Buffer} data The data to parse into messages + */ + receiveStderr(data:string|Buffer) { + return this.recieveInternal(data, 'stderr'); + }; + + private recieveInternal(data:string|Buffer, emitType:'message'|'stderr'){ + let self = this; + let parts = (''+data).split(new RegExp(newline,'g')); + + if (parts.length === 1) { + // an incomplete record, keep buffering + this._remaining = (this._remaining || '') + parts[0]; + return this; + } + + let lastLine = parts.pop(); + // fix the first line with the remaining from the previous iteration of 'receive' + parts[0] = (this._remaining || '') + parts[0]; + // keep the remaining for the next iteration of 'receive' + this._remaining = lastLine; + + parts.forEach(function (part) { + if(emitType == 'message') self.emit(emitType, self.parser(part)); + else if(emitType == 'stderr') self.emit(emitType, self.stderrParser(part)); + }); + + return this; + } + + /** + * Closes the stdin stream, which should cause the process to finish its work and close + * @returns {PythonShell} The same instance for chaining calls + */ + end(callback:(err:PythonShellError, exitCode:number,exitSignal:string)=>any) { + this.childProcess.stdin.end(); + this._endCallback = callback; + return this; + }; + + /** + * Closes the stdin stream, which should cause the process to finish its work and close + * @returns {PythonShell} The same instance for chaining calls + */ + terminate(signal?:string) { + this.childProcess.kill(signal); + this.terminated = true; + return this; + }; +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5d4f9e4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,873 @@ +{ + "name": "python-shell", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/mocha": { + "version": "2.2.48", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", + "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==", + "dev": true + }, + "@types/node": { + "version": "9.4.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.4.5.tgz", + "integrity": "sha512-DvC7bzO5797bkApgukxouHmkOdYN2D0yL5olw0RncDpXUa6n39qTVsUi/5g2QJjPgl8qn4zh+4h0sofNoWGLRg==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "dev": true, + "requires": { + "lodash": "^4.14.0" + } + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bl": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", + "integrity": "sha1-/cqHGplxOqANGeO7ukHER4emU5g=", + "dev": true, + "requires": { + "readable-stream": "~2.0.5" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.x.x" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "depd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=", + "dev": true + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "escape-string-regexp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", + "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", + "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", + "dev": true, + "requires": { + "async": "^2.0.1", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.11" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "requires": { + "chalk": "^1.1.1", + "commander": "^2.9.0", + "is-my-json-valid": "^2.12.4", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==", + "dev": true + } + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-my-json-valid": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz", + "integrity": "sha512-Q2khNw+oBlWuaYvEEHtKSw/pCxD2L5Rc1C+UQme9X6JdRDh7m5D7HkozA0qa3DUkQ6VzCnEm8mVIQPyIRkI5sQ==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", + "dev": true + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", + "dev": true + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "dev": true, + "requires": { + "mime-db": "~1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.0.tgz", + "integrity": "sha512-ukB2dF+u4aeJjc6IGtPNnJXfeby5d4ZqySlIBT0OEyva/DrMjVm5HkQxKnHDLKEfEQBsEnwTg9HHhtPHJdTd8w==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + } + } + }, + "mocha-appveyor-reporter": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mocha-appveyor-reporter/-/mocha-appveyor-reporter-0.4.0.tgz", + "integrity": "sha1-gpOC/8Bla2Z+e+ZQoJSgba69Uk8=", + "dev": true, + "requires": { + "request-json": "^0.6.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz", + "integrity": "sha1-HPyyXBCpsrSDBT/zn138kjOQjP4=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "request": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.74.0.tgz", + "integrity": "sha1-dpPKdou7DqXIzgjAhKRe+gW4kqs=", + "dev": true, + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "bl": "~1.1.2", + "caseless": "~0.11.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~1.0.0-rc4", + "har-validator": "~2.0.6", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "node-uuid": "~1.4.7", + "oauth-sign": "~0.8.1", + "qs": "~6.2.0", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "~0.4.1" + } + }, + "request-json": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/request-json/-/request-json-0.6.2.tgz", + "integrity": "sha1-KMdtBdYMU8nhjUCXywFyO5UGkyI=", + "dev": true, + "requires": { + "depd": "1.1.0", + "request": "2.74.0" + } + }, + "should": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.1.tgz", + "integrity": "sha512-l+/NwEMO+DcstsHEwPHRHzC9j4UOE3VQwJGcMWSsD/vqpqHbnQ+1iSHy64Ihmmjx1uiRPD9pFadTSc3MJtXAgw==", + "dev": true, + "requires": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "dev": true, + "requires": { + "should-type": "^1.4.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", + "dev": true + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz", + "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "dev": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "typescript": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz", + "integrity": "sha512-zQIMOmC+372pC/CCVLqnQ0zSBiY7HHodU7mpQdjiZddek4GMj31I3dUJ7gAs9o65X7mnRma6OokOkc6f9jjfBg==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + } + } +} diff --git a/package.json b/package.json index 8878e74..c98b4fe 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,23 @@ { "name": "python-shell", - "version": "0.5.0", + "version": "1.0.0", "description": "Run Python scripts from Node.js with simple (but efficient) inter-process communication through stdio", "keywords": [ "python" ], "scripts": { - "test": "mocha -R spec" + "test": "tsc -p ./ && mocha -R spec", + "appveyorTest": "tsc -p ./ && mocha --ui tdd --reporter mocha-appveyor-reporter test/*.js", + "compile":"tsc -watch -p ./" }, "dependencies": {}, "devDependencies": { - "should": "^6.0.0", - "mocha": "^2.2.5" + "@types/mocha": "^2.2.48", + "@types/node": "^9.3.0", + "mocha": "^5.0.0", + "mocha-appveyor-reporter": "^0.4.0", + "should": "^13.2.1", + "typescript": "^3.0.1" }, "repository": { "type": "git", diff --git a/test/python/echo_args.py b/test/python/echo_args.py index e5a8a86..9676d59 100644 --- a/test/python/echo_args.py +++ b/test/python/echo_args.py @@ -2,4 +2,4 @@ # simple argument echo script for v in sys.argv[1:]: - print v + print(v) diff --git a/test/python/echo_json.py b/test/python/echo_json.py index aa95d68..37b9766 100644 --- a/test/python/echo_json.py +++ b/test/python/echo_json.py @@ -2,4 +2,4 @@ # simple JSON echo script for line in sys.stdin: - print json.dumps(json.loads(line)) + print(json.dumps(json.loads(line))) diff --git a/test/python/echo_text.py b/test/python/echo_text.py index 9dd2901..04e69d0 100644 --- a/test/python/echo_text.py +++ b/test/python/echo_text.py @@ -2,4 +2,4 @@ # simple JSON echo script for line in sys.stdin: - print line[:-1] + print(line[:-1]) diff --git a/test/python/error.py b/test/python/error.py index b5ebc3f..32f03aa 100644 --- a/test/python/error.py +++ b/test/python/error.py @@ -1,6 +1,6 @@ # simple error script def divide_by_zero(): - print 1/0 + print(1/0) divide_by_zero() diff --git a/test/python/stderrLogging.py b/test/python/stderrLogging.py new file mode 100644 index 0000000..919d43f --- /dev/null +++ b/test/python/stderrLogging.py @@ -0,0 +1,21 @@ +# logging example taken from https://docs.python.org/3/howto/logging-cookbook.html +# Note that logging logs to stderr by default + +import logging + +# set up logging to file - see previous section for more details +logging.basicConfig(level=logging.DEBUG) + +# Now, we can log to the root logger, or any other logger. First the root... +logging.info('Jackdaws love my big sphinx of quartz.') + +# Now, define a couple of other loggers which might represent areas in your +# application: + +logger1 = logging.getLogger('log1') +logger2 = logging.getLogger('log2') + +logger1.debug('Quick zephyrs blow, vexing daft Jim.') +logger1.info('How quickly daft jumping zebras vex.') +logger2.warning('Jail zesty vixen who grabbed pay from quack.') +logger2.error('The five boxing wizards jump quickly.') \ No newline at end of file diff --git a/test/test-python-shell.js b/test/test-python-shell.ts similarity index 58% rename from test/test-python-shell.js rename to test/test-python-shell.ts index 1722fac..8a5c3e7 100644 --- a/test/test-python-shell.js +++ b/test/test-python-shell.ts @@ -1,5 +1,7 @@ -var should = require('should'); -var PythonShell = require('..'); +import * as should from 'should'; +import {PythonShell} from '..' +import {sep} from 'path' +import {EOL as newline} from 'os' describe('PythonShell', function () { @@ -9,8 +11,8 @@ describe('PythonShell', function () { describe('#ctor(script, options)', function () { it('should spawn a Python process', function (done) { - var pyshell = new PythonShell('exit-code.py'); - pyshell.command.should.eql(['test/python/exit-code.py']); + let pyshell = new PythonShell('exit-code.py'); + pyshell.command.should.eql(['test' + sep + 'python' + sep + 'exit-code.py']); pyshell.terminated.should.be.false; pyshell.end(function (err) { if (err) return done(err); @@ -19,41 +21,79 @@ describe('PythonShell', function () { }); }); it('should spawn a Python process with options', function (done) { - var pyshell = new PythonShell('exit-code.py', { - pythonOptions: '-u' + let pyshell = new PythonShell('exit-code.py', { + pythonOptions: ['-u'] }); - pyshell.command.should.eql(['-u', 'test/python/exit-code.py']); + pyshell.command.should.eql(['-u', 'test' + sep + 'python' + sep + 'exit-code.py']); pyshell.end(done); }); it('should spawn a Python process with script arguments', function (done) { - var pyshell = new PythonShell('echo_args.py', { + let pyshell = new PythonShell('echo_args.py', { args: ['hello', 'world'] }); - pyshell.command.should.eql(['test/python/echo_args.py', 'hello', 'world']); + pyshell.command.should.eql(['test' + sep + 'python' + sep + 'echo_args.py', 'hello', 'world']); pyshell.end(done); }); }); + describe('#checkSyntax(code:string)', function () { + + // note checkSyntax is a wrapper around checkSyntaxFile + // so this tests checkSyntaxFile as well + + it('should check syntax', function ( done) { + PythonShell.checkSyntax("x=1").then(()=>{ + done(); + }) + }) + + it('should invalidate bad syntax', function ( done) { + PythonShell.checkSyntax("x=").catch(()=>{ + done(); + }) + }) + }) + + describe('#runString(script, options)', function () { + before(()=>{ + PythonShell.defaultOptions = {}; + }) + it('should be able to execute a string of python code', function (done) { + PythonShell.runString('print("hello");print("world")', null, function (err, results) { + if (err) return done(err); + results.should.be.an.Array().and.have.lengthOf(2); + results.should.eql(['hello', 'world']); + done(); + }); + }); + after(()=>{ + PythonShell.defaultOptions = { + // reset to match initial value + scriptPath: './test/python' + }; + }) + }); + describe('#run(script, options)', function () { it('should run the script and return output data', function (done) { PythonShell.run('echo_args.py', { args: ['hello', 'world'] }, function (err, results) { if (err) return done(err); - results.should.be.an.Array.and.have.lengthOf(2); + results.should.be.an.Array().and.have.lengthOf(2); results.should.eql(['hello', 'world']); done(); }); }); it('should try to run the script and fail appropriately', function (done) { - PythonShell.run('unknown_script.py', function (err, results) { + PythonShell.run('unknown_script.py', null, function (err, results) { err.should.be.an.Error; err.exitCode.should.be.exactly(2); done(); }); }); it('should run the script and fail with an extended stack trace', function (done) { - PythonShell.run('error.py', function (err, results) { + PythonShell.run('error.py', null, function (err, results) { err.should.be.an.Error; err.exitCode.should.be.exactly(1); err.stack.should.containEql('----- Python Traceback -----'); @@ -61,11 +101,11 @@ describe('PythonShell', function () { }); }); it('should run multiple scripts and fail with an extended stack trace for each of them', function (done) { - var numberOfTimesToRun = 20; - for (var i = 0; i < numberOfTimesToRun; i++) { + let numberOfTimesToRun = 20; + for (let i = 0; i < numberOfTimesToRun; i++) { runSingleErrorScript(end); } - var count = 0; + let count = 0; function end() { count++; if (count === numberOfTimesToRun) { @@ -73,7 +113,7 @@ describe('PythonShell', function () { } } function runSingleErrorScript(callback) { - PythonShell.run('error.py', function (err, results) { + PythonShell.run('error.py', null, function (err, results) { err.should.be.an.Error; err.exitCode.should.be.exactly(1); err.stack.should.containEql('----- Python Traceback -----'); @@ -83,11 +123,11 @@ describe('PythonShell', function () { }); it('should run multiple scripts and return output data for each of them', function (done) { - var numberOfTimesToRun = 20; - for (var i = 0; i < numberOfTimesToRun; i++) { + let numberOfTimesToRun = 20; + for (let i = 0; i < numberOfTimesToRun; i++) { runSingleScript(end); } - var count = 0; + let count = 0; function end() { count++; if (count === numberOfTimesToRun) { @@ -99,7 +139,7 @@ describe('PythonShell', function () { args: ['hello', 'world'] }, function (err, results) { if (err) return done(err); - results.should.be.an.Array.and.have.lengthOf(2); + results.should.be.an.Array().and.have.lengthOf(2); results.should.eql(['hello', 'world']); callback(); }); @@ -110,54 +150,54 @@ describe('PythonShell', function () { describe('.send(message)', function () { it('should send string messages when mode is "text"', function (done) { - var pyshell = new PythonShell('echo_text.py', { + let pyshell = new PythonShell('echo_text.py', { mode: 'text' }); - var output = ''; + let output = ''; pyshell.stdout.on('data', function (data) { output += ''+data; }); pyshell.send('hello').send('world').end(function (err) { if (err) return done(err); - output.should.be.exactly('hello\nworld\n'); + output.should.be.exactly('hello'+newline+'world'+newline); done(); }); }); it('should send JSON messages when mode is "json"', function (done) { - var pyshell = new PythonShell('echo_json.py', { + let pyshell = new PythonShell('echo_json.py', { mode: 'json' }); - var output = ''; + let output = ''; pyshell.stdout.on('data', function (data) { output += ''+data; }); pyshell.send({ a: 'b' }).send(null).send([1, 2, 3]).end(function (err) { if (err) return done(err); - output.should.be.exactly('{"a": "b"}\nnull\n[1, 2, 3]\n'); + output.should.be.exactly('{"a": "b"}'+newline+'null'+newline+'[1, 2, 3]'+newline); done(); }); }); it('should use a custom formatter', function (done) { - var pyshell = new PythonShell('echo_text.py', { + let pyshell = new PythonShell('echo_text.py', { formatter: function (message) { return message.toUpperCase(); } }); - var output = ''; + let output = ''; pyshell.stdout.on('data', function (data) { output += ''+data; }); pyshell.send('hello').send('world').end(function (err) { if (err) return done(err); - output.should.be.exactly('HELLO\nWORLD\n'); + output.should.be.exactly('HELLO'+newline+'WORLD'+newline+''); done(); }); }); it('should write as-is when mode is "binary"', function (done) { - var pyshell = new PythonShell('echo_binary.py', { + let pyshell = new PythonShell('echo_binary.py', { mode: 'binary' }); - var output = ''; + let output = ''; pyshell.stdout.on('data', function (data) { output += ''+data; }); @@ -171,10 +211,10 @@ describe('PythonShell', function () { describe('.receive(data)', function () { it('should emit messages as strings when mode is "text"', function (done) { - var pyshell = new PythonShell('echo_text.py', { + let pyshell = new PythonShell('echo_text.py', { mode: 'text' }); - var count = 0; + let count = 0; pyshell.on('message', function (message) { count === 0 && message.should.be.exactly('hello'); count === 1 && message.should.be.exactly('world'); @@ -184,10 +224,10 @@ describe('PythonShell', function () { }).send('hello').send('world').end(done); }); it('should emit messages as JSON when mode is "json"', function (done) { - var pyshell = new PythonShell('echo_json.py', { + let pyshell = new PythonShell('echo_json.py', { mode: 'json' }); - var count = 0; + let count = 0; pyshell.send({ a: 'b' }).send(null).send([1, 2, 3, 4, 5]); pyshell.on('message', function (message) { count === 0 && message.should.eql({ a: 'b' }); @@ -199,16 +239,16 @@ describe('PythonShell', function () { }).end(done); }); it('should properly buffer partial messages', function (done) { - var pyshell = new PythonShell('echo_json.py', { + let pyshell = new PythonShell('echo_json.py', { mode: 'json' }); pyshell.on('message', function (message) { message.should.be.an.Object; message.should.eql({ a: true }); - }).receive('{"a"').receive(':').receive('true}\n').end(done); + }).receive('{"a"').receive(':').receive('true}'+newline+'').end(done); }); it('should not be invoked when mode is "binary"', function (done) { - var pyshell = new PythonShell('echo_args.py', { + let pyshell = new PythonShell('echo_args.py', { args: ['hello', 'world'], mode: 'binary' }); @@ -218,13 +258,13 @@ describe('PythonShell', function () { pyshell.end(done); }); it('should use a custom parser function', function (done) { - var pyshell = new PythonShell('echo_text.py', { + let pyshell = new PythonShell('echo_text.py', { mode: 'text', parser: function (message) { return message.toUpperCase(); } }); - var count = 0; + let count = 0; pyshell.on('message', function (message) { count === 0 && message.should.be.exactly('HELLO'); count === 1 && message.should.be.exactly('WORLD!'); @@ -235,9 +275,51 @@ describe('PythonShell', function () { }); }); + describe('.receiveStderr(data)', function () { + it('should emit stderr logs as strings when mode is "text"', function (done) { + let pyshell = new PythonShell('stderrLogging.py', { + mode: 'text' + }); + let count = 0; + pyshell.on('stderr', function (stderr) { + count === 0 && stderr.should.be.exactly('INFO:root:Jackdaws love my big sphinx of quartz.'); + count === 1 && stderr.should.be.exactly('DEBUG:log1:Quick zephyrs blow, vexing daft Jim.'); + count++; + }).on('close', function () { + count.should.be.exactly(5); + }).send('hello').send('world').end(done); + }); + it('should not be invoked when mode is "binary"', function (done) { + let pyshell = new PythonShell('echo_args.py', { + args: ['hello', 'world'], + mode: 'binary' + }); + pyshell.receiveStderr = function () { + throw new Error('should not emit stderr in binary mode'); + }; + pyshell.end(done); + }); + it('should use a custom parser function', function (done) { + let pyshell = new PythonShell('stderrLogging.py', { + mode: 'text', + stderrParser: function (stderr) { + return stderr.toUpperCase(); + } + }); + let count = 0; + pyshell.on('stderr', function (stderr) { + count === 0 && stderr.should.be.exactly('INFO:ROOT:JACKDAWS LOVE MY BIG SPHINX OF QUARTZ.'); + count === 1 && stderr.should.be.exactly('DEBUG:LOG1:QUICK ZEPHYRS BLOW, VEXING DAFT JIM.'); + count++; + }).on('close', function () { + count.should.be.exactly(5); + }).send('hello').send('world!').end(done); + }); + }); + describe('.end(callback)', function () { it('should end normally when exit code is zero', function (done) { - var pyshell = new PythonShell('exit-code.py'); + let pyshell = new PythonShell('exit-code.py'); pyshell.end(function (err,code,signal) { if (err) return done(err); code.should.be.exactly(0); @@ -245,8 +327,8 @@ describe('PythonShell', function () { }); }); it('should emit error if exit code is not zero', function (done) { - var pyshell = new PythonShell('exit-code.py', { - args: 3 + let pyshell = new PythonShell('exit-code.py', { + args: ['3'] }); pyshell.on('error', function (err) { err.should.have.properties({ @@ -256,21 +338,30 @@ describe('PythonShell', function () { done(); }); }); - it('should emit error when data is written to stderr', function (done) { - var pyshell = new PythonShell('error.py'); + it('should emit error when the program exits because of an unhandled exception', function (done) { + let pyshell = new PythonShell('error.py'); pyshell.on('error', function (err) { - err.message.should.be.exactly('ZeroDivisionError: integer division or modulo by zero'); + err.message.should.be.equalOneOf('ZeroDivisionError: integer division or modulo by zero','ZeroDivisionError: division by zero'); err.should.have.property('traceback'); err.traceback.should.containEql('Traceback (most recent call last)'); done(); }); }); + it('should NOT emit error when logging is written to stderr', function (done) { + let pyshell = new PythonShell('stderrLogging.py'); + pyshell.on('error', function (err) { + done(new Error("an error should not have been raised")); + }); + pyshell.on('close', function(){ + done(); + }) + }); }); describe('.parseError(data)', function () { it('should extend error with context properties', function (done) { - var pyshell = new PythonShell('exit-code.py', { - args: 1 + let pyshell = new PythonShell('exit-code.py', { + args: ['1'] }); pyshell.on('error', function (err) { err.should.have.properties(['exitCode', 'script', 'options', 'args']); @@ -278,11 +369,11 @@ describe('PythonShell', function () { }); }); it('should extend err.stack with traceback', function (done) { - var pyshell = new PythonShell('error.py'); + let pyshell = new PythonShell('error.py'); pyshell.on('error', function (err) { err.stack.should.containEql('----- Python Traceback -----'); - err.stack.should.containEql('File "test/python/error.py", line 6'); - err.stack.should.containEql('File "test/python/error.py", line 4'); + err.stack.should.containEql('File "test' + sep + 'python' + sep + 'error.py", line 4'); + err.stack.should.containEql('File "test' + sep + 'python' + sep + 'error.py", line 6'); done(); }); }); @@ -290,14 +381,14 @@ describe('PythonShell', function () { describe('.terminate()', function () { it('set terminated to true', function (done) { - var pyshell = new PythonShell('infinite_loop.py'); + let pyshell = new PythonShell('infinite_loop.py'); pyshell.terminate(); pyshell.terminated.should.be.true done(); }); it('run the end callback if specified', function (done) { - var pyshell = new PythonShell('infinite_loop.py'); - var endCalled = false; + let pyshell = new PythonShell('infinite_loop.py'); + let endCalled = false; pyshell.end(()=>{ endCalled = true; }) @@ -306,8 +397,8 @@ describe('PythonShell', function () { done(); }); it('terminate with correct kill signal', function (done) { - var pyshell = new PythonShell('infinite_loop.py'); - var endCalled = false; + let pyshell = new PythonShell('infinite_loop.py'); + let endCalled = false; pyshell.end(()=>{ endCalled = true; }) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3676a76 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "lib": [ + "es6" + ], + "sourceMap": true, + "rootDir": "." + }, + "exclude": [ + "node_modules", + "personal" + ] +} \ No newline at end of file