Integrating Node.js & Python to Write Cross-Language Modules using pyExecJs
The Purpose
To answer the inevitable first question of "why" I'm doing this tutorial, it's because I have been writing more and more JavaScript lately, and I have been using paver, which is a Python module, to automate my JavaScript build tasks (e.g. compiling CoffeeScript, and minification.)
Now, in an attempt to minimize the actual amount of JavaScript I write, I would like to add ng-annotate as a step in my build process. This will allow me to write all of my angular code without thinking about its dependency injection, but still taking advantage of it. However, there currently is no Python module that I can use to run code through ng-annotate, which is a JavaScript library.
PyExecJs
How are we going to write cross-language modules?
With the Python module PyexecJs
! PyexecJs
allows us to write functions in JavaScript, which we can then call from our Python code. The JavaScript functions we write can return data right to our Python code. PyExecJS
also lets us choose the JavaScript engine to use, which helps as we will need to take advantage of the Node engine to work with ng-annotate, since ng-annotate is a Node module.
So to start lets use pip to install the pyexecjs library:
$ pip install pyexecjs
Next we will start our module file (which we will call ng_annotate.py
) by importing the get
function from execjs, which allows us to request a specific JavaScript engine to run our code.
from execjs import get
import os
runtime = get('Node')
Now that we have a runtime engine to work with, we have what we need.
Next, we will need to be sure that if someone installs this, it will have access to the node module ng-annotate. So, create a file called package.json, and put this in it:
{
"dependencies":{
"ng-annotate":"*"
}
}
Finally, we just need to add an instruction to run npm install
before using it.
And now back in our module file, we need to write our JavaScript function that our module will use, as well as make sure it can access the node_modules folder that will be added by npm install
. We do this by wrapping our JavaScript in a call to runtime.compile
so we can access the functions inside from our Python code.
context = runtime.compile('''
module.paths.push('%s');
ng = require('ng-annotate');
function annotate(src,cfg){
return ng(src,cfg);
}
''' % os.path.join(os.path.dirname(__file__),'node_modules'))
Now we can call the annotate
function from our Python code with
context.call('annotate',*args)
, and it will return the result just like it would in the JavaScript. So to make that into a standard Python function we can use, it's as easy as:
def ng_annotate(src,cfg=None):
if cfg is None:
cfg = dict(add=True)
return context.call('annotate',src,cfg)
Now we can use it from our Python code with from ng_annotate import ng_annotate
and calling that function, but let's also make it a convenient command line tool:
import sys
def main():
print ng_annotate(open(sys.argv[-1],'r').read())
if __name__ == "__main__":
main()
Here's the whole ng_annotate.py file for reference:
import os
import sys
from execjs import get
runtime = get('Node')
context = runtime.compile('''
module.paths.push('%s');
var ng = require('ng-annotate');
function annotate(src,cfg){
return ng(src,cfg);
}
''' % os.path.join(os.path.dirname(__file__),'node_modules'))
def ng_annotate(src,cfg=None):
if cfg is None:
cfg = dict(add=True)
return context.call('annotate',src,cfg)
def main():
print ng_annotate(open(sys.argv[-1],'r').read())
if __name__ == "__main__":
main()
Great article! I want to wrap TS with Python and this is my first contact with TS/JS, so that would have been very useful. I missed some link to guide the reader about the installation of nvm and node.js.
Steps I have followed are here: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
Opened a PowerShell and typed: nvm current to see if all was OK. I got:
v18.13.0
Then I followed your instructions step by step.
As I am using python 3, I found the print needs parenthesis.
Finally, I ran:
python .\ng_annotate.py
And I got:
{‘errors’: [“error: couldn’t process source due to parse error”, “‘import’ and ‘export’ may appear only with ‘sourceType: module’ (1:0)”]}
I did some research about ng_annotate to try to understand what I was doing, and I found it is deprecated: https://www.npmjs.com/package/ng-annotate
I hope I have learned enough so far to wrap some TS functions myself.
Despite my previous comments, I found your post very useful !! Thank you !!
import os
import sys
from execjs import get
import json
runtime = get(‘Node’)
context = runtime.compile(’’’
module.paths.push(’%s’);
‘use strict’;
var amphtmlValidator = require(‘amphtml-validator’);
function validate(params,cb) {
amphtmlValidator.getInstance().then((validator) => {
cb(JSON.stringify(validator.validateString(params)))
})
}
‘’’ % os.path.join(os.path.dirname(file),‘node_modules’))
def callback(params):
print(str(params))
def ng_annotate(params):
return context.call(‘validate’,params,callback)
def main():
ng_annotate(’<html>Hello, world.</html>’)
if name == “main”:
main()
It shows some errors while runnig?
I would need to see the error to be sure but I’m guessing it doesn’t like passing a python function into the context, just have the JavaScript return a value, then in python print it, because js has no “print” or in the JavaScript console.log the value
Thanks for the post. Worked perfectly.
Haha, I’m very selfish in that if I didn’t ever need this at some point it may not have been written, but it’s nice to hear others have found uses for it, that is exactly why I wrote this post. Thanks for letting me know it worked out for u.