@waynew/boa
Version:
Use Python modules seamlessly in Node.js
431 lines (410 loc) • 12.3 kB
JavaScript
;
const util = require('util');
const binary = require('@mapbox/node-pre-gyp');
const path = require('path');
const bindingPath = binary.find(path.resolve(path.join(__dirname, '../package.json')));
const native = require(bindingPath);
const debug = require('debug')('boa');
const DelegatorLoader = require('./delegator-loader');
const { pyInst, builtins } = require('./factory');
const {
PyGetAttrSymbol,
PySetAttrSymbol,
PyGetItemSymbol,
PySetItemSymbol,
GetOwnershipSymbol,
} = require('./utils');
const delegators = DelegatorLoader.load();
// internal symbols
const IterIdxForSeqSymbol = Symbol('The iteration index for sequence');
function getTypeInfo(T) {
const typeo = builtins.__getitem__('type').invoke(asHandleObject(T));
const tinfo = { module: null, name: null };
if (typeo.__hasattr__('__module__')) {
tinfo.module = typeo.__getattr__('__module__').toString();
}
if (typeo.__hasattr__('__name__')) {
tinfo.name = typeo.__getattr__('__name__').toString();
}
return tinfo;
}
function dump(T) {
return pyInst.import('json')
.__getattr__('dumps')
.invoke(asHandleObject(T), {
// use str method to serialize those fields which cannot be serialized by default
default: _internalWrap(builtins).str,
[native.NODE_PYTHON_KWARGS_NAME]: true,
});
}
function getDelegator(type) {
if (typeof type === 'string') {
return delegators[type];
}
const { module, name } = type;
if (Object.prototype.hasOwnProperty.call(delegators, module)) {
return delegators[module][name];
}
return undefined;
}
// The function `wrap(T)` is used to return an object or value for using.
// It depends on the type of `T` in Python world, usually it returns a
// `Proxy` object that's based on `T`, when the type could be represented
// as number/boolean/string/null, the return value would be converted to
// corresponding JS primative.
function wrap(T) {
// if `T` is null or undefined, returning itself by default.
if (T === null || T == undefined) {
return T;
}
const type = getTypeInfo(T);
debug(`start wrapping an object, and its type is "${type.module}.${type.name}"`);
// if `type` is "NoneType", returning the null.
if (type.module === 'builtins' && type.name === 'NoneType') {
return null;
}
// FIXME(Yorkie): directly returning the primitive value on the
// following conditions.
if ([
/** python types convert to primitive values. */
'int', /** Number */
'int64', /** BigInt */
'float', /** Number */
'float64', /** BigDecimal(depends on new tc39 proposal) */
'bool', /** Boolean */
'str', /** String */
/** except for null and undefined */
].includes(type.name)) {
return T.toPrimitive();
}
let fn = getDelegator(T.isCallable() ? 'callee' : type);
if (typeof fn !== 'function') {
fn = getDelegator('default');
}
// use internalWrap to generate proxy object with corresponding delegator.
const wrapped = _internalWrap(T, fn(T, wrap), type);
T[native.NODE_PYTHON_WRAPPED_NAME] = wrapped;
return wrapped;
}
function asHandleObject(T) {
return {
// namely shortcut for Python object.
[native.NODE_PYTHON_HANDLE_NAME]: T
};
}
function _internalWrap(T, src={}, pyType={}) {
Object.defineProperties(src, {
/**
* @property native.NODE_PYTHON_WRAPPED_NAME
* @private
*/
[native.NODE_PYTHON_HANDLE_NAME]: {
enumerable: true,
writable: false,
value: T,
},
/**
* @method native.NODE_PYTHON_JS_DISPATCH
* @private
*/
[native.NODE_PYTHON_JS_DISPATCH]: {
enumerable: true,
// FIXME(Yorkie): temporarily set `configurable` to false here.
// See https://github.com/v8/v8/blob/7.9.317/src/objects/objects.cc#L1176-L1179
//
// The proxy object's get trap handler is inconsistent with this descriptor when
// the value is a function, which means the `inconsistent` to be true, then throwing
// a `kProxyGetNonConfigurableData` error.
//
// In order to better solve, we need to define both `get` and `has` traps in the
// proxy object, and move descriptors to the trap handler.
configurable: true,
writable: false,
value: function(fn, isClassMethod, ...args) {
if (isClassMethod) {
return fn.apply(wrap(args[0]), args.slice(1).map(wrap));
} else {
return fn.apply(this, args.map(wrap));
}
},
},
/**
* @method invoke
* @param {object} args
* @private
*/
invoke: {
enumerable: false,
writable: false,
value: args => {
return T.invoke.apply(T, args);
},
},
/**
* @method toString
* @public
*/
toString: {
configurable: true,
enumerable: false,
writable: false,
value: () => T.toString(),
},
/**
* @method toJSON
* @public
*/
toJSON: {
configurable: true,
enumerable: false,
writable: false,
value: () => {
const type = getTypeInfo(T);
let str;
if (type.module === 'numpy') {
str = dump(T.__getattr__('tolist').invoke());
} else {
str = dump(T);
}
// TODO(Yorkie): more performant way to serialize objects?
return JSON.parse(wrap(str));
},
},
/**
* Shortcut for slicing object.
* @method slice
* @public
*/
slice: {
configurable: true,
enumerable: false,
writable: false,
value: (start, end, step) => {
// slice(start, end, step)
const slice = builtins.__getitem__('slice')
.invoke(start, end, step);
// use the slice object as the key for s[x:y:z]
return wrap(T.__getitem__(asHandleObject(slice)));
},
},
/**
* This is used to custom the console.log output by calling toString().
* @method util.inspect.custom
* @public
*/
[util.inspect.custom]: {
configurable: true,
enumerable: false,
writable: false,
value: () => T.toString(),
},
/**
* @method Symbol.toPrimitive
* @param {string} hint
* @public
*/
// Forward compatible with newer `toPrimitive` spec
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive
[Symbol.toPrimitive]: {
configurable: true,
enumerable: false,
writable: false,
value: () => T.toString(),
},
/**
* Implementation of ES iterator protocol, See:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
*
* @method Symbol.iterator
* @public
*/
[Symbol.iterator]: {
configurable: true,
enumerable: false,
writable: false,
value: () => {
if (T.isIterator()) {
return {
next: () => {
const curr = T.next();
return {
done: curr.done,
value: wrap(curr.value),
};
},
};
}
if (T.isSequence()) {
return {
next: function next() {
if (typeof this[IterIdxForSeqSymbol] === 'number') {
this[IterIdxForSeqSymbol] += 1;
} else {
this[IterIdxForSeqSymbol] = 0;
}
const lengthOfSeq = builtins.__getitem__('len')
.invoke(asHandleObject(T)).toPrimitive();
const index = this[IterIdxForSeqSymbol];
if (index >= lengthOfSeq) {
return { done: true, value: undefined };
}
return {
done: false,
value: wrap(T.__getitem__(index)),
};
}
};
}
throw new TypeError('object is not iteratable or sequence.');
},
},
/**
* @method __hash__
* @public
*/
__hash__: {
configurable: true,
enumerable: true,
writable: false,
value: () => T.__hash__(),
},
/**
* @method [GetOwnershipSymbol]
* @public
*/
[GetOwnershipSymbol]: {
configurable: true,
enumerable: false,
writable: false,
value: () => T.getOwnership(),
},
/**
* @method [PyGetAttrSymbol]
* @public
*/
[PyGetAttrSymbol]: {
configurable: true,
enumerable: true,
writable: false,
value: k => wrap(T.__getattr__(k)),
},
/**
* @method [PySetAttrSymbol]
* @public
*/
[PySetAttrSymbol]: {
configurable: true,
enumerable: true,
writable: false,
value: (k, v) => T.__setattr__(k, v),
},
/**
* @method [PyGetItemSymbol]
* @public
*/
[PyGetItemSymbol]: {
configurable: true,
enumerable: true,
writable: false,
value: k => wrap(T.__getitem__(k)),
},
/**
* @method [PySetItemSymbol]
* @public
*/
[PySetItemSymbol]: {
configurable: true,
enumerable: true,
writable: false,
value: (k, v) => T.__setitem__(k, v),
},
});
// Create the proxy object for handlers
let newTarget;
return (newTarget = new Proxy(src, {
'get'(target, name) {
debug(`get property on "${target.constructor.name}", ` +
`name is "${name.toString()}"`);
const { hasOwnProperty } = Object.prototype;
const constructProto = target.constructor.prototype;
if (hasOwnProperty.call(target, name) /* check for own property */ ||
hasOwnProperty.call(constructProto, name) /* check for inherited one-level */
) {
const value = target[name];
debug(`found "${name.toString()}" from object own properties ` +
`or one-level properties.`);
if (typeof value === 'function') {
// the `next` called on generator expects its caller to be a `generator` type where
// proxy does not have such base class.
if (pyType.module === 'builtins' && pyType.name === 'generator' && name === 'next') {
return value.bind(src);
}
// FIXME(Yorkie): make sure the function's this is correct.
return value.bind(newTarget);
} else {
return value;
}
}
/** Enter the Python world. */
if (typeof name === 'string') {
if (/^[0-9]+$/.test(name)) {
debug('name is detected to be an index.');
const n = parseInt(name, 10);
return wrap(T.__getitem__(n));
}
if (T.__hasattr__(name)) {
debug(`found "${name}" as python attr`);
return wrap(T.__getattr__(name));
}
}
try {
const r = T.__getitem__(name);
if (r != null) {
debug(`found "${name.toString()}" as python item`);
return wrap(r);
}
} catch (e) {
debug(`accessing the item["${name.toString()}"] failed ` +
`with ${e.message}`);
}
},
'set'(target, name, value) {
if (typeof name === 'string') {
if (/^[0-9]+$/.test(name)) {
return T.__setitem__(parseInt(name, 10), value) !== -1;
}
if (T.__hasattr__(name)) {
return T.__setattr__(name, value) !== -1;
}
}
let r = T.__setitem__(name, value);
if (r === -1) {
r = T.__setattr__(name, value);
}
return r !== -1;
},
'apply'(target, thisArg, argumentsList) {
return wrap(target.invoke(argumentsList));
},
'construct'(target, argumentsList, newClass) {
if (newClass.name === 'PythonCallable') {
return wrap(target.invoke(argumentsList));
}
if (!newClass.prototype.$pyclass) {
const pyclass = T.createClass(newClass.name, target);
Object.getOwnPropertyNames(newClass.prototype)
.filter(name => name !== 'constructor' || name !== '__init__')
.forEach(name => {
pyclass.setClassMethod(name, newClass.prototype[name]);
});
newClass.prototype.$pyclass = wrap(pyclass);
}
// return the instance
return newClass.prototype.$pyclass.apply(null, argumentsList);
},
}));
}
module.exports = {
wrap,
_internalWrap,
};