diff --git a/CHANGELOG.md b/CHANGELOG.md index 295a6ea..d19321e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [5.0.0] - 2023-02-10 +### BREAKING CHANGES +- run and runString now return a promise instead of a using a callback. +- You will need to 1) check the return value, 2) remove the callback argument, and 3) change to a promise +- see readme for usage examples + +### Other notes +- I confirmed that python-shell works with python 3.11 and node v18. + ## [4.0.0] - 2023-02-10 ### Changed - run and runString now return a promise instead of a using a callback. diff --git a/README.md b/README.md index be2fba1..3f3409a 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,7 @@ npm install python-shell ```typescript import {PythonShell} from 'python-shell'; -PythonShell.runString('x=1+1;print(x)', null, function (err) { - if (err) throw err; +PythonShell.runString('x=1+1;print(x)', null).then(messages=>{ console.log('finished'); }); ``` @@ -47,8 +46,7 @@ let {PythonShell} = require('python-shell') ```typescript import {PythonShell} from 'python-shell'; -PythonShell.run('my_script.py', null, function (err) { - if (err) throw err; +PythonShell.run('my_script.py', null).then(messages=>{ console.log('finished'); }); ``` @@ -68,8 +66,7 @@ let options = { args: ['value1', 'value2', 'value3'] }; -PythonShell.run('my_script.py', options, function (err, results) { - if (err) throw err; +PythonShell.run('my_script.py', options).then(messages=>{ // results is an array consisting of messages collected during execution console.log('results: %j', results); }); @@ -205,32 +202,28 @@ Example: PythonShell.defaultOptions = { scriptPath: '../scripts' }; ``` -#### `#run(script, options, callback)` - -Runs the Python script 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. +#### `#run(script, options)` -This method is also returning the `PythonShell` instance. +Runs the Python script and returns a promise. When you handle the promise the argument will be an array of messages emitted from the Python script. Example: ```typescript // run a simple script -PythonShell.run('script.py', null, function (err, results) { +PythonShell.run('script.py', null).then(results => { // script finished }); ``` -#### `#runString(code, options, callback)` +#### `#runString(code, options)` -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. +Runs the Python script and returns a promise. When you handle the promise the argument will be an array of messages emitted from the Python script. Example: ```typescript -// run a simple script -PythonShell.runString('x=1;print(x)', null, function (err, results) { +// run some simple code +PythonShell.runString('x=1;print(x)', null).then(messages=>{ // script finished }); ``` @@ -247,7 +240,7 @@ Promise is rejected if there is a syntax error. #### `#getVersion(pythonPath?:string)` -Returns the python version. Optional pythonPath param to get the version +Returns the python version as a promise. Optional pythonPath param to get the version of a specific python interpreter. #### `#getVersionSync(pythonPath?:string)` diff --git a/index.ts b/index.ts index 634232b..2d643a2 100644 --- a/index.ts +++ b/index.ts @@ -67,6 +67,10 @@ export class PythonShellError extends Error { exitCode?: number; } +export class PythonShellErrorWithLogs extends PythonShellError { + logs: any[] +} + /** * Takes in a string stream and emits batches seperated by newlines */ @@ -306,62 +310,44 @@ export class PythonShell extends EventEmitter { } /** - * 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} (deprecated argument) callback The callback function to invoke with the script results - * @return {Promise | PythonShell} the output from the python script + * Runs a Python script and returns collected messages as a promise. + * If the promise is rejected, the err will probably be of type PythonShellErrorWithLogs + * @param scriptPath The path to the script to execute + * @param options The execution options */ - static run(scriptPath: string, options?: Options, callback?: (err?: PythonShellError, output?: any[]) => any) { - - if(callback) { - console.warn('PythonShell.run() callback is deprecated. Use PythonShell.run() promise instead.') - - return this.runLegacy(scriptPath, options, callback); - } - else { - return new Promise((resolve, reject) => { - let pyshell = new PythonShell(scriptPath, options); - let output = []; - - pyshell.on('message', function (message) { - output.push(message); - }).end(function (err) { - if(err) reject(err); - else resolve(output); - }); + static run(scriptPath: string, options?: Options): Promise { + return new Promise((resolve, reject) => { + let pyshell = new PythonShell(scriptPath, options); + let output = []; + + pyshell.on('message', function (message) { + output.push(message); + }).end(function (err) { + if(err){ + (err as PythonShellErrorWithLogs).logs = output + reject(err); + } + else resolve(output); }); - } - }; - - private static runLegacy(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) { - return callback(err ? err : 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 + * Runs the inputted string of python code and returns collected messages as a promise. DO NOT ALLOW UNTRUSTED USER INPUT HERE! + * @param code The python code to execute + * @param options The execution options + * @return a promise with the output from the python script */ - static runString(code: string, options?: Options, callback?: (err: PythonShellError, output?: any[]) => any) { + static runString(code: string, options?: Options) { // put code in temp file const randomInt = getRandomInt(); const filePath = tmpdir + sep + `pythonShellFile${randomInt}.py` writeFileSync(filePath, code); - return PythonShell.run(filePath, options, callback); + return PythonShell.run(filePath, options); }; static getVersion(pythonPath?: string) { diff --git a/package-lock.json b/package-lock.json index 14981de..5cdfd9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python-shell", - "version": "4.0.0", + "version": "5.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "python-shell", - "version": "4.0.0", + "version": "5.0.0", "license": "MIT", "devDependencies": { "@types/mocha": "^8.2.1", diff --git a/package.json b/package.json index ede5a33..013e349 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "python-shell", - "version": "4.0.0", + "version": "5.0.0", "description": "Run Python scripts from Node.js with simple (but efficient) inter-process communication through stdio", "keywords": [ "python" @@ -8,7 +8,8 @@ "scripts": { "test": "tsc -p ./ && mocha -r ts-node/register", "appveyorTest": "tsc -p ./ && nyc mocha --reporter mocha-appveyor-reporter test/*.js", - "compile": "tsc -watch -p ./" + "compile": "tsc -watch -p ./", + "compileOnce": "tsc -p ./" }, "devDependencies": { "@types/mocha": "^8.2.1", diff --git a/test/test-python-shell.ts b/test/test-python-shell.ts index 947e287..17bd8fe 100644 --- a/test/test-python-shell.ts +++ b/test/test-python-shell.ts @@ -117,21 +117,23 @@ describe('PythonShell', function () { before(() => { PythonShell.defaultOptions = {}; }) - it('should be able to execute a string of python code using callbacks', function (done) { - let pythonshell = PythonShell.runString('print("hello");print("world")', null, function (err, results) { - if (err) return done(err); + it('should be able to execute a string of python code', function (done) { + PythonShell.runString('print("hello");print("world")', null).then((results) => { results.should.be.an.Array().and.have.lengthOf(2); results.should.eql(['hello', 'world']); done(); }); - - pythonshell.should.be.an.instanceOf(PythonShell); }); it('should be able to execute a string of python code using promises', async function () { let results = await PythonShell.runString('print("hello");print("world")'); results.should.be.an.Array().and.have.lengthOf(2); results.should.eql(['hello', 'world']); }); + it('should be able to execute a string of python code async', async function () { + let results = await PythonShell.runString('print("hello");print("world")'); + results.should.be.an.Array().and.have.lengthOf(2); + results.should.eql(['hello', 'world']); + }); after(() => { PythonShell.defaultOptions = { // reset to match initial value @@ -144,14 +146,13 @@ describe('PythonShell', function () { it('should run the script and return output data using callbacks', function (done) { PythonShell.run('echo_args.py', { args: ['hello', 'world'] - }, function (err, results) { - if (err) return done(err); + }).then((results) => { results.should.be.an.Array().and.have.lengthOf(2); results.should.eql(['hello', 'world']); done(); }); }); - it('should run the script and return output data using promise', async function () { + it('should run the script and return output data async', async function () { let results = await PythonShell.run('echo_args.py', { args: ['hello', 'world'] }); @@ -159,13 +160,13 @@ describe('PythonShell', function () { results.should.eql(['hello', 'world']); }); it('should try to run the script and fail appropriately', function (done) { - PythonShell.run('unknown_script.py', null, function (err, results) { + PythonShell.run('unknown_script.py', null).catch((err) => { err.should.be.an.Error; err.exitCode.should.be.exactly(2); done(); }); }); - it('should try to run the script and fail appropriately', async function () { + it('should try to run the script and fail appropriately - async', async function () { try { let results = await PythonShell.run('unknown_script.py'); throw new Error(`should not get here because the script should fail` + results); @@ -175,14 +176,16 @@ describe('PythonShell', function () { } }); it('should include both output and error', function (done) { - PythonShell.run('echo_hi_then_error.py', null, function (err, results) { - err.should.be.an.Error; - results.should.eql(['hi']) - done(); + PythonShell.run('echo_hi_then_error.py', null).then((results) => { + done("Error: This promise should never successfully resolve"); + }).catch((err)=>{ + err.logs.should.eql(['hi']) + err.should.be.an.Error + done() }); }); it('should run the script and fail with an extended stack trace', function (done) { - PythonShell.run('error.py', null, function (err, results) { + PythonShell.run('error.py', null).catch((err) => { err.should.be.an.Error; err.exitCode.should.be.exactly(1); err.stack.should.containEql('----- Python Traceback -----'); @@ -190,7 +193,7 @@ describe('PythonShell', function () { }); }); it('should run the script and fail with an extended stack trace even when mode is binary', function (done) { - PythonShell.run('error.py', { mode: "binary" }, function (err, results) { + PythonShell.run('error.py', { mode: "binary" }).catch((err) => { err.should.be.an.Error; err.exitCode.should.be.exactly(1); err.stack.should.containEql('----- Python Traceback -----'); @@ -210,7 +213,7 @@ describe('PythonShell', function () { } } function runSingleErrorScript(callback) { - PythonShell.run('error.py', null, function (err, results) { + PythonShell.run('error.py', null).catch((err) => { err.should.be.an.Error; err.exitCode.should.be.exactly(1); err.stack.should.containEql('----- Python Traceback -----'); @@ -234,8 +237,7 @@ describe('PythonShell', function () { function runSingleScript(callback) { PythonShell.run('echo_args.py', { args: ['hello', 'world'] - }, function (err, results) { - if (err) return done(err); + }).then((results)=> { results.should.be.an.Array().and.have.lengthOf(2); results.should.eql(['hello', 'world']); callback(); @@ -249,14 +251,11 @@ describe('PythonShell', function () { PythonShell.run('-m', { args: ['timeit', '-n 1', `'x=5'`] - }, function (err, results) { - + }).then((results)=> { PythonShell.defaultOptions = { // reset to match initial value scriptPath: pythonFolder }; - - if (err) return done(err); results.should.be.an.Array(); results[0].should.be.an.String(); results[0].slice(0, 6).should.eql('1 loop'); @@ -522,8 +521,8 @@ describe('PythonShell', function () { let pyshell = new PythonShell('error.py'); pyshell.on('pythonError', function (err) { err.stack.should.containEql('----- Python Traceback -----'); - 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'); + err.stack.should.containEql('test' + sep + 'python' + sep + 'error.py", line 4'); + err.stack.should.containEql('test' + sep + 'python' + sep + 'error.py", line 6'); done(); }); }); @@ -531,8 +530,8 @@ describe('PythonShell', function () { let pyshell = new PythonShell('error.py', { mode: 'json' }); pyshell.on('pythonError', function (err) { err.stack.should.containEql('----- Python Traceback -----'); - 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'); + err.stack.should.containEql('test' + sep + 'python' + sep + 'error.py", line 4'); + err.stack.should.containEql('test' + sep + 'python' + sep + 'error.py", line 6'); done(); }); }); @@ -563,4 +562,4 @@ describe('PythonShell', function () { }, 500); }); }); -}); +}); \ No newline at end of file