mirror of https://github.com/pyodide/pyodide.git
Split PyProxy method definitions into separate js file (#1368)
This commit is contained in:
parent
f2d6137673
commit
d4525a188f
|
@ -8,7 +8,3 @@
|
|||
api/python-api.md
|
||||
api/micropip-api.md
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -686,554 +686,6 @@ EM_JS_REF(JsRef, pyproxy_new, (PyObject * ptrobj), {
|
|||
return Module.hiwire.new_value(proxy);
|
||||
});
|
||||
|
||||
// clang-format off
|
||||
EM_JS_NUM(int, pyproxy_init_js, (), {
|
||||
Module.PyProxies = {};
|
||||
|
||||
function _getPtr(jsobj) {
|
||||
let ptr = jsobj.$$.ptr;
|
||||
if (ptr === null) {
|
||||
throw new Error("Object has already been destroyed");
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let _pyproxyClassMap = new Map();
|
||||
/**
|
||||
* Retreive the appropriate mixins based on the features requested in flags.
|
||||
* Used by pyproxy_new. The "flags" variable is produced by the C function
|
||||
* pyproxy_getflags. Multiple PyProxies with the same set of feature flags
|
||||
* will share the same prototype, so the memory footprint of each individual
|
||||
* PyProxy is minimal.
|
||||
*/
|
||||
Module.getPyProxyClass = function(flags){
|
||||
let result = _pyproxyClassMap.get(flags);
|
||||
if(result){
|
||||
return result;
|
||||
}
|
||||
let descriptors = {};
|
||||
for(let [feature_flag, methods] of [
|
||||
[HAS_LENGTH, Module.PyProxyLengthMethods],
|
||||
[HAS_GET, Module.PyProxyGetItemMethods],
|
||||
[HAS_SET, Module.PyProxySetItemMethods],
|
||||
[HAS_CONTAINS, Module.PyProxyContainsMethods],
|
||||
[IS_ITERABLE, Module.PyProxyIterableMethods],
|
||||
[IS_ITERATOR, Module.PyProxyIteratorMethods],
|
||||
[IS_AWAITABLE, Module.PyProxyAwaitableMethods],
|
||||
[IS_BUFFER, Module.PyProxyBufferMethods],
|
||||
[IS_CALLABLE, Module.PyProxyCallableMethods],
|
||||
]){
|
||||
if(flags & feature_flag){
|
||||
Object.assign(descriptors,
|
||||
Object.getOwnPropertyDescriptors(methods)
|
||||
);
|
||||
}
|
||||
}
|
||||
let new_proto = Object.create(Module.PyProxyClass.prototype, descriptors);
|
||||
function PyProxy(){};
|
||||
PyProxy.prototype = new_proto;
|
||||
_pyproxyClassMap.set(flags, PyProxy);
|
||||
return PyProxy;
|
||||
};
|
||||
|
||||
// Static methods
|
||||
Module.PyProxy = {
|
||||
_getPtr,
|
||||
isPyProxy: function(jsobj) {
|
||||
return jsobj && jsobj.$$ !== undefined && jsobj.$$.type === 'PyProxy';
|
||||
},
|
||||
};
|
||||
|
||||
Module.callPyObject = function(ptrobj, ...jsargs) {
|
||||
let idargs = Module.hiwire.new_value(jsargs);
|
||||
let idresult;
|
||||
try {
|
||||
idresult = __pyproxy_apply(ptrobj, idargs);
|
||||
} catch(e){
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idargs);
|
||||
}
|
||||
if(idresult === 0){
|
||||
_pythonexc2js();
|
||||
}
|
||||
return Module.hiwire.pop_value(idresult);
|
||||
};
|
||||
|
||||
// Now a lot of boilerplate to wrap the abstract Object protocol wrappers
|
||||
// above in Javascript functions.
|
||||
|
||||
Module.PyProxyClass = class {
|
||||
constructor(){
|
||||
throw new TypeError('PyProxy is not a constructor');
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag] (){
|
||||
return "PyProxy";
|
||||
}
|
||||
get type() {
|
||||
let ptrobj = _getPtr(this);
|
||||
return Module.hiwire.pop_value(__pyproxy_type(ptrobj));
|
||||
}
|
||||
toString() {
|
||||
let ptrobj = _getPtr(this);
|
||||
let jsref_repr;
|
||||
try {
|
||||
jsref_repr = __pyproxy_repr(ptrobj);
|
||||
} catch(e){
|
||||
Module.fatal_error(e);
|
||||
}
|
||||
if(jsref_repr === 0){
|
||||
_pythonexc2js();
|
||||
}
|
||||
return Module.hiwire.pop_value(jsref_repr);
|
||||
}
|
||||
destroy() {
|
||||
let ptrobj = _getPtr(this);
|
||||
__pyproxy_destroy(ptrobj);
|
||||
this.$$.ptr = null;
|
||||
}
|
||||
/**
|
||||
* This one doesn't follow the pattern: the inner function
|
||||
* _python2js_with_depth is defined in python2js.c and is not a Python
|
||||
* Object Protocol wrapper.
|
||||
*/
|
||||
toJs(depth = -1){
|
||||
let idresult = _python2js_with_depth(_getPtr(this), depth);
|
||||
let result = Module.hiwire.get_value(idresult);
|
||||
Module.hiwire.decref(idresult);
|
||||
return result;
|
||||
}
|
||||
apply(jsthis, jsargs) {
|
||||
return Module.callPyObject(_getPtr(this), ...jsargs);
|
||||
}
|
||||
call(jsthis, ...jsargs){
|
||||
return Module.callPyObject(_getPtr(this), ...jsargs);
|
||||
}
|
||||
};
|
||||
|
||||
// Controlled by HAS_LENGTH, appears for any object with __len__ or sq_length
|
||||
// or mp_length methods
|
||||
Module.PyProxyLengthMethods = {
|
||||
get length(){
|
||||
let ptrobj = _getPtr(this);
|
||||
let length;
|
||||
try {
|
||||
length = _PyObject_Size(ptrobj);
|
||||
} catch(e) {
|
||||
Module.fatal_error(e);
|
||||
}
|
||||
if(length === -1){
|
||||
_pythonexc2js();
|
||||
}
|
||||
return length;
|
||||
}
|
||||
};
|
||||
|
||||
// Controlled by HAS_GET, appears for any class with __getitem__,
|
||||
// mp_subscript, or sq_item methods
|
||||
Module.PyProxyGetItemMethods = {
|
||||
get : function(key){
|
||||
let ptrobj = _getPtr(this);
|
||||
let idkey = Module.hiwire.new_value(key);
|
||||
let idresult;
|
||||
try {
|
||||
idresult = __pyproxy_getitem(ptrobj, idkey);
|
||||
} catch(e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if(idresult === 0){
|
||||
if(Module._PyErr_Occurred()){
|
||||
_pythonexc2js();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return Module.hiwire.pop_value(idresult);
|
||||
},
|
||||
};
|
||||
|
||||
// Controlled by HAS_SET, appears for any class with __setitem__, __delitem__,
|
||||
// mp_ass_subscript, or sq_ass_item.
|
||||
Module.PyProxySetItemMethods = {
|
||||
set : function(key, value){
|
||||
let ptrobj = _getPtr(this);
|
||||
let idkey = Module.hiwire.new_value(key);
|
||||
let idval = Module.hiwire.new_value(value);
|
||||
let errcode;
|
||||
try {
|
||||
errcode = __pyproxy_setitem(ptrobj, idkey, idval);
|
||||
} catch(e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
Module.hiwire.decref(idval);
|
||||
}
|
||||
if(errcode === -1){
|
||||
_pythonexc2js();
|
||||
}
|
||||
},
|
||||
delete : function(key) {
|
||||
let ptrobj = _getPtr(this);
|
||||
let idkey = Module.hiwire.new_value(key);
|
||||
let errcode;
|
||||
try {
|
||||
errcode = __pyproxy_delitem(ptrobj, idkey);
|
||||
} catch(e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if(errcode === -1){
|
||||
_pythonexc2js();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Controlled by HAS_CONTAINS flag, appears for any class with __contains__ or
|
||||
// sq_contains
|
||||
Module.PyProxyContainsMethods = {
|
||||
has : function(key) {
|
||||
let ptrobj = _getPtr(this);
|
||||
let idkey = Module.hiwire.new_value(key);
|
||||
let result;
|
||||
try {
|
||||
result = __pyproxy_contains(ptrobj, idkey);
|
||||
} catch(e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if(result === -1){
|
||||
_pythonexc2js();
|
||||
}
|
||||
return result === 1;
|
||||
},
|
||||
};
|
||||
|
||||
// Controlled by IS_ITERABLE, appears for any object with __iter__ or tp_iter, unless
|
||||
// they are iterators.
|
||||
// See:
|
||||
// https://docs.python.org/3/c-api/iter.html
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
|
||||
// This avoids allocating a PyProxy wrapper for the temporary iterator.
|
||||
Module.PyProxyIterableMethods = {
|
||||
[Symbol.iterator] : function*() {
|
||||
let iterptr = _PyObject_GetIter(_getPtr(this));
|
||||
if(iterptr === 0){
|
||||
pythonexc2js();
|
||||
}
|
||||
let item;
|
||||
while((item = __pyproxy_iter_next(iterptr))){
|
||||
yield Module.hiwire.pop_value(item);
|
||||
}
|
||||
if(_PyErr_Occurred()){
|
||||
pythonexc2js();
|
||||
}
|
||||
_Py_DecRef(iterptr);
|
||||
}
|
||||
};
|
||||
|
||||
// Controlled by IS_ITERATOR, appears for any object with a __next__ or
|
||||
// tp_iternext method.
|
||||
Module.PyProxyIteratorMethods = {
|
||||
[Symbol.iterator] : function() {
|
||||
return this;
|
||||
},
|
||||
next : function(arg) {
|
||||
let idresult;
|
||||
// Note: arg is optional, if arg is not supplied, it will be undefined
|
||||
// which gets converted to "Py_None". This is as intended.
|
||||
let idarg = Module.hiwire.new_value(arg);
|
||||
try {
|
||||
idresult = __pyproxyGen_Send(_getPtr(this), idarg);
|
||||
} catch(e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idarg);
|
||||
}
|
||||
|
||||
let done = false;
|
||||
if(idresult === 0){
|
||||
idresult = __pyproxyGen_FetchStopIterationValue();
|
||||
if (idresult){
|
||||
done = true;
|
||||
} else {
|
||||
_pythonexc2js();
|
||||
}
|
||||
}
|
||||
let value = Module.hiwire.pop_value(idresult);
|
||||
return { done, value };
|
||||
},
|
||||
};
|
||||
|
||||
// Another layer of boilerplate. The PyProxyHandlers have some annoying logic
|
||||
// to deal with straining out the spurious "Function" properties "prototype",
|
||||
// "arguments", and "length", to deal with correctly satisfying the Proxy
|
||||
// invariants, and to deal with the mro
|
||||
function python_hasattr(jsobj, jskey){
|
||||
let ptrobj = _getPtr(jsobj);
|
||||
let idkey = Module.hiwire.new_value(jskey);
|
||||
let result;
|
||||
try {
|
||||
result = __pyproxy_hasattr(ptrobj, idkey);
|
||||
} catch(e){
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if(result === -1){
|
||||
_pythonexc2js();
|
||||
}
|
||||
return result !== 0;
|
||||
}
|
||||
|
||||
// Returns a JsRef in order to allow us to differentiate between "not found"
|
||||
// (in which case we return 0) and "found 'None'" (in which case we return
|
||||
// Js_undefined).
|
||||
function python_getattr(jsobj, jskey){
|
||||
let ptrobj = _getPtr(jsobj);
|
||||
let idkey = Module.hiwire.new_value(jskey);
|
||||
let idresult;
|
||||
try {
|
||||
idresult = __pyproxy_getattr(ptrobj, idkey);
|
||||
} catch(e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if(idresult === 0){
|
||||
if(_PyErr_Occurred()){
|
||||
_pythonexc2js();
|
||||
}
|
||||
}
|
||||
return idresult;
|
||||
}
|
||||
|
||||
function python_setattr(jsobj, jskey, jsval){
|
||||
let ptrobj = _getPtr(jsobj);
|
||||
let idkey = Module.hiwire.new_value(jskey);
|
||||
let idval = Module.hiwire.new_value(jsval);
|
||||
let errcode;
|
||||
try {
|
||||
errcode = __pyproxy_setattr(ptrobj, idkey, idval);
|
||||
} catch(e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
Module.hiwire.decref(idval);
|
||||
}
|
||||
if(errcode === -1){
|
||||
_pythonexc2js();
|
||||
}
|
||||
}
|
||||
|
||||
function python_delattr(jsobj, jskey){
|
||||
let ptrobj = _getPtr(jsobj);
|
||||
let idkey = Module.hiwire.new_value(jskey);
|
||||
let errcode;
|
||||
try {
|
||||
errcode = __pyproxy_delattr(ptrobj, idkey);
|
||||
} catch(e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if(errcode === -1){
|
||||
_pythonexc2js();
|
||||
}
|
||||
}
|
||||
|
||||
// See explanation of which methods should be defined here and what they do here:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
|
||||
Module.PyProxyHandlers = {
|
||||
isExtensible: function() { return true },
|
||||
has: function (jsobj, jskey) {
|
||||
// Note: must report "prototype" in proxy when we are callable.
|
||||
// (We can return the wrong value from "get" handler though.)
|
||||
let objHasKey = Reflect.has(jsobj, jskey);
|
||||
if(objHasKey){
|
||||
return true;
|
||||
}
|
||||
// python_hasattr will crash when given a Symbol.
|
||||
if(typeof(jskey) === "symbol"){
|
||||
return false;
|
||||
}
|
||||
return python_hasattr(jsobj, jskey);
|
||||
},
|
||||
get: function (jsobj, jskey) {
|
||||
// Preference order:
|
||||
// 1. things we have to return to avoid making Javascript angry
|
||||
// 2. the result of Python getattr
|
||||
// 3. stuff from the prototype chain
|
||||
|
||||
// 1. things we have to return to avoid making Javascript angry
|
||||
// This conditional looks funky but it's the only thing I found that
|
||||
// worked right in all cases.
|
||||
if((jskey in jsobj) && !(jskey in Object.getPrototypeOf(jsobj)) ){
|
||||
return Reflect.get(jsobj, jskey);
|
||||
}
|
||||
// python_getattr will crash when given a Symbol
|
||||
if(typeof(jskey) === "symbol"){
|
||||
return Reflect.get(jsobj, jskey);
|
||||
}
|
||||
// 2. The result of getattr
|
||||
let idresult = python_getattr(jsobj, jskey);
|
||||
if(idresult !== 0){
|
||||
return Module.hiwire.pop_value(idresult);
|
||||
}
|
||||
// 3. stuff from the prototype chain.
|
||||
return Reflect.get(jsobj, jskey);
|
||||
},
|
||||
set: function (jsobj, jskey, jsval) {
|
||||
// We're only willing to set properties on the python object, throw an
|
||||
// error if user tries to write over any key of type 1. things we have to
|
||||
// return to avoid making Javascript angry
|
||||
if(typeof(jskey) === "symbol"){
|
||||
throw new TypeError(`Cannot set read only field '${jskey.description}'`);
|
||||
}
|
||||
// Again this is a funny looking conditional, I found it as the result of
|
||||
// a lengthy search for something that worked right.
|
||||
let descr = Object.getOwnPropertyDescriptor(jsobj, jskey);
|
||||
if(descr && !descr.writable){
|
||||
throw new TypeError(`Cannot set read only field '${jskey}'`);
|
||||
}
|
||||
python_setattr(jsobj, jskey, jsval);
|
||||
return true;
|
||||
},
|
||||
deleteProperty: function (jsobj, jskey) {
|
||||
// We're only willing to delete properties on the python object, throw an
|
||||
// error if user tries to write over any key of type 1. things we have to
|
||||
// return to avoid making Javascript angry
|
||||
if(typeof(jskey) === "symbol"){
|
||||
throw new TypeError(`Cannot delete read only field '${jskey.description}'`);
|
||||
}
|
||||
let descr = Object.getOwnPropertyDescriptor(jsobj, jskey);
|
||||
if(descr && !descr.writable){
|
||||
throw new TypeError(`Cannot delete read only field '${jskey}'`);
|
||||
}
|
||||
python_delattr(jsobj, jskey);
|
||||
// Must return "false" if "jskey" is a nonconfigurable own property.
|
||||
// Otherwise Javascript will throw a TypeError.
|
||||
return !descr || descr.configurable;
|
||||
},
|
||||
ownKeys: function (jsobj) {
|
||||
let ptrobj = _getPtr(jsobj);
|
||||
let idresult;
|
||||
try {
|
||||
idresult = __pyproxy_ownKeys(ptrobj);
|
||||
} catch(e) {
|
||||
Module.fatal_error(e);
|
||||
}
|
||||
let result = Module.hiwire.pop_value(idresult);
|
||||
result.push(...Reflect.ownKeys(jsobj));
|
||||
return result;
|
||||
},
|
||||
apply: function (jsobj, jsthis, jsargs) {
|
||||
return jsobj.apply(jsthis, jsargs);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The Promise / javascript awaitable API.
|
||||
*/
|
||||
Module.PyProxyAwaitableMethods = {
|
||||
/**
|
||||
* This wraps __pyproxy_ensure_future and makes a function that converts a
|
||||
* Python awaitable to a promise, scheduling the awaitable on the Python
|
||||
* event loop if necessary.
|
||||
*/
|
||||
_ensure_future : function(){
|
||||
let resolve_handle_id = 0;
|
||||
let reject_handle_id = 0;
|
||||
let resolveHandle;
|
||||
let rejectHandle;
|
||||
let promise;
|
||||
try {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
resolveHandle = resolve;
|
||||
rejectHandle = reject;
|
||||
});
|
||||
resolve_handle_id = Module.hiwire.new_value(resolveHandle);
|
||||
reject_handle_id = Module.hiwire.new_value(rejectHandle);
|
||||
let ptrobj = _getPtr(this);
|
||||
let errcode = __pyproxy_ensure_future(ptrobj, resolve_handle_id, reject_handle_id);
|
||||
if(errcode === -1){
|
||||
_pythonexc2js();
|
||||
}
|
||||
} finally {
|
||||
Module.hiwire.decref(resolve_handle_id);
|
||||
Module.hiwire.decref(reject_handle_id);
|
||||
}
|
||||
return promise;
|
||||
},
|
||||
then : function(onFulfilled, onRejected){
|
||||
let promise = this._ensure_future();
|
||||
return promise.then(onFulfilled, onRejected);
|
||||
},
|
||||
catch : function(onRejected){
|
||||
let promise = this._ensure_future();
|
||||
return promise.catch(onRejected);
|
||||
},
|
||||
finally : function(onFinally){
|
||||
let promise = this._ensure_future();
|
||||
return promise.finally(onFinally);
|
||||
}
|
||||
};
|
||||
|
||||
Module.PyProxyCallableMethods = { prototype : Function.prototype };
|
||||
Module.PyProxyBufferMethods = {};
|
||||
|
||||
// A special proxy that we use to wrap pyodide.globals to allow property access
|
||||
// like `pyodide.globals.x`.
|
||||
let globalsPropertyAccessWarned = false;
|
||||
let globalsPropertyAccessWarningMsg =
|
||||
"Access to pyodide.globals via pyodide.globals.key is deprecated and " +
|
||||
"will be removed in version 0.18.0. Use pyodide.globals.get('key'), " +
|
||||
"pyodide.globals.set('key', value), pyodide.globals.delete('key') instead.";
|
||||
let NamespaceProxyHandlers = {
|
||||
has : function(obj, key){
|
||||
return Reflect.has(obj, key) || obj.has(key);
|
||||
},
|
||||
get : function(obj, key){
|
||||
if(Reflect.has(obj, key)){
|
||||
return Reflect.get(obj, key);
|
||||
}
|
||||
let result = obj.get(key);
|
||||
if(!globalsPropertyAccessWarned && result !== undefined){
|
||||
console.warn(globalsPropertyAccessWarningMsg);
|
||||
globalsPropertyAccessWarned = true;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
set : function(obj, key, value){
|
||||
if(Reflect.has(obj, key)){
|
||||
throw new Error(`Cannot set read only field ${key}`);
|
||||
}
|
||||
if(!globalsPropertyAccessWarned){
|
||||
globalsPropertyAccessWarned = true;
|
||||
console.warn(globalsPropertyAccessWarningMsg);
|
||||
}
|
||||
obj.set(key, value);
|
||||
},
|
||||
ownKeys: function (obj) {
|
||||
let result = new Set(Reflect.ownKeys(obj));
|
||||
let iter = obj.keys();
|
||||
for(let key of iter){
|
||||
result.add(key);
|
||||
}
|
||||
iter.destroy();
|
||||
return Array.from(result);
|
||||
}
|
||||
};
|
||||
|
||||
Module.wrapNamespace = function wrapNamespace(ns){
|
||||
return new Proxy(ns, NamespaceProxyHandlers);
|
||||
};
|
||||
return 0;
|
||||
});
|
||||
// clang-format on
|
||||
|
||||
EM_JS_REF(JsRef, create_once_callable, (PyObject * obj), {
|
||||
|
@ -1348,6 +800,19 @@ static PyMethodDef pyproxy_methods[] = {
|
|||
{ NULL } /* Sentinel */
|
||||
};
|
||||
|
||||
// Some special helper macros to hack it so that "pyproxy.js" parses as a
|
||||
// javascript file for JsDoc. See comment with explanation there.
|
||||
#define UNPAIRED_OPEN_BRACE {
|
||||
#define UNPAIRED_CLOSE_BRACE } // Just here to help text editors pair braces up
|
||||
#define TEMP_EMJS_HELPER(a, args...) \
|
||||
EM_JS_NUM(int, pyproxy_init_js, (), UNPAIRED_OPEN_BRACE { args return 0; })
|
||||
|
||||
#include "pyproxy.js"
|
||||
|
||||
#undef TEMP_EMJS_HELPER
|
||||
#undef UNPAIRED_OPEN_BRACE
|
||||
#undef UNPAIRED_CLOSE_BRACE
|
||||
|
||||
int
|
||||
pyproxy_init(PyObject* core)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,567 @@
|
|||
// This file to be included from pyproxy.c
|
||||
//
|
||||
// The point is to make a file that works with JsDoc. JsDoc will give up if it
|
||||
// fails to parse the file as javascript. Thus, it's key that this file should
|
||||
// parse as valid javascript. `TEMP_EMJS_HELPER` is a specially designed macro
|
||||
// to allow us to do this. We need TEMP_EMJS_HELPER to parse like a javascript
|
||||
// function call. The easiest way to get it to parse is to make the "argument"
|
||||
// look like a function call, which we do with `()=>{`. However, `()=>{` is an
|
||||
// invalid C string so the macro needs to remove it. We put `()=>{0,`,
|
||||
// TEMP_EMJS_HELPER removes everything up to the comma and replace it with a
|
||||
// single open brace.
|
||||
//
|
||||
// See definition of TEMP_EMJS_HELPER:
|
||||
// #define TEMP_EMJS_HELPER(a, args...) \
|
||||
// EM_JS(int, pyproxy_init, (), UNPAIRED_OPEN_BRACE { args return 0; })
|
||||
|
||||
// clang-format off
|
||||
TEMP_EMJS_HELPER(() => {0, /* Magic, see comment */
|
||||
Module.PyProxies = {};
|
||||
// clang-format on
|
||||
|
||||
function _getPtr(jsobj) {
|
||||
let ptr = jsobj.$$.ptr;
|
||||
if (ptr === null) {
|
||||
throw new Error("Object has already been destroyed");
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let _pyproxyClassMap = new Map();
|
||||
/**
|
||||
* Retreive the appropriate mixins based on the features requested in flags.
|
||||
* Used by pyproxy_new. The "flags" variable is produced by the C function
|
||||
* pyproxy_getflags. Multiple PyProxies with the same set of feature flags
|
||||
* will share the same prototype, so the memory footprint of each individual
|
||||
* PyProxy is minimal.
|
||||
*/
|
||||
Module.getPyProxyClass = function(flags) {
|
||||
let result = _pyproxyClassMap.get(flags);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
let descriptors = {};
|
||||
// clang-format off
|
||||
for(let [feature_flag, methods] of [
|
||||
[HAS_LENGTH, Module.PyProxyLengthMethods],
|
||||
[HAS_GET, Module.PyProxyGetItemMethods],
|
||||
[HAS_SET, Module.PyProxySetItemMethods],
|
||||
[HAS_CONTAINS, Module.PyProxyContainsMethods],
|
||||
[IS_ITERABLE, Module.PyProxyIterableMethods],
|
||||
[IS_ITERATOR, Module.PyProxyIteratorMethods],
|
||||
[IS_AWAITABLE, Module.PyProxyAwaitableMethods],
|
||||
[IS_BUFFER, Module.PyProxyBufferMethods],
|
||||
[IS_CALLABLE, Module.PyProxyCallableMethods],
|
||||
]){
|
||||
// clang-format on
|
||||
if (flags & feature_flag) {
|
||||
Object.assign(descriptors, Object.getOwnPropertyDescriptors(methods));
|
||||
}
|
||||
}
|
||||
let new_proto = Object.create(Module.PyProxyClass.prototype, descriptors);
|
||||
function PyProxy() {};
|
||||
PyProxy.prototype = new_proto;
|
||||
_pyproxyClassMap.set(flags, PyProxy);
|
||||
return PyProxy;
|
||||
};
|
||||
|
||||
// Static methods
|
||||
Module.PyProxy = {
|
||||
_getPtr,
|
||||
isPyProxy : function(jsobj) {
|
||||
return jsobj && jsobj.$$ !== undefined && jsobj.$$.type === 'PyProxy';
|
||||
},
|
||||
};
|
||||
|
||||
Module.callPyObject = function(ptrobj, ...jsargs) {
|
||||
let idargs = Module.hiwire.new_value(jsargs);
|
||||
let idresult;
|
||||
try {
|
||||
idresult = __pyproxy_apply(ptrobj, idargs);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idargs);
|
||||
}
|
||||
if (idresult === 0) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
return Module.hiwire.pop_value(idresult);
|
||||
};
|
||||
|
||||
// Now a lot of boilerplate to wrap the abstract Object protocol wrappers
|
||||
// above in Javascript functions.
|
||||
|
||||
Module.PyProxyClass = class {
|
||||
constructor() { throw new TypeError('PyProxy is not a constructor'); }
|
||||
|
||||
get[Symbol.toStringTag]() { return "PyProxy"; }
|
||||
get type() {
|
||||
let ptrobj = _getPtr(this);
|
||||
return Module.hiwire.pop_value(__pyproxy_type(ptrobj));
|
||||
}
|
||||
toString() {
|
||||
let ptrobj = _getPtr(this);
|
||||
let jsref_repr;
|
||||
try {
|
||||
jsref_repr = __pyproxy_repr(ptrobj);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
}
|
||||
if (jsref_repr === 0) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
return Module.hiwire.pop_value(jsref_repr);
|
||||
}
|
||||
destroy() {
|
||||
let ptrobj = _getPtr(this);
|
||||
__pyproxy_destroy(ptrobj);
|
||||
this.$$.ptr = null;
|
||||
}
|
||||
/**
|
||||
* This one doesn't follow the pattern: the inner function
|
||||
* _python2js_with_depth is defined in python2js.c and is not a Python
|
||||
* Object Protocol wrapper.
|
||||
*/
|
||||
toJs(depth = -1) {
|
||||
let idresult = _python2js_with_depth(_getPtr(this), depth);
|
||||
let result = Module.hiwire.get_value(idresult);
|
||||
Module.hiwire.decref(idresult);
|
||||
return result;
|
||||
}
|
||||
apply(jsthis, jsargs) {
|
||||
return Module.callPyObject(_getPtr(this), ...jsargs);
|
||||
}
|
||||
call(jsthis, ...jsargs) {
|
||||
return Module.callPyObject(_getPtr(this), ...jsargs);
|
||||
}
|
||||
};
|
||||
|
||||
// Controlled by HAS_LENGTH, appears for any object with __len__ or sq_length
|
||||
// or mp_length methods
|
||||
Module.PyProxyLengthMethods = {
|
||||
get length() {
|
||||
let ptrobj = _getPtr(this);
|
||||
let length;
|
||||
try {
|
||||
length = _PyObject_Size(ptrobj);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
}
|
||||
if (length === -1) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
return length;
|
||||
}
|
||||
};
|
||||
|
||||
// Controlled by HAS_GET, appears for any class with __getitem__,
|
||||
// mp_subscript, or sq_item methods
|
||||
Module.PyProxyGetItemMethods = {
|
||||
get : function(key) {
|
||||
let ptrobj = _getPtr(this);
|
||||
let idkey = Module.hiwire.new_value(key);
|
||||
let idresult;
|
||||
try {
|
||||
idresult = __pyproxy_getitem(ptrobj, idkey);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if (idresult === 0) {
|
||||
if (Module._PyErr_Occurred()) {
|
||||
_pythonexc2js();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return Module.hiwire.pop_value(idresult);
|
||||
},
|
||||
};
|
||||
|
||||
// Controlled by HAS_SET, appears for any class with __setitem__, __delitem__,
|
||||
// mp_ass_subscript, or sq_ass_item.
|
||||
Module.PyProxySetItemMethods = {
|
||||
set : function(key, value) {
|
||||
let ptrobj = _getPtr(this);
|
||||
let idkey = Module.hiwire.new_value(key);
|
||||
let idval = Module.hiwire.new_value(value);
|
||||
let errcode;
|
||||
try {
|
||||
errcode = __pyproxy_setitem(ptrobj, idkey, idval);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
Module.hiwire.decref(idval);
|
||||
}
|
||||
if (errcode === -1) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
},
|
||||
delete : function(key) {
|
||||
let ptrobj = _getPtr(this);
|
||||
let idkey = Module.hiwire.new_value(key);
|
||||
let errcode;
|
||||
try {
|
||||
errcode = __pyproxy_delitem(ptrobj, idkey);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if (errcode === -1) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Controlled by HAS_CONTAINS flag, appears for any class with __contains__ or
|
||||
// sq_contains
|
||||
Module.PyProxyContainsMethods = {
|
||||
has : function(key) {
|
||||
let ptrobj = _getPtr(this);
|
||||
let idkey = Module.hiwire.new_value(key);
|
||||
let result;
|
||||
try {
|
||||
result = __pyproxy_contains(ptrobj, idkey);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if (result === -1) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
return result === 1;
|
||||
},
|
||||
};
|
||||
|
||||
// Controlled by IS_ITERABLE, appears for any object with __iter__ or tp_iter,
|
||||
// unless they are iterators. See: https://docs.python.org/3/c-api/iter.html
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
|
||||
// This avoids allocating a PyProxy wrapper for the temporary iterator.
|
||||
Module.PyProxyIterableMethods = {
|
||||
[Symbol.iterator] : function*() {
|
||||
let iterptr = _PyObject_GetIter(_getPtr(this));
|
||||
if (iterptr === 0) {
|
||||
pythonexc2js();
|
||||
}
|
||||
let item;
|
||||
while ((item = __pyproxy_iter_next(iterptr))) {
|
||||
yield Module.hiwire.pop_value(item);
|
||||
}
|
||||
if (_PyErr_Occurred()) {
|
||||
pythonexc2js();
|
||||
}
|
||||
_Py_DecRef(iterptr);
|
||||
}
|
||||
};
|
||||
|
||||
// Controlled by IS_ITERATOR, appears for any object with a __next__ or
|
||||
// tp_iternext method.
|
||||
Module.PyProxyIteratorMethods = {
|
||||
[Symbol.iterator] : function() { return this; },
|
||||
next : function(arg) {
|
||||
let idresult;
|
||||
// Note: arg is optional, if arg is not supplied, it will be undefined
|
||||
// which gets converted to "Py_None". This is as intended.
|
||||
let idarg = Module.hiwire.new_value(arg);
|
||||
try {
|
||||
idresult = __pyproxyGen_Send(_getPtr(this), idarg);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idarg);
|
||||
}
|
||||
|
||||
let done = false;
|
||||
if (idresult === 0) {
|
||||
idresult = __pyproxyGen_FetchStopIterationValue();
|
||||
if (idresult) {
|
||||
done = true;
|
||||
} else {
|
||||
_pythonexc2js();
|
||||
}
|
||||
}
|
||||
let value = Module.hiwire.pop_value(idresult);
|
||||
return {done, value};
|
||||
},
|
||||
};
|
||||
|
||||
// Another layer of boilerplate. The PyProxyHandlers have some annoying logic
|
||||
// to deal with straining out the spurious "Function" properties "prototype",
|
||||
// "arguments", and "length", to deal with correctly satisfying the Proxy
|
||||
// invariants, and to deal with the mro
|
||||
function python_hasattr(jsobj, jskey) {
|
||||
let ptrobj = _getPtr(jsobj);
|
||||
let idkey = Module.hiwire.new_value(jskey);
|
||||
let result;
|
||||
try {
|
||||
result = __pyproxy_hasattr(ptrobj, idkey);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if (result === -1) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
return result !== 0;
|
||||
}
|
||||
|
||||
// Returns a JsRef in order to allow us to differentiate between "not found"
|
||||
// (in which case we return 0) and "found 'None'" (in which case we return
|
||||
// Js_undefined).
|
||||
function python_getattr(jsobj, jskey) {
|
||||
let ptrobj = _getPtr(jsobj);
|
||||
let idkey = Module.hiwire.new_value(jskey);
|
||||
let idresult;
|
||||
try {
|
||||
idresult = __pyproxy_getattr(ptrobj, idkey);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if (idresult === 0) {
|
||||
if (_PyErr_Occurred()) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
}
|
||||
return idresult;
|
||||
}
|
||||
|
||||
function python_setattr(jsobj, jskey, jsval) {
|
||||
let ptrobj = _getPtr(jsobj);
|
||||
let idkey = Module.hiwire.new_value(jskey);
|
||||
let idval = Module.hiwire.new_value(jsval);
|
||||
let errcode;
|
||||
try {
|
||||
errcode = __pyproxy_setattr(ptrobj, idkey, idval);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
Module.hiwire.decref(idval);
|
||||
}
|
||||
if (errcode === -1) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
}
|
||||
|
||||
function python_delattr(jsobj, jskey) {
|
||||
let ptrobj = _getPtr(jsobj);
|
||||
let idkey = Module.hiwire.new_value(jskey);
|
||||
let errcode;
|
||||
try {
|
||||
errcode = __pyproxy_delattr(ptrobj, idkey);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
} finally {
|
||||
Module.hiwire.decref(idkey);
|
||||
}
|
||||
if (errcode === -1) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
}
|
||||
|
||||
// See explanation of which methods should be defined here and what they do
|
||||
// here:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
|
||||
Module.PyProxyHandlers = {
|
||||
isExtensible : function() { return true },
|
||||
has : function(jsobj, jskey) {
|
||||
// Note: must report "prototype" in proxy when we are callable.
|
||||
// (We can return the wrong value from "get" handler though.)
|
||||
let objHasKey = Reflect.has(jsobj, jskey);
|
||||
if (objHasKey) {
|
||||
return true;
|
||||
}
|
||||
// python_hasattr will crash when given a Symbol.
|
||||
if (typeof (jskey) === "symbol") {
|
||||
return false;
|
||||
}
|
||||
return python_hasattr(jsobj, jskey);
|
||||
},
|
||||
get : function(jsobj, jskey) {
|
||||
// Preference order:
|
||||
// 1. things we have to return to avoid making Javascript angry
|
||||
// 2. the result of Python getattr
|
||||
// 3. stuff from the prototype chain
|
||||
|
||||
// 1. things we have to return to avoid making Javascript angry
|
||||
// This conditional looks funky but it's the only thing I found that
|
||||
// worked right in all cases.
|
||||
if ((jskey in jsobj) && !(jskey in Object.getPrototypeOf(jsobj))) {
|
||||
return Reflect.get(jsobj, jskey);
|
||||
}
|
||||
// python_getattr will crash when given a Symbol
|
||||
if (typeof (jskey) === "symbol") {
|
||||
return Reflect.get(jsobj, jskey);
|
||||
}
|
||||
// 2. The result of getattr
|
||||
let idresult = python_getattr(jsobj, jskey);
|
||||
if (idresult !== 0) {
|
||||
return Module.hiwire.pop_value(idresult);
|
||||
}
|
||||
// 3. stuff from the prototype chain.
|
||||
return Reflect.get(jsobj, jskey);
|
||||
},
|
||||
set : function(jsobj, jskey, jsval) {
|
||||
// We're only willing to set properties on the python object, throw an
|
||||
// error if user tries to write over any key of type 1. things we have to
|
||||
// return to avoid making Javascript angry
|
||||
if (typeof (jskey) === "symbol") {
|
||||
throw new TypeError(
|
||||
`Cannot set read only field '${jskey.description}'`);
|
||||
}
|
||||
// Again this is a funny looking conditional, I found it as the result of
|
||||
// a lengthy search for something that worked right.
|
||||
let descr = Object.getOwnPropertyDescriptor(jsobj, jskey);
|
||||
if (descr && !descr.writable) {
|
||||
throw new TypeError(`Cannot set read only field '${jskey}'`);
|
||||
}
|
||||
python_setattr(jsobj, jskey, jsval);
|
||||
return true;
|
||||
},
|
||||
deleteProperty : function(jsobj, jskey) {
|
||||
// We're only willing to delete properties on the python object, throw an
|
||||
// error if user tries to write over any key of type 1. things we have to
|
||||
// return to avoid making Javascript angry
|
||||
if (typeof (jskey) === "symbol") {
|
||||
throw new TypeError(
|
||||
`Cannot delete read only field '${jskey.description}'`);
|
||||
}
|
||||
let descr = Object.getOwnPropertyDescriptor(jsobj, jskey);
|
||||
if (descr && !descr.writable) {
|
||||
throw new TypeError(`Cannot delete read only field '${jskey}'`);
|
||||
}
|
||||
python_delattr(jsobj, jskey);
|
||||
// Must return "false" if "jskey" is a nonconfigurable own property.
|
||||
// Otherwise Javascript will throw a TypeError.
|
||||
return !descr || descr.configurable;
|
||||
},
|
||||
ownKeys : function(jsobj) {
|
||||
let ptrobj = _getPtr(jsobj);
|
||||
let idresult;
|
||||
try {
|
||||
idresult = __pyproxy_ownKeys(ptrobj);
|
||||
} catch (e) {
|
||||
Module.fatal_error(e);
|
||||
}
|
||||
let result = Module.hiwire.pop_value(idresult);
|
||||
result.push(...Reflect.ownKeys(jsobj));
|
||||
return result;
|
||||
},
|
||||
// clang-format off
|
||||
apply : function(jsobj, jsthis, jsargs) {
|
||||
return jsobj.apply(jsthis, jsargs);
|
||||
},
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
/**
|
||||
* The Promise / javascript awaitable API.
|
||||
*/
|
||||
Module.PyProxyAwaitableMethods = {
|
||||
/**
|
||||
* This wraps __pyproxy_ensure_future and makes a function that converts a
|
||||
* Python awaitable to a promise, scheduling the awaitable on the Python
|
||||
* event loop if necessary.
|
||||
*/
|
||||
_ensure_future : function() {
|
||||
let resolve_handle_id = 0;
|
||||
let reject_handle_id = 0;
|
||||
let resolveHandle;
|
||||
let rejectHandle;
|
||||
let promise;
|
||||
try {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
resolveHandle = resolve;
|
||||
rejectHandle = reject;
|
||||
});
|
||||
resolve_handle_id = Module.hiwire.new_value(resolveHandle);
|
||||
reject_handle_id = Module.hiwire.new_value(rejectHandle);
|
||||
let ptrobj = _getPtr(this);
|
||||
let errcode = __pyproxy_ensure_future(ptrobj, resolve_handle_id,
|
||||
reject_handle_id);
|
||||
if (errcode === -1) {
|
||||
_pythonexc2js();
|
||||
}
|
||||
} finally {
|
||||
Module.hiwire.decref(resolve_handle_id);
|
||||
Module.hiwire.decref(reject_handle_id);
|
||||
}
|
||||
return promise;
|
||||
},
|
||||
then : function(onFulfilled, onRejected) {
|
||||
let promise = this._ensure_future();
|
||||
return promise.then(onFulfilled, onRejected);
|
||||
},
|
||||
catch : function(onRejected) {
|
||||
let promise = this._ensure_future();
|
||||
return promise.catch(onRejected);
|
||||
},
|
||||
finally : function(onFinally) {
|
||||
let promise = this._ensure_future();
|
||||
return promise.finally(onFinally);
|
||||
}
|
||||
};
|
||||
|
||||
Module.PyProxyCallableMethods = {prototype : Function.prototype};
|
||||
Module.PyProxyBufferMethods = {};
|
||||
|
||||
// A special proxy that we use to wrap pyodide.globals to allow property
|
||||
// access like `pyodide.globals.x`.
|
||||
let globalsPropertyAccessWarned = false;
|
||||
let globalsPropertyAccessWarningMsg =
|
||||
"Access to pyodide.globals via pyodide.globals.key is deprecated and " +
|
||||
"will be removed in version 0.18.0. Use pyodide.globals.get('key'), " +
|
||||
"pyodide.globals.set('key', value), pyodide.globals.delete('key') instead.";
|
||||
let NamespaceProxyHandlers = {
|
||||
// clang-format off
|
||||
has : function(obj, key) {
|
||||
return Reflect.has(obj, key) || obj.has(key);
|
||||
},
|
||||
// clang-format on
|
||||
get : function(obj, key) {
|
||||
if (Reflect.has(obj, key)) {
|
||||
return Reflect.get(obj, key);
|
||||
}
|
||||
let result = obj.get(key);
|
||||
if (!globalsPropertyAccessWarned && result !== undefined) {
|
||||
console.warn(globalsPropertyAccessWarningMsg);
|
||||
globalsPropertyAccessWarned = true;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
set : function(obj, key, value) {
|
||||
if (Reflect.has(obj, key)) {
|
||||
throw new Error(`Cannot set read only field ${key}`);
|
||||
}
|
||||
if (!globalsPropertyAccessWarned) {
|
||||
globalsPropertyAccessWarned = true;
|
||||
console.warn(globalsPropertyAccessWarningMsg);
|
||||
}
|
||||
obj.set(key, value);
|
||||
},
|
||||
ownKeys : function(obj) {
|
||||
let result = new Set(Reflect.ownKeys(obj));
|
||||
let iter = obj.keys();
|
||||
for (let key of iter) {
|
||||
result.add(key);
|
||||
}
|
||||
iter.destroy();
|
||||
return Array.from(result);
|
||||
}
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
Module.wrapNamespace = function wrapNamespace(ns) {
|
||||
return new Proxy(ns, NamespaceProxyHandlers);
|
||||
};
|
||||
// clang-format on
|
||||
return 0;
|
||||
});
|
Loading…
Reference in New Issue