Skip to content

Commit 67fde32

Browse files
xitij2000therealphildini
authored andcommittedOct 11, 2017
Node script to run Python code using Batavia (beeware#671)
* Added basic command to run some python code using batavia and node * The compile-and-encode script now outputs the filename and bytecode as JSON Cleaned up the run-in-batavia script and updated its description * Replace Python run-in-batavia script with Node-based run_in_batavia script * Output null if module is not found Directly compile Python file given a path Add current directory to Python path so imports work * Improved error handling. * Added documentation for the run_in_batavia.js command. * Added run_in_batavia.js and compile_module.py in the manifest file * Remove old version of loader. * Removed special-case handling of Python paths ending with .py Correction run_with_batavia -> run_in_batavia * Added npm script to allow running Python code as ``npm run python file.py`` * The module compile is now never passed a file name, just the module name * Updated tutorial and readme file for running Python code on command line using Batavia * Allow running ``npm run python`` from subdirectory of project
1 parent 218698b commit 67fde32

8 files changed

+262
-4
lines changed
 

‎MANIFEST.in

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ include AUTHORS
44
include LICENSE
55
include Makefile
66
include requirements*.txt
7+
include run_in_batavia.js
8+
include compile_module.py
79
recursive-include docs *.bat
810
recursive-include docs *.py
911
recursive-include docs *.rst
@@ -12,4 +14,4 @@ recursive-include tests *.py
1214
recursive-include batavia *.js
1315
recursive-include testserver *.html
1416
recursive-include testserver *.py
15-
recursive-include testserver *.txt
17+
recursive-include testserver *.txt

‎README.rst

+13
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ For more detailed instructions, see the `Python In The Browser
127127
<http://batavia.readthedocs.io/en/latest/intro/tutorial-1.html>`_ guide.
128128

129129

130+
Running Batavia in the terminal
131+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
132+
133+
If you want to run some Python code from a file in the terminal, you can also run Batavia on Node: ::
134+
135+
$ npm run python /path/to/python/file.py
136+
137+
This will should run the Python file and show output on the terminal.
138+
139+
For more details see `Running Python code using Batavia from the command line
140+
<http://batavia.readthedocs.io/en/latest/intro/tutorial-2.html>`_.
141+
142+
130143
Documentation
131144
-------------
132145

‎compile_module.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# coding=utf-8
2+
"""Compile Python module and returns its file name and base64-encoded bytecode
3+
as JSON.
4+
For use by 'run_in_batavia' script."""
5+
6+
import argparse
7+
import json
8+
import py_compile
9+
import sys
10+
11+
import base64
12+
import importlib
13+
import os
14+
import tempfile
15+
16+
sys.path.insert(0, os.getcwd())
17+
18+
19+
def get_module_path(module):
20+
module_spec = importlib.util.find_spec(module)
21+
22+
# TODO: handle importing namespace packages
23+
if module_spec is None or module_spec.origin == 'namespace':
24+
return
25+
26+
return module_spec.origin
27+
28+
29+
def python_module_to_b64_pyc(module):
30+
module_file = get_module_path(module)
31+
32+
if module_file is None:
33+
return
34+
35+
fp = tempfile.NamedTemporaryFile(delete=False)
36+
fp.close()
37+
38+
try:
39+
py_compile.compile(module_file, cfile=fp.name)
40+
with open(fp.name, 'rb') as fin:
41+
pyc = fin.read()
42+
finally:
43+
os.unlink(fp.name)
44+
45+
return {
46+
'filename': os.path.basename(module_file),
47+
'bytecode': base64.b64encode(pyc).decode('utf8'),
48+
}
49+
50+
51+
def main():
52+
parser = argparse.ArgumentParser(description=__doc__)
53+
parser.add_argument('module', help='Python module')
54+
55+
args = parser.parse_args()
56+
57+
print(json.dumps(python_module_to_b64_pyc(args.module),
58+
indent=4))
59+
60+
61+
if __name__ == '__main__':
62+
main()

‎docs/intro/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ reference objects and classes defined natively in JavaScript.
1313

1414
tutorial-0
1515
tutorial-1
16+
tutorial-2
1617
faq

‎docs/intro/tutorial-0.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ You now have a working Batavia environment!
8181
Next Steps
8282
----------
8383

84-
Next, we can :doc:`setup the sandbox <tutorial-1>`, and try out
85-
running Python in your browser.
84+
Next, we can :doc:`setup the sandbox <tutorial-1>`, and try out running Python
85+
in your browser. Or your can try running some Python code :doc:`from the
86+
command line <tutorial-2>`.
8687

8788
Troubleshooting Tips
8889
--------------------

‎docs/intro/tutorial-2.rst

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Tutorial: Running Python code using Batavia from the command line
2+
=================================================================
3+
4+
Batavia includes a simple command-line script that you can use to run your
5+
Python code. To use this tool you need to have followed the instructions from
6+
:doc:`tutorial-0`.
7+
8+
You can now run Python code from a code from the command line as follows:
9+
10+
.. code-block:: bash
11+
12+
npm run python /path/to/python_file.py
13+
14+
This runs the ``run_in_batavia.js`` script which in turn runs the Python code.
15+
This command will only work if you call it within the Batavia project directory
16+
and provide it the absolute path to the Python file to run.
17+
18+
You can alternatively directly run the ``run_in_batavia.js`` in Node. If
19+
you are not in the Batavia project directory you can still use this script as
20+
follows:
21+
22+
.. code-bloc:: bash
23+
24+
node /path/to/batavia/run_in_batavia.js /path/to/python_file.py
25+

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"build:noCache": "DISABLE_WEBPACK_CACHE=true node_modules/.bin/webpack --progress",
1515
"watch": "node_modules/.bin/webpack --progress --watch",
1616
"serve": "node_modules/.bin/webpack-dev-server --inline --hot --port 3000",
17-
"lint": "node_modules/.bin/eslint batavia"
17+
"lint": "node_modules/.bin/eslint batavia",
18+
"python": "./run_in_batavia.js"
1819
},
1920
"repository": {
2021
"type": "git",

‎run_in_batavia.js

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Run Python file in a new Batavia VM.
4+
*/
5+
6+
const fs = require('fs')
7+
const path = require('path')
8+
const {spawnSync} = require('child_process')
9+
const batavia = require('./batavia/batavia.js')
10+
11+
// The compile_module script is in the same directory as this script.
12+
const compileScriptPath = path.join(__dirname, 'compile_module.py')
13+
14+
function displayHelpText() {
15+
console.log(`
16+
usage: [-h] file
17+
18+
Runs Python file in node JS using using the Batavia virtual machine in Node.
19+
20+
positional arguments:
21+
file Python file to run
22+
23+
optional arguments:
24+
-h, --help show this help message and exit`.trim())
25+
}
26+
27+
function main() {
28+
if (process.argv.length === 3) {
29+
const argument = process.argv[2]
30+
31+
if (argument === '-h' || argument === '--help') {
32+
displayHelpText()
33+
return
34+
}
35+
36+
let resolveBase
37+
// The INIT_CWD environment variable is set in npm v5.4 and above. It
38+
// contains the directory in which the npm was run. If we have that
39+
// information we can resolve paths relative to it.
40+
// If the file path not relative and INIT_CWD is defined, we'll resolve
41+
// the provided path relative to it.
42+
if (process.env.INIT_CWD && !path.isAbsolute(argument[0])) {
43+
resolveBase = process.env.INIT_CWD
44+
} else {
45+
resolveBase = ''
46+
}
47+
48+
const filePath = path.resolve(resolveBase, argument)
49+
const modulePath = path.basename(filePath, '.py')
50+
const basePath = path.dirname(filePath)
51+
52+
fs.access(
53+
filePath,
54+
fs.constants.F_OK | fs.constants.R_OK,
55+
function(err) {
56+
if (err) {
57+
console.log(
58+
'File "' + argument + '" does not exist ' +
59+
'or cannot be accessed by current user.')
60+
} else {
61+
try {
62+
runInBatavia(basePath, modulePath)
63+
} catch (error) {
64+
if (error instanceof TypeError) {
65+
console.log('Invalid Python file.')
66+
} else throw error
67+
}
68+
}
69+
})
70+
} else {
71+
displayHelpText()
72+
}
73+
}
74+
75+
/**
76+
* Runs the specified Python module
77+
*
78+
* @param basePath
79+
* @param module
80+
*/
81+
function runInBatavia(basePath, module) {
82+
const vm = new batavia.VirtualMachine({
83+
loader: makeBataviaLoader(basePath),
84+
frame: null
85+
})
86+
87+
vm.run(module, [])
88+
}
89+
90+
/**
91+
* Creates a loader function for the Batavia VM that looks for code around the
92+
* specified base path.
93+
*
94+
* @param basePath
95+
* @returns {bataviaLoader}
96+
*/
97+
function makeBataviaLoader(basePath) {
98+
/**
99+
* Compiles the specified Python module and returns its bytecode in the
100+
* format that Batavia expects. Returns null if module was not found so an
101+
* ImportError can be raised.
102+
*
103+
* @param modulePath
104+
* @returns {*}
105+
*/
106+
function bataviaLoader(modulePath) {
107+
const compileProcess = spawnSync(
108+
'python3',
109+
[compileScriptPath, modulePath],
110+
{cwd: basePath}
111+
)
112+
113+
checkForErrors(compileProcess)
114+
115+
const module = JSON.parse(compileProcess.stdout)
116+
117+
// Module compiler will output null if the specified module was not
118+
// found.
119+
if (!module) { return null }
120+
121+
return {
122+
'__python__': true,
123+
'bytecode': module.bytecode.trim(),
124+
'filename': module.filename.trim()
125+
}
126+
}
127+
128+
return bataviaLoader
129+
}
130+
131+
/**
132+
* Checks the Python compile process result for errors. In case of error it
133+
* alerts the user quits the program.
134+
*
135+
* @param processResult
136+
*/
137+
function checkForErrors(processResult) {
138+
if (processResult.error) {
139+
console.log(
140+
'There was an error running the Python compile process.\n' +
141+
'Ensure that Python 3 is installed and available as "python3".')
142+
process.exit(1)
143+
}
144+
if (processResult.status !== 0) {
145+
console.log('There was an error during import.')
146+
console.log(processResult.stderr.toString())
147+
process.exit(1)
148+
}
149+
}
150+
151+
if (require.main === module) {
152+
main()
153+
}

0 commit comments

Comments
 (0)
Please sign in to comment.