UNPKG

pythonia

Version:

Bridge to call and interop Python APIs from Node.js

200 lines (181 loc) 6.36 kB
/** * The JavaScript Interface for Python */ const util = require('util') const debug = process.env.DEBUG?.includes('jspybridge') ? console.debug : () => { } const supportsColors = process.env.FORCE_COLOR !== '0' function getType (obj) { if (obj?.ffid) return 'py' if (typeof obj === 'function') { // Some tricks to check if we have a function, class or object if (obj.prototype) { // SO ... we COULD automatically call new for ES5 functions, but this gets complicated. // Since old ES5 classes can be called both with and without new, but with different // behavior. By forcing the new operator, we can no longer access ES5 classes variables // because of lack of support in proxy.py for functions with variables inside.. So instead // just don't call `new` for non-ES6 classes and let the user use the .new() psuedomethod. // The below could would check if the prototype has functions in it and assume class if so. // const props = Object.getOwnPropertyNames(obj.prototype) // if (props.length > 1) return 'class' // The below code just checks to see if we have an ES6 class (non-writable) const desc = Object.getOwnPropertyDescriptor(obj, 'prototype') if (!desc.writable) return 'class' } return 'fn' } if (typeof obj === 'bigint') return 'big' if (obj === null) return 'void' if (typeof obj === 'object') return 'obj' if (!isNaN(obj)) return 'num' if (typeof obj === 'string') return 'string' } class JSBridge { constructor (ipc, pyi) { // This is an ID that increments each time a new object is returned // to Python. this.ffid = 10000 this.pyi = pyi // This contains a refrence map of FFIDs to JS objects. this.m = { 0: { console, require, globalThis } } this.ipc = ipc this.eventMap = {} // ipc.on('message', this.onMessage) } addWeakRef (object, ffid) { const weak = new WeakRef(object) Object.defineProperty(this.m, ffid, { get () { return weak.deref() } }) } async get (r, ffid, attr) { try { var v = await this.m[ffid][attr] var type = v.ffid ? 'py' : getType(v) } catch (e) { return this.ipc.send({ r, key: 'void', val: this.ffid }) } switch (type) { case 'string': return this.ipc.send({ r, key: 'string', val: v }) case 'big': return this.ipc.send({ r, key: 'big', val: Number(v) }) case 'num': return this.ipc.send({ r, key: 'num', val: v }) case 'py': return this.ipc.send({ r, key: 'py', val: v.ffid }) case 'class': this.m[++this.ffid] = v return this.ipc.send({ r, key: 'class', val: this.ffid }) case 'fn': this.m[++this.ffid] = v return this.ipc.send({ r, key: 'fn', val: this.ffid }) case 'obj': this.m[++this.ffid] = v return this.ipc.send({ r, key: 'obj', val: this.ffid }) default: return this.ipc.send({ r, key: 'void', val: this.ffid }) } } set (r, ffid, attr, [val]) { try { this.m[ffid][attr] = val } catch (e) { return this.ipc.send({ r, key: 'error', error: e.stack || JSON.stringify(e) }) } this.ipc.send({ r, key: '', val: true }) } // Call property with new keyword to construct classes init (r, ffid, attr, args) { // console.log('init', r, ffid, attr, args) this.m[++this.ffid] = attr ? new this.m[ffid][attr](...args) : new this.m[ffid](...args) this.ipc.send({ r, key: 'inst', val: this.ffid }) } // Call function with async keyword (also works with sync funcs) async call (r, ffid, attr, args) { try { if (attr) { var v = await this.m[ffid][attr].apply(this.m[ffid], args) // eslint-disable-line } else { var v = await this.m[ffid](...args) // eslint-disable-line } } catch (e) { return this.ipc.send({ r, key: 'error', error: e.stack || JSON.stringify(e) }) } const type = getType(v) // console.log('GetType', type, v) switch (type) { case 'string': return this.ipc.send({ r, key: 'string', val: v }) case 'big': return this.ipc.send({ r, key: 'big', val: Number(v) }) case 'num': return this.ipc.send({ r, key: 'num', val: v }) case 'py': return this.ipc.send({ r, key: 'py', val: v.ffid }) case 'class': this.m[++this.ffid] = v return this.ipc.send({ r, key: 'class', val: this.ffid }) case 'fn': // Fix for functions that return functions, use .call() wrapper // this.m[++this.ffid] = { call: v } this.m[++this.ffid] = v return this.ipc.send({ r, key: 'fn', val: this.ffid }) case 'obj': this.m[++this.ffid] = v return this.ipc.send({ r, key: 'obj', val: this.ffid }) default: return this.ipc.send({ r, key: 'void', val: this.ffid }) } } // called for debug in JS, print() in python via __str__ async inspect (r, ffid, mode) { const colors = supportsColors && (mode === 'str') const s = util.inspect(await this.m[ffid], { colors }) this.ipc.send({ r, val: s }) } // for __dict__ in python (used in json.dumps) async serialize (r, ffid) { const v = await this.m[ffid] this.ipc.send({ r, val: v.valueOf() }) } async keys (r, ffid) { const v = await this.m[ffid] const keys = Object.getOwnPropertyNames(v) this.ipc.send({ r, keys }) } free (r, ffid, attr, args) { for (const id of args) { delete this.m[id] } } process (r, args) { const parse = input => { if (typeof input !== 'object') return for (const k in input) { const v = input[k] if (v && typeof v === 'object') { if (v.ffid) { const proxy = this.pyi.makePyObject(v.ffid) this.m[v.ffid] = proxy input[k] = proxy } else { parse(v) } } else { parse(v) } } } parse(args) } async onMessage ({ r, action, p, ffid, key, args }) { // console.debug('onMessage!', arguments, r, action) try { if (p) { this.process(r, args) } await this[action](r, ffid, key, args) } catch (e) { return this.ipc.send({ r, key: 'error', error: e.stack || JSON.stringify(e) }) } } } module.exports = { JSBridge }