Skip to content

Commit

Permalink
Fix ES5 class handling, allow disabling timeouts (#137)
Browse files Browse the repository at this point in the history
* Fix handling of ES5 classes with .new

* update timeout handling

* simplify
  • Loading branch information
extremeheat authored May 29, 2024
1 parent 05ea87e commit a2d7369
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 28 deletions.
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules
__*__
__*
.DS*
src/**/package.json
package-lock.json
*.egg-info
*.egg-info
build
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Requires Node.js 18 and Python 3.8 or newer.
* Object inspection allows you to easily `console.log` or `print()` any foreign objects
* (Bridge to call Python from JS) Python class extension and inheritance. [See pytorch and tensorflow examples](https://github.com/extremeheat/JSPyBridge/blob/master/examples/javascript/pytorch-train.js).
* (Bridge to call JS from Python) Native decorator-based event emitter support
* (Bridge to call JS from Python) **First-class Jupyter Notebook/Google Colab support.** See some Google Colab uses below.
* (Bridge to call JS from Python) First-class Jupyter Notebook/Google Colab support. See some Google Colab uses below.


## Basic usage example
Expand Down Expand Up @@ -300,15 +300,15 @@ for await (const file of files) {
```
</details>

## Details
## Extra details

* When doing a function call, any returned foreign objects will be sent to you as a reference. For example, if you're in JavaScript and do a function call to Python that returns an array, you won't get a JS array back, but you will get a reference to the Python array. You can still access the array normally with the [] notation, as long as you use await.

* This behavior makes it very fast to pass objects directly between same-language functions, avoiding costly cross-language data transfers.

* However, this does not apply with callbacks or non-native function input parameters. The bridge will try to serialize what it can, and will give you a foreign reference if it's unable to serialize something. So if you pass a JS object, you'll get a Python dict, but if the dict contains something like a class, you'll get a reference in its place.

* If you would like the bridge to turn a foreign reference to something native, you can use `.valueOf()` to transfer an object via JSON serialization, or `.blobValueOf()` to write an object into the communication pipe directly.
* (On the bridge to call JavaScript from Python) If you would like the bridge to turn a foreign reference to something native, you can use `.valueOf()` to transfer an object via JSON serialization, or `.blobValueOf()` to write an object into the communication pipe directly.
- `.valueOf()` can be used on any JSON-serializable object, but may be very slow for big data.
- `.blobValueOf()` can be used on any pipe-writeable object implementing the `length` property (e.g. `Buffer`). It can be massively faster by circumventing the JSON+UTF8 encode/decode layer, which is inept for large byte arrays.

Expand All @@ -322,4 +322,4 @@ for await (const file of files) {

* On the bridge to call JavaScript from Python, due to the limiatations of Python and cross-platform IPC, we currently communicate over standard error which means that specific output in JS standard error can interfere with the bridge (as of this writing, the prefices `{"r"` and `blob!` are reserved). A similar issue exists on Windows with Python. You are however very unlikely to have issues with this.

* Function calls will timeout after 100000 ms and throw a `BridgeException` error. That default value can be overridden by defining the new value of `REQ_TIMEOUT` in an environment variable.
* Function calls will timeout after 100000 ms and throw a `BridgeException` error. That default value can be overridden by defining the new value of `REQ_TIMEOUT` in an environment variable, and setting it to 0 will disable timeout checks.
2 changes: 1 addition & 1 deletion src/javascript/js/pyi.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class PythonException extends Error {

async function waitFor (cb, withTimeout, onTimeout) {
let t
if (withTimeout === Infinity) return new Promise(resolve => cb(resolve))
if ((withTimeout === Infinity) || (withTimeout === 0)) return new Promise(resolve => cb(resolve))
const ret = await Promise.race([
new Promise(resolve => cb(resolve)),
new Promise(resolve => { t = setTimeout(() => resolve('timeout'), withTimeout) })
Expand Down
14 changes: 7 additions & 7 deletions src/javascript/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,20 @@ def get(self, ffid):
return self.bridge.m[ffid]


INTERNAL_VARS = ["ffid", "_ix", "_exe", "_pffid", "_pname", "_es6", "_resolved", "_Keys"]
INTERNAL_VARS = ["ffid", "_ix", "_exe", "_pffid", "_pname", "_Is_class", "_Resolved", "_Keys"]

# "Proxy" classes get individually instanciated for every thread and JS object
# that exists. It interacts with an Executor to communicate.
class Proxy(object):
def __init__(self, exe, ffid, prop_ffid=None, prop_name="", es6=False):
def __init__(self, exe, ffid, prop_ffid=None, prop_name="", is_class=False):
self.ffid = ffid
self._exe = exe
self._ix = 0
#
self._pffid = prop_ffid if (prop_ffid != None) else ffid
self._pname = prop_name
self._es6 = es6
self._resolved = {}
self._Is_class = is_class
self._Resolved = {}
self._Keys = None

def _call(self, method, methodType, val):
Expand All @@ -199,7 +199,7 @@ def _call(self, method, methodType, val):
if methodType == "fn":
return Proxy(self._exe, val, self.ffid, method)
if methodType == "class":
return Proxy(self._exe, val, es6=True)
return Proxy(self._exe, val, is_class=True)
if methodType == "obj":
return Proxy(self._exe, val)
if methodType == "inst":
Expand All @@ -214,7 +214,7 @@ def _call(self, method, methodType, val):
def __call__(self, *args, timeout=10, forceRefs=False):
mT, v = (
self._exe.initProp(self._pffid, self._pname, args)
if self._es6
if self._Is_class
else self._exe.callProp(
self._pffid, self._pname, args, timeout=timeout, forceRefs=forceRefs
)
Expand All @@ -226,7 +226,7 @@ def __call__(self, *args, timeout=10, forceRefs=False):
def __getattr__(self, attr):
# Special handling for new keyword for ES5 classes
if attr == "new":
return self._call(self._pname if self._pffid == self.ffid else "", "class", self._pffid)
return Proxy(self._exe, self.ffid, self._pffid, self._pname, True)
methodType, val = self._exe.getProp(self._pffid, attr)
return self._call(attr, methodType, val)

Expand Down
2 changes: 1 addition & 1 deletion src/pythonia/Bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class PyClass {

async function waitFor (cb, withTimeout, onTimeout) {
let t
if (withTimeout === Infinity) return new Promise(resolve => cb(resolve))
if ((withTimeout === Infinity) || (withTimeout === 0)) return new Promise(resolve => cb(resolve))
const ret = await Promise.race([
new Promise(resolve => cb(resolve)),
new Promise(resolve => { t = setTimeout(() => resolve('timeout'), withTimeout) })
Expand Down
30 changes: 17 additions & 13 deletions src/pythonia/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ def ipc(self, action, ffid, attr, args=None):
l = None # the lock
if action == "get": # return obj[prop]
l = self.queue(r, {"r": r, "action": "get", "ffid": ffid, "key": attr})
if action == "init": # return new obj[prop]
elif action == "init": # return new obj[prop]
l = self.queue(r, {"r": r, "action": "init", "ffid": ffid, "key": attr, "args": args})
if action == "inspect": # return require('util').inspect(obj[prop])
elif action == "inspect": # return require('util').inspect(obj[prop])
l = self.queue(r, {"r": r, "action": "inspect", "ffid": ffid, "key": attr})
if action == "serialize": # return JSON.stringify(obj[prop])
elif action == "serialize": # return JSON.stringify(obj[prop])
l = self.queue(r, {"r": r, "action": "serialize", "ffid": ffid})
if action == "keys":
elif action == "set":
l = self.queue(r, {"r": r, "action": "set", "ffid": ffid, "key": attr, "args": args})
elif action == "keys":
l = self.queue(r, {"r": r, "action": "keys", "ffid": ffid})
if action == "raw":
# (not really a FFID, but request ID)
Expand Down Expand Up @@ -90,8 +92,8 @@ def setProp(self, ffid, method, val):
self.pcall(ffid, "set", method, [val])
return True

def callProp(self, ffid, method, args, timeout=None):
resp = self.pcall(ffid, "call", method, args, timeout)
def callProp(self, ffid, method, args, *, timeout=None):
resp = self.pcall(ffid, "call", method, args, timeout=timeout)
return resp

def initProp(self, ffid, method, args):
Expand Down Expand Up @@ -121,19 +123,19 @@ def get(self, ffid):
return self.loop.m[ffid]


INTERNAL_VARS = ["ffid", "_ix", "_exe", "_pffid", "_pname", "_es6", "~class", "_Keys"]
INTERNAL_VARS = ["ffid", "_ix", "_exe", "_pffid", "_pname", "_Is_class", "~class", "_Keys"]

# "Proxy" classes get individually instanciated for every thread and JS object
# that exists. It interacts with an Executor to communicate.
class Proxy(object):
def __init__(self, exe, ffid, prop_ffid=None, prop_name="", es6=False):
def __init__(self, exe, ffid, prop_ffid=None, prop_name="", is_class=False):
self.ffid = ffid
self._exe = exe
self._ix = 0
#
self._pffid = prop_ffid if (prop_ffid != None) else ffid
self._pname = prop_name
self._es6 = es6
self._Is_class = is_class
self._Keys = None

def _call(self, method, methodType, val):
Expand All @@ -143,7 +145,7 @@ def _call(self, method, methodType, val):
if methodType == "fn":
return Proxy(self._exe, val, self.ffid, method)
if methodType == "class":
return Proxy(self._exe, val, es6=True)
return Proxy(self._exe, val, is_class=True)
if methodType == "obj":
return Proxy(self._exe, val)
if methodType == "inst":
Expand All @@ -158,8 +160,10 @@ def _call(self, method, methodType, val):
def __call__(self, *args, timeout=10):
mT, v = (
self._exe.initProp(self._pffid, self._pname, args)
if self._es6
else self._exe.callProp(self._pffid, self._pname, args, timeout)
if self._Is_class
else self._exe.callProp(
self._pffid, self._pname, args, timeout=timeout
)
)
if mT == "fn":
return Proxy(self._exe, v)
Expand All @@ -168,7 +172,7 @@ def __call__(self, *args, timeout=10):
def __getattr__(self, attr):
# Special handling for new keyword for ES5 classes
if attr == "new":
return self._call(self._pname if self._pffid == self.ffid else "", "class", self._pffid)
return Proxy(self._exe, self.ffid, self._pffid, self._pname, True)
methodType, val = self._exe.getProp(self._pffid, attr)
return self._call(attr, methodType, val)

Expand Down

0 comments on commit a2d7369

Please sign in to comment.