ENH Pyproxy mixins (#1264)

* Set up mixins in pyproxy.c

* Add a bunch of documentation comments to pyproxy

* PyProxy should only subclass Function when the Python object is Callable

* Don't leak name, length, or prototype. Add tests

* More tests, some final tuneups, more comments

* Remove tests that show different behavior between Firefox and Chrome

* Reduce repitition in getPyProxyClass

* Update comment
This commit is contained in:
Hood Chatham 2021-03-05 08:51:44 -08:00 committed by GitHub
parent 529caac8ef
commit 40b63d65d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 676 additions and 235 deletions

View File

@ -13,6 +13,136 @@ _Py_IDENTIFIER(add_done_callback);
static PyObject* asyncio;
// Flags controlling presence or absence of many small mixins depending on which
// abstract protocols the Python object supports.
// clang-format off
#define HAS_LENGTH (1 << 0)
#define HAS_GET (1 << 1)
#define HAS_SET (1 << 2)
#define HAS_CONTAINS (1 << 3)
#define IS_ITERABLE (1 << 4)
#define IS_ITERATOR (1 << 5)
#define IS_AWAITABLE (1 << 6)
#define IS_BUFFER (1 << 7)
#define IS_CALLABLE (1 << 8)
// clang-format on
// Taken from genobject.c
// For checking whether an object is awaitable.
static int
gen_is_coroutine(PyObject* o)
{
if (PyGen_CheckExact(o)) {
PyCodeObject* code = (PyCodeObject*)((PyGenObject*)o)->gi_code;
if (code->co_flags & CO_ITERABLE_COROUTINE) {
return 1;
}
}
return 0;
}
/**
* Do introspection on the python object to work out which abstract protocols it
* supports. Most of these tests are taken from a corresponding abstract Object
* protocol API defined in `abstract.c`. We wrote these tests to check whether
* the corresponding CPython APIs are likely to work without actually creating
* any temporary objects.
*/
int
pyproxy_getflags(PyObject* pyobj)
{
// Reduce casework by ensuring that protos aren't NULL.
PyTypeObject* obj_type = pyobj->ob_type;
PySequenceMethods null_seq_proto = { 0 };
PySequenceMethods* seq_proto =
obj_type->tp_as_sequence ? obj_type->tp_as_sequence : &null_seq_proto;
PyMappingMethods null_map_proto = { 0 };
PyMappingMethods* map_proto =
obj_type->tp_as_mapping ? obj_type->tp_as_mapping : &null_map_proto;
PyAsyncMethods null_async_proto = { 0 };
PyAsyncMethods* async_proto =
obj_type->tp_as_async ? obj_type->tp_as_async : &null_async_proto;
PyBufferProcs null_buffer_proto = { 0 };
PyBufferProcs* buffer_proto =
obj_type->tp_as_buffer ? obj_type->tp_as_buffer : &null_buffer_proto;
int result = 0;
// PyObject_Size
if (seq_proto->sq_length || map_proto->mp_length) {
result |= HAS_LENGTH;
}
// PyObject_GetItem
if (map_proto->mp_subscript || seq_proto->sq_item) {
result |= HAS_GET;
} else if (PyType_Check(pyobj)) {
_Py_IDENTIFIER(__class_getitem__);
if (_PyObject_HasAttrId(pyobj, &PyId___class_getitem__)) {
result |= HAS_GET;
}
}
// PyObject_SetItem
if (map_proto->mp_ass_subscript || seq_proto->sq_ass_item) {
result |= HAS_SET;
}
// PySequence_Contains
if (seq_proto->sq_contains) {
result |= HAS_CONTAINS;
}
// PyObject_GetIter
if (obj_type->tp_iter || PySequence_Check(pyobj)) {
result |= IS_ITERABLE;
}
if (PyIter_Check(pyobj)) {
result &= ~IS_ITERABLE;
result |= IS_ITERATOR;
}
// There's no CPython API that corresponds directly to the "await" keyword.
// Looking at disassembly, "await" translates into opcodes GET_AWAITABLE and
// YIELD_FROM. GET_AWAITABLE uses _PyCoro_GetAwaitableIter defined in
// genobject.c. This tests whether _PyCoro_GetAwaitableIter is likely to
// succeed.
if (async_proto->am_await || gen_is_coroutine(pyobj)) {
result |= IS_AWAITABLE;
}
if (buffer_proto->bf_getbuffer) {
result |= IS_BUFFER;
}
// PyObject_Call (from call.c)
if (_PyVectorcall_Function(pyobj) || PyCFunction_Check(pyobj) ||
obj_type->tp_call) {
result |= IS_CALLABLE;
}
return result;
}
///////////////////////////////////////////////////////////////////////////////
//
// Object protocol wrappers
//
// This section defines wrappers for Python Object protocol API calls that we
// are planning to offer on the PyProxy. Much of this could be written in
// Javascript instead. Some reasons to do it in C:
// 1. Some CPython APIs are actually secretly macros which cannot be used from
// Javascript.
// 2. The code is a bit more concise in C.
// 3. It may be preferable to minimize the number of times we cross between
// wasm and javascript for performance reasons
// 4. Better separation of functionality: Most of the Javascript code is
// boilerpalte. Most of this code here is boilerplate. However, the
// boilerplate in these C API wwrappers is a bit different than the
// boilerplate in the javascript wrappers, so we've factored it into two
// distinct layers of boilerplate.
//
// Item 1 makes it technically necessary to use these wrappers once in a while.
// I think all of these advantages outweigh the cost of splitting up the
// implementation of each feature like this, especially because most of the
// logic is very boilerplatey, so there isn't much surprising code hidden
// somewhere else.
JsRef
_pyproxy_repr(PyObject* pyobj)
{
@ -23,6 +153,32 @@ _pyproxy_repr(PyObject* pyobj)
return repr_js;
}
/**
* Wrapper for the "proxy.type" getter, which behaves a little bit like
* `type(obj)`, but instead of returning the class we just return the name of
* the class. The exact behavior is that this usually gives "module.name" but
* for builtins just gives "name". So in particular, usually it is equivalent
* to:
*
* `type(x).__module__ + "." + type(x).__name__`
*
* But other times it behaves like:
*
* `type(x).__name__`
*/
JsRef
_pyproxy_type(PyObject* ptrobj)
{
return hiwire_string_ascii(ptrobj->ob_type->tp_name);
}
void
_pyproxy_destroy(PyObject* ptrobj)
{ // See bug #1049
Py_DECREF(ptrobj);
EM_ASM({ delete Module.PyProxies[$0]; }, ptrobj);
}
int
_pyproxy_hasattr(PyObject* pyobj, JsRef idkey)
{
@ -60,6 +216,9 @@ finally:
Py_CLEAR(pykey);
Py_CLEAR(pyresult);
if (!success) {
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
}
hiwire_CLEAR(idresult);
}
return idresult;
@ -163,6 +322,21 @@ finally:
return success ? 0 : -1;
}
int
_pyproxy_contains(PyObject* pyobj, JsRef idkey)
{
PyObject* pykey = NULL;
int result = -1;
pykey = js2python(idkey);
FAIL_IF_NULL(pykey);
result = PySequence_Contains(pyobj, pykey);
finally:
Py_CLEAR(pykey);
return result;
}
JsRef
_pyproxy_ownKeys(PyObject* pyobj)
{
@ -207,22 +381,6 @@ _pyproxy_apply(PyObject* pyobj, JsRef idargs)
return idresult;
}
// Return 2 if obj is iterator
// Return 1 if iterable but not iterator
// Return 0 if not iterable
int
_pyproxy_iterator_type(PyObject* obj)
{
if (PyIter_Check(obj)) {
return 2;
}
PyObject* iter = PyObject_GetIter(obj);
int result = iter != NULL;
Py_CLEAR(iter);
PyErr_Clear();
return result;
}
JsRef
_pyproxy_iter_next(PyObject* iterator)
{
@ -235,15 +393,23 @@ _pyproxy_iter_next(PyObject* iterator)
return result;
}
/**
* In Python 3.10, they have added the PyIter_Send API (and removed _PyGen_Send)
* so in v3.10 this would be a simple API call wrapper like the rest of the code
* here. For now, we're just copying the YIELD_FROM opcode (see ceval.c).
*
* When the iterator is done, it returns NULL and sets StopIteration. We'll use
* _pyproxyGen_FetchStopIterationValue below to get the return value of the
* generator (again copying from YIELD_FROM).
*/
JsRef
_pyproxy_iter_send(PyObject* receiver, JsRef jsval)
_pyproxyGen_Send(PyObject* receiver, JsRef jsval)
{
bool success = false;
PyObject* v = NULL;
PyObject* retval = NULL;
JsRef jsresult = NULL;
// cf implementation of YIELD_FROM opcode in ceval.c
v = js2python(jsval);
FAIL_IF_NULL(v);
if (PyGen_CheckExact(receiver) || PyCoro_CheckExact(receiver)) {
@ -269,8 +435,12 @@ finally:
return jsresult;
}
/**
* If StopIteration was set, return the value it was set with. Otherwise, return
* NULL.
*/
JsRef
_pyproxy_iter_fetch_stopiteration()
_pyproxyGen_FetchStopIterationValue()
{
PyObject* val = NULL;
// cf implementation of YIELD_FROM opcode in ceval.c
@ -285,44 +455,24 @@ _pyproxy_iter_fetch_stopiteration()
return result;
}
JsRef
_pyproxy_type(PyObject* ptrobj)
{
return hiwire_string_ascii(ptrobj->ob_type->tp_name);
}
///////////////////////////////////////////////////////////////////////////////
//
// Await / "then" Implementation
//
// We want convert the object to a future with `ensure_future` and then make a
// promise that resolves when the future does. We can add a callback to the
// future with future.add_done_callback but we need to make a little python
// closure "FutureDoneCallback" that remembers how to resolve the promise.
//
// From Javascript we will use the single function _pyproxy_ensure_future, the
// rest of this segment is helper functions for _pyproxy_ensure_future. The
// FutureDoneCallback object is never exposed to the user.
void
_pyproxy_destroy(PyObject* ptrobj)
{ // See bug #1049
Py_DECREF(ptrobj);
EM_ASM({ delete Module.PyProxies[$0]; }, ptrobj);
}
/**
* Test if a PyObject is awaitable.
* Uses _PyCoro_GetAwaitableIter like in the implementation of the GET_AWAITABLE
* opcode (see ceval.c). Unfortunately this is not a public API (see issue
* https://bugs.python.org/issue24510) so it could be a source of instability.
*
* :param pyobject: The Python object.
* :return: 1 if the python code "await obj" would succeed, 0 otherwise. Never
* fails.
*/
bool
_pyproxy_is_awaitable(PyObject* pyobject)
{
PyObject* awaitable = _PyCoro_GetAwaitableIter(pyobject);
PyErr_Clear();
bool result = awaitable != NULL;
Py_CLEAR(awaitable);
return result;
}
// clang-format off
/**
* A simple Callable python object. Intended to be called with a single argument
* which is the future that was resolved.
*/
// clang-format off
typedef struct {
PyObject_HEAD
/** Will call this function with the result if the future succeeded */
@ -468,6 +618,33 @@ finally:
return success ? 0 : -1;
}
///////////////////////////////////////////////////////////////////////////////
//
// Javascript code
//
// The rest of the file is in Javascript. It would probably be better to move it
// into a .js file.
//
/**
* In the case that the Python object is callable, PyProxyClass inherits from
* Function so that PyProxy objects can be callable.
*
* The following properties on a Python object will be shadowed in the proxy in
* the case that the Python object is callable:
* - "arguments" and
* - "caller"
*
* Inheriting from Function has the unfortunate side effect that we MUST expose
* the members "proxy.arguments" and "proxy.caller" because they are
* nonconfigurable, nonwritable, nonenumerable own properties. They are just
* always `null`.
*
* We also get the properties "length" and "name" which are configurable so we
* delete them in the constructor. "prototype" is not configurable so we can't
* delete it, however it *is* writable so we set it to be undefined. We must
* still make "prototype in proxy" be true though.
*/
EM_JS_REF(JsRef, pyproxy_new, (PyObject * ptrobj), {
// Technically, this leaks memory, since we're holding on to a reference
// to the proxy forever. But we have that problem anyway since we don't
@ -477,43 +654,39 @@ EM_JS_REF(JsRef, pyproxy_new, (PyObject * ptrobj), {
if (Module.PyProxies.hasOwnProperty(ptrobj)) {
return Module.hiwire.new_value(Module.PyProxies[ptrobj]);
}
let flags = _pyproxy_getflags(ptrobj);
let cls = Module.getPyProxyClass(flags);
// Reflect.construct calls the constructor of Module.PyProxyClass but sets the
// prototype as cls.prototype. This gives us a way to dynamically create
// subclasses of PyProxyClass (as long as we don't need to use the "new
// cls(ptrobj)" syntax).
let target;
if (flags & IS_CALLABLE) {
// To make a callable proxy, we must call the Function constructor.
// In this case we are effectively subclassing Function.
target = Reflect.construct(Function, [], cls);
// Remove undesireable properties added by Function constructor. Note: we
// can't remove "arguments" or "caller" because they are not configurable
// and not writable
delete target.length;
delete target.name;
// prototype isn't configurable so we can't delete it but it's writable.
target.prototype = undefined;
} else {
target = Object.create(cls.prototype);
}
Object.defineProperty(
target, "$$", { value : { ptr : ptrobj, type : 'PyProxy' } });
_Py_IncRef(ptrobj);
let target = new Module.PyProxyClass();
target['$$'] = { ptr : ptrobj, type : 'PyProxy' };
// clang-format off
if (_PyMapping_Check(ptrobj) === 1) {
// clang-format on
// Note: this applies to lists and tuples and sequence-like things
// _PyMapping_Check returns true on a superset of things _PySequence_Check
// accepts.
Object.assign(target, Module.PyProxyMappingMethods);
}
let proxy = new Proxy(target, Module.PyProxyHandlers);
let itertype = __pyproxy_iterator_type(ptrobj);
// clang-format off
if (itertype === 2) {
Object.assign(target, Module.PyProxyIteratorMethods);
}
if (itertype === 1) {
Object.assign(target, Module.PyProxyIterableMethods);
}
// clang-format on
Module.PyProxies[ptrobj] = proxy;
let is_awaitable = __pyproxy_is_awaitable(ptrobj);
if (is_awaitable) {
Object.assign(target, Module.PyProxyAwaitableMethods);
}
return Module.hiwire.new_value(proxy);
});
// clang-format off
EM_JS_NUM(int, pyproxy_init_js, (), {
// clang-format off
Module.PyProxies = {};
function _getPtr(jsobj) {
let ptr = jsobj.$$.ptr;
if (ptr === null) {
@ -521,7 +694,45 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
}
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,
@ -530,8 +741,14 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
},
};
// We inherit from Function so that we can be callable.
Module.PyProxyClass = class extends Function {
// 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";
}
@ -557,22 +774,11 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
__pyproxy_destroy(ptrobj);
this.$$.ptr = null;
}
apply(jsthis, jsargs) {
let ptrobj = _getPtr(this);
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);
}
/**
* 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);
@ -581,9 +787,27 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
}
};
// These methods appear for lists and tuples and sequence-like things
// _PyMapping_Check returns true on a superset of things _PySequence_Check accepts.
Module.PyProxyMappingMethods = {
// 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);
@ -604,6 +828,11 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
}
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);
@ -621,9 +850,6 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
_pythonexc2js();
}
},
has : function(key) {
return this.get(key) !== undefined;
},
delete : function(key) {
let ptrobj = _getPtr(this);
let idkey = Module.hiwire.new_value(key);
@ -641,6 +867,29 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
}
};
// 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
@ -662,6 +911,8 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
}
};
// Controlled by IS_ITERATOR, appears for any object with a __next__ or
// tp_iternext method.
Module.PyProxyIteratorMethods = {
[Symbol.iterator] : function() {
return this;
@ -672,7 +923,7 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
// which gets converted to "Py_None". This is as intended.
let idarg = Module.hiwire.new_value(arg);
try {
idresult = __pyproxy_iter_send(_getPtr(this), idarg);
idresult = __pyproxyGen_Send(_getPtr(this), idarg);
} catch(e) {
Module.fatal_error(e);
} finally {
@ -681,7 +932,7 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
let done = false;
if(idresult === 0){
idresult = __pyproxy_iter_fetch_stopiteration();
idresult = __pyproxyGen_FetchStopIterationValue();
if (idresult){
done = true;
} else {
@ -693,21 +944,11 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
},
};
// These fields appear in the target by default because the target is a function.
// we want to filter them out.
let ignoredTargetFields = ["name", "length"];
// 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) {
if(Reflect.has(jsobj, jskey) && !ignoredTargetFields.includes(jskey)){
return true;
}
if(typeof(jskey) === "symbol"){
return false;
}
// 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;
@ -722,86 +963,138 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
_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) {
if(Reflect.has(jsobj, jskey) && !ignoredTargetFields.includes(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 undefined;
return Reflect.get(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);
// 2. The result of getattr
let idresult = python_getattr(jsobj, jskey);
if(idresult !== 0){
return Module.hiwire.pop_value(idresult);
}
if(idresult === 0){
_pythonexc2js();
}
return Module.hiwire.pop_value(idresult);
// 3. stuff from the prototype chain.
return Reflect.get(jsobj, jskey);
},
set: function (jsobj, jskey, jsval) {
if(
Reflect.has(jsobj, jskey) && !ignoredTargetFields.includes(jskey)
|| typeof(jskey) === "symbol"
){
if(typeof(jskey) === "symbol"){
jskey = jskey.description;
}
throw new Error(`Cannot set read only field ${jskey}`);
// 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}'`);
}
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();
// 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) {
if(
Reflect.has(jsobj, jskey) && !ignoredTargetFields.includes(jskey)
|| typeof(jskey) === "symbol"
){
if(typeof(jskey) === "symbol"){
jskey = jskey.description;
}
throw new Error(`Cannot delete read only field ${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 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);
let descr = Object.getOwnPropertyDescriptor(jsobj, jskey);
if(descr && !descr.writable){
throw new TypeError(`Cannot delete read only field '${jskey}'`);
}
if(errcode === -1){
_pythonexc2js();
}
return true;
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 result = new Set(Reflect.ownKeys(jsobj));
for(let key of ignoredTargetFields){
result.delete(key);
}
let ptrobj = _getPtr(jsobj);
let idresult;
try {
@ -809,18 +1102,37 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
} catch(e) {
Module.fatal_error(e);
}
let jsresult = Module.hiwire.pop_value(idresult);
for(let key of jsresult){
result.add(key);
}
return Array.from(result);
let result = Module.hiwire.pop_value(idresult);
result.push(...Reflect.ownKeys(jsobj));
return result;
},
apply: function (jsobj, jsthis, jsargs) {
return jsobj.apply(jsthis, jsargs);
let ptrobj = _getPtr(jsobj);
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);
},
};
/**
* 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;
@ -859,10 +1171,11 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
}
};
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`.
// TODO: Should we have this?
let globalsPropertyAccessWarned = false;
let globalsPropertyAccessWarningMsg =
"Access to pyodide.globals via pyodide.globals.key is deprecated and " +
@ -909,8 +1222,8 @@ EM_JS_NUM(int, pyproxy_init_js, (), {
};
return 0;
// clang-format on
});
// clang-format on
int
pyproxy_init()

View File

@ -59,52 +59,55 @@ def test_pyproxy(selenium):
def test_pyproxy_refcount(selenium):
selenium.run_js("window.jsfunc = function (f) { f(); }")
selenium.run(
result = selenium.run_js(
"""
import sys
from js import window
function getRefCount(){
return pyodide.runPython("sys.getrefcount(pyfunc)");
}
let result = [];
window.jsfunc = function (f) { f(); };
pyodide.runPython(`
import sys
from js import window
def pyfunc(*args, **kwargs):
print(*args, **kwargs)
def pyfunc(*args, **kwargs):
print(*args, **kwargs)
`);
// the refcount should be 2 because:
//
// 1. pyfunc exists
// 2. pyfunc is referenced from the sys.getrefcount()-test below
result.push([getRefCount(), 2]);
// the refcount should be 3 because:
//
// 1. pyfunc exists
// 2. one reference from PyProxy to pyfunc is alive
// 3. pyfunc is referenced from the sys.getrefcount()-test below
pyodide.runPython(`
window.jsfunc(pyfunc) # creates new PyProxy
`);
result.push([getRefCount(), 3])
pyodide.runPython(`
window.jsfunc(pyfunc) # re-used existing PyProxy
window.jsfunc(pyfunc) # re-used existing PyProxy
`)
// the refcount should be 3 because:
//
// 1. pyfunc exists
// 2. one reference from PyProxy to pyfunc is alive
// 3. pyfunc is referenced from the sys.getrefcount()-test
result.push([getRefCount(), 3]);
return result;
"""
)
# the refcount should be 2 because:
#
# 1. pyfunc exists
# 2. pyfunc is referenced from the sys.getrefcount()-test below
#
assert selenium.run("sys.getrefcount(pyfunc)") == 2
selenium.run(
"""
window.jsfunc(pyfunc) # creates new PyProxy
"""
)
# the refcount should be 3 because:
#
# 1. pyfunc exists
# 2. one reference from PyProxy to pyfunc is alive
# 3. pyfunc is referenced from the sys.getrefcount()-test below
#
assert selenium.run("sys.getrefcount(pyfunc)") == 3
selenium.run(
"""
window.jsfunc(pyfunc) # re-used existing PyProxy
window.jsfunc(pyfunc) # re-used existing PyProxy
"""
)
# the refcount should still be 3 because:
#
# 1. pyfunc exists
# 2. one reference from PyProxy to pyfunc is still alive
# 3. pyfunc is referenced from the sys.getrefcount()-test below
#
assert selenium.run("sys.getrefcount(pyfunc)") == 3
for [a, b] in result:
assert a == b, result
def test_pyproxy_destroy(selenium):
@ -255,3 +258,128 @@ def test_pyproxy_mixins(selenium):
then=True, catch=True, finally_=True, iterable=True, iterator=True
),
)
def test_pyproxy_mixins2(selenium):
selenium.run_js(
"""
window.assert = function assert(cb){
if(cb() !== true){
throw new Error(`Assertion failed: ${cb.toString().slice(6)}`);
}
};
window.assertThrows = function assert(cb, errname, pattern){
let err = undefined;
try {
cb();
} catch(e) {
err = e;
} finally {
if(!err){
throw new Error(`assertThrows(${cb.toString()}) failed, no error thrown`);
}
if(err.constructor.name !== errname){
console.log(err.toString());
throw new Error(
`assertThrows(${cb.toString()}) failed, expected error` +
`of type '${errname}' got type '${err.constructor.name}'`
);
}
if(!pattern.test(err.message)){
console.log(err.toString());
throw new Error(
`assertThrows(${cb.toString()}) failed, expected error` +
`message to match pattern '${pattern}' got:\n${err.message}`
);
}
}
};
assert(() => !("prototype" in pyodide.globals));
assert(() => !("caller" in pyodide.globals));
assert(() => !("name" in pyodide.globals));
assert(() => "length" in pyodide.globals);
let get_method = pyodide.globals.__getitem__;
assert(() => "prototype" in get_method);
assert(() => get_method.prototype === undefined);
assert(() => !("length" in get_method));
assert(() => !("name" in get_method));
assert(() => pyodide.globals.get.type === "builtin_function_or_method");
assert(() => pyodide.globals.set.type === undefined);
let [Test, t] = pyodide.runPython(`
class Test: pass
[Test, Test()]
`);
assert(() => Test.prototype === undefined);
assert(() => !("name" in Test));
assert(() => !("length" in Test));
assert(() => !("prototype" in t));
assert(() => !("caller" in t));
assert(() => !("name" in t));
assert(() => !("length" in t));
Test.prototype = 7;
Test.name = 7;
Test.length = 7;
pyodide.runPython("assert Test.prototype == 7");
pyodide.runPython("assert Test.name == 7");
pyodide.runPython("assert Test.length == 7");
delete Test.prototype;
delete Test.name;
delete Test.length;
pyodide.runPython(`assert not hasattr(Test, "prototype")`);
pyodide.runPython(`assert not hasattr(Test, "name")`);
pyodide.runPython(`assert not hasattr(Test, "length")`);
assertThrows( () => Test.$$ = 7, "TypeError", /^Cannot set read only field/);
assertThrows( () => delete Test.$$, "TypeError", /^Cannot delete read only field/);
[Test, t] = pyodide.runPython(`
class Test:
caller="fifty"
prototype="prototype"
name="me"
length=7
[Test, Test()]
`);
assert(() => Test.prototype === "prototype");
assert(() => Test.name==="me");
assert(() => Test.length === 7);
assert(() => t.caller === "fifty");
assert(() => t.prototype === "prototype");
assert(() => t.name==="me");
assert(() => t.length === 7);
[Test, t] = pyodide.runPython(`
class Test:
def __len__(self):
return 9
[Test, Test()]
`);
assert(() => !("length" in Test));
assert(() => t.length === 9);
t.length = 10;
assert(() => t.length === 10);
assert(() => t.__len__() === 9);
let l = pyodide.runPython(`
l = [5, 6, 7] ; l
`);
assert(() => l.get.type === undefined);
assert(() => l.get(1) === 6);
assert(() => l.length === 3);
l.set(0, 80);
pyodide.runPython(`
assert l[0] == 80
`);
l.delete(1);
pyodide.runPython(`
assert len(l) == 2 and l[1] == 7
`);
assert(() => l.length === 2 && l.get(1) === 7);
"""
)