From bea01afdad6cbdfd783aac5feb54aa2637150e38 Mon Sep 17 00:00:00 2001 From: almenon Date: Fri, 10 Feb 2023 20:42:10 -0800 Subject: [PATCH 1/7] remove callbacks --- CHANGELOG.md | 9 ++++++ README.md | 28 ++++++---------- index.ts | 68 ++++++++++++++++----------------------- test/test-python-shell.ts | 53 +++++++++++++++--------------- 4 files changed, 73 insertions(+), 85 deletions(-) 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..e4d06cf 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); }); @@ -91,7 +88,6 @@ pyshell.on('message', function (message) { // end the input stream and allow the process to exit pyshell.end(function (err,code,signal) { - if (err) throw err; console.log('The exit code was: ' + code); console.log('The exit signal was: ' + signal); console.log('finished'); @@ -205,32 +201,28 @@ Example: PythonShell.defaultOptions = { scriptPath: '../scripts' }; ``` -#### `#run(script, options, callback)` +#### `#run(script, options)` -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. - -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)` -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 +239,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..27fd62c 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 + * @param scriptPath The path to the script to execute + * @param options The execution options + * @return a promise with the output from the python script */ - 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) { + 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/test/test-python-shell.ts b/test/test-python-shell.ts index 947e287..309fe58 100644 --- a/test/test-python-shell.ts +++ b/test/test-python-shell.ts @@ -117,9 +117,8 @@ 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(); @@ -132,6 +131,11 @@ describe('PythonShell', function () { 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 +148,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 +162,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 +178,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 +195,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 +215,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 +239,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 +253,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 +523,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 +532,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 +564,4 @@ describe('PythonShell', function () { }, 500); }); }); -}); +}); \ No newline at end of file From b47190781be99c13415c7012a389e552e2a554b3 Mon Sep 17 00:00:00 2001 From: almenon Date: Fri, 10 Feb 2023 20:47:56 -0800 Subject: [PATCH 2/7] add convenient script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ede5a33..eb0a662 100644 --- a/package.json +++ b/package.json @@ -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", From 91bca43bf9f99789352d4eac67acd0b4f71b259e Mon Sep 17 00:00:00 2001 From: almenon Date: Fri, 10 Feb 2023 20:49:29 -0800 Subject: [PATCH 3/7] type run --- index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index 27fd62c..2d643a2 100644 --- a/index.ts +++ b/index.ts @@ -310,12 +310,12 @@ export class PythonShell extends EventEmitter { } /** - * Runs a Python script and returns collected messages as a promise + * 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 - * @return a promise with the output from the python script */ - static run(scriptPath: string, options?: Options) { + static run(scriptPath: string, options?: Options): Promise { return new Promise((resolve, reject) => { let pyshell = new PythonShell(scriptPath, options); let output = []; From ff858cfa9ed55bb672be55314ec890d48f8c90a1 Mon Sep 17 00:00:00 2001 From: almenon Date: Fri, 10 Feb 2023 21:09:48 -0800 Subject: [PATCH 4/7] remove bad rebase code --- test/test-python-shell.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test-python-shell.ts b/test/test-python-shell.ts index 309fe58..17bd8fe 100644 --- a/test/test-python-shell.ts +++ b/test/test-python-shell.ts @@ -123,8 +123,6 @@ describe('PythonShell', function () { 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")'); From 9cdf10a16848e202010006c682405ca791edc20b Mon Sep 17 00:00:00 2001 From: almenon Date: Fri, 10 Feb 2023 21:11:02 -0800 Subject: [PATCH 5/7] fix end example --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e4d06cf..6990203 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ pyshell.on('message', function (message) { // end the input stream and allow the process to exit pyshell.end(function (err,code,signal) { + if (err) throw err; console.log('The exit code was: ' + code); console.log('The exit signal was: ' + signal); console.log('finished'); From fad9eb0c664071a05d601a6a7c33baf79fca79d2 Mon Sep 17 00:00:00 2001 From: almenon Date: Fri, 10 Feb 2023 21:42:41 -0800 Subject: [PATCH 6/7] remove extra arg --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6990203..3f3409a 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ PythonShell.run('script.py', null).then(results => { }); ``` -#### `#runString(code, options, callback)` +#### `#runString(code, options)` 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. From 91f1097bd80e2d3c48c8844bf66c2d0b20ab7daa Mon Sep 17 00:00:00 2001 From: almenon Date: Fri, 10 Feb 2023 21:43:00 -0800 Subject: [PATCH 7/7] 5.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 eb0a662..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"