@waynew/boa
Version:
Use Python modules seamlessly in Node.js
219 lines (208 loc) • 5.74 kB
JavaScript
;
const vm = require('vm');
const path = require('path');
const binary = require('@mapbox/node-pre-gyp');
const bindingPath = binary.find(path.resolve(path.join(__dirname, '../package.json')));
const native = require(bindingPath);
const {
notEmpty,
getIndent,
removeIndent,
asHandleObject,
GetOwnershipSymbol,
PyGetAttrSymbol,
PySetAttrSymbol,
PyGetItemSymbol,
PySetItemSymbol,
} = require('./utils');
const { SharedPythonObject } = require('./worker');
const { condaPath, pyInst, globals, builtins } = require('./factory');
const { wrap, _internalWrap } = require('./proxy');
const { getPythonVersion, resolveAndUpdateCondaPath } = require('../tools/utils');
const importedNames = [];
const sharedModules = ['sys', 'torch'];
let defaultSysPath = [];
// reset some envs for Python
setenv(null);
function setenv(externalSearchPath) {
const sys = pyInst.import('sys');
if (!defaultSysPath || !defaultSysPath.length) {
defaultSysPath = vm.runInThisContext(sys.__getattr__('path').toString()) || [];
}
const sysPath = Object.assign([], defaultSysPath);
sysPath.push(path.join(condaPath, `lib/python${getPythonVersion()}/lib-dynload`));
sysPath.push(path.join(resolveAndUpdateCondaPath(), `lib/python${getPythonVersion()}/site-packages`));
if (externalSearchPath) {
sysPath.push(externalSearchPath);
}
sys.__setattr__('path', sysPath);
// reset the cached modules that imported before.
for (let name of importedNames) {
name.split('.').reduce((ns, n, i) => {
const nss = ns + (i === 0 ? n : `.${n}`);
sys.__getattr__('modules').__delitem__(nss);
return nss;
}, '');
}
// set `length` to zero to release all references of the array.
importedNames.length = 0;
}
// shadow copy an object, and returns the new copied object.
function copy(T) {
const fn = pyInst.import('copy').__getattr__('copy');
return fn.invoke(asHandleObject(T));
}
function asBytesObject(str) {
return {
[native.NODE_PYTHON_VALUE_NAME]: str,
[native.NODE_PYTHON_BYTES_NAME]: true,
};
}
module.exports = {
/**
* Reset the Python module environment, it clears the `sys.modules`, and
* add the given search paths if provided.
* @param {string} extraSearchPath
*/
setenv,
/**
* @class SharedPythonObject
*/
SharedPythonObject,
/*
* Import a Python module.
* @method import
* @param {string} name - the module name.
*/
'import': name => {
const pyo = wrap(pyInst.import(name));
if (sharedModules.indexOf(name) === -1 &&
importedNames.indexOf(name) === -1) {
importedNames.push(name);
}
return pyo;
},
/*
* Get the builtins
* @method builtins
*/
'builtins': () => _internalWrap(builtins),
/**
* Create a bytes object.
* @method bytes
* @param {string|Buffer|TypedArray} data - the input data.
*/
'bytes': data => asBytesObject(data),
/**
* Create a keyword arguments objects.
* @method kwargs
* @param {object} input - the kwargs input.
*/
'kwargs': input => {
if (typeof input !== 'object') {
throw new TypeError('input must be an object.');
}
return Object.assign({}, input, {
[native.NODE_PYTHON_KWARGS_NAME]: true,
});
},
/**
* With-statement function, See:
* https://docs.python.org/3/reference/compound_stmts.html#the-with-statement
* @method with
* @param {function} fn
*/
'with': (ctx, fn) => {
if (typeof ctx.__enter__ !== 'function' ||
typeof ctx.__exit__ !== 'function') {
throw new TypeError('the context object must have the ' +
'magic methods: `__enter__`, `__exit__`.');
}
if (typeof fn !== 'function') {
// FIXME(Yorkie): should call __exit__ before throwing the error.
ctx.__exit__(null, null, null);
throw new TypeError('the `fn` must be a function.');
}
return (async () => {
let hitException = false;
let v = null;
try {
v = await fn(ctx.__enter__());
} catch (err) {
hitException = true;
if (!ctx.__exit__(
asHandleObject(err.ptype),
asHandleObject(err.pvalue),
asHandleObject(err.ptrace))) {
// TODO(Yorkie): throw an new error that hides python objects?
throw err;
}
} finally {
if (!hitException) {
ctx.__exit__(null, null, null);
}
}
return v;
})();
},
/**
* Evaluate a Python expression.
* @param {string} strs the Python exprs.
*/
'eval': (strs, ...params) => {
let src = '';
let env = globals;
if (typeof strs === 'string') {
src = strs
} else if (strs.length === 1) {
[src] = strs;
} else {
let idx = 0;
env = copy(globals);
src = strs.reduce((acc, str) => {
let next = acc
next += str;
if (idx < params.length) {
const k = `boa_eval_var_${idx}`;
const v = params[idx];
env.__setitem__(k, v);
next += k;
idx += 1;
}
return next;
}, src);
}
// for multiline executing.
const lines = src.split('\n').filter(notEmpty);
const indent = getIndent(lines);
return wrap(pyInst.eval(
lines.map(removeIndent(indent)).join('\n'),
{ globals: env, locals: env }
));
},
/**
* Symbols
*/
symbols: {
/**
* The symbol is used to get the ownership value on an object.
*/
GetOwnershipSymbol,
/**
* __getattr__
*/
PyGetAttrSymbol,
/**
* __setattr__
*/
PySetAttrSymbol,
/**
* __getitem__
*/
PyGetItemSymbol,
/**
* __setitem__
*/
PySetItemSymbol,
},
};