Skip to content

Commit 9cbe7df

Browse files
committed
Add pyproxy hasattr, getattr, setattr, and delattr
1 parent 41336aa commit 9cbe7df

4 files changed

Lines changed: 387 additions & 3 deletions

File tree

Lib/test/test_pyodide.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,19 @@ def test_pyproxy_to_py(self):
372372
x = [1, 2, 3]
373373
self.assertIs(run_js("(x) => x")(x), x)
374374

375+
def test_pyproxy_getattr(self):
376+
class T:
377+
a = 7
378+
b = "zz"
379+
380+
self.assertEqual(run_js("(o) => o.a")(T), 7)
381+
self.assertEqual(run_js("(o) => o.b")(T), "zz")
382+
self.assertEqual(run_js("(o) => o.c")(T), None)
383+
run_js("(o) => {delete o.a}")(T)
384+
self.assertFalse(hasattr(T, "a"))
385+
run_js("(o) => {o.a = 72}")(T)
386+
self.assertTrue(T.a == 72)
387+
375388
def test_pyproxy_call_simple(self):
376389
def f(x):
377390
return x * x + 7

Modules/_pyodide_core/pyproxy.c

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
#include "emscripten.h"
55
#include "error_handling.h"
66

7+
#define Py_ENTER()
8+
#define Py_EXIT()
9+
710
#define HAS_CONTAINS (1 << 0)
811
#define HAS_GET (1 << 1)
912
#define HAS_LENGTH (1 << 2)
@@ -14,6 +17,11 @@ EM_JS_VAL(JsVal, pyproxy_new, (PyObject * ptrobj), {
1417
return Module.pyproxy_new(ptrobj);
1518
});
1619

20+
EM_JS(int, PyProxy_Check, (JsVal val), {
21+
return API.isPyProxy(val);
22+
});
23+
24+
1725
EMSCRIPTEN_KEEPALIVE JsVal
1826
_pyproxy_str(PyObject* pyobj)
1927
{
@@ -84,8 +92,150 @@ pyproxy_getflags(PyObject* pyobj)
8492
return type_getflags(obj_type);
8593
}
8694

87-
#define Py_ENTER()
88-
#define Py_EXIT()
95+
EMSCRIPTEN_KEEPALIVE int
96+
_pyproxy_hasattr(PyObject* pyobj, JsVal jskey)
97+
{
98+
PyObject* pykey = NULL;
99+
int result = -1;
100+
101+
pykey = js2python(jskey);
102+
FAIL_IF_NULL(pykey);
103+
result = PyObject_HasAttr(pyobj, pykey);
104+
105+
finally:
106+
Py_CLEAR(pykey);
107+
return result;
108+
}
109+
110+
/* Specialized version of _PyObject_GenericGetAttrWithDict
111+
specifically for the LOAD_METHOD opcode.
112+
113+
Return 1 if a method is found, 0 if it's a regular attribute
114+
from __dict__ or something returned by using a descriptor
115+
protocol.
116+
117+
`method` will point to the resolved attribute or NULL. In the
118+
latter case, an error will be set.
119+
*/
120+
int
121+
_PyObject_GetMethod(PyObject* obj, PyObject* name, PyObject** method);
122+
123+
EM_JS(JsVal, proxy_cache_get, (JsVal proxyCache, PyObject* descr), {
124+
const proxy = proxyCache.get(descr);
125+
if (!proxy) {
126+
return Module.error;
127+
}
128+
// Okay found a proxy. Is it alive?
129+
if (pyproxyIsAlive(proxy)) {
130+
return proxy;
131+
} else {
132+
// It's dead, tidy up
133+
proxyCache.delete(descr);
134+
return Module.error;
135+
}
136+
})
137+
138+
// clang-format off
139+
EM_JS(void,
140+
proxy_cache_set,
141+
(JsVal proxyCache, PyObject* descr, JsVal proxy), {
142+
proxyCache.set(descr, proxy);
143+
})
144+
// clang-format on
145+
146+
EMSCRIPTEN_KEEPALIVE JsVal
147+
_pyproxy_getattr(PyObject* pyobj, JsVal key, JsVal proxyCache)
148+
{
149+
bool success = false;
150+
PyObject* pykey = NULL;
151+
PyObject* pydescr = NULL;
152+
PyObject* pyresult = NULL;
153+
JsVal result = JS_ERROR;
154+
155+
pykey = js2python(key);
156+
FAIL_IF_NULL(pykey);
157+
// If it's a method, we use the descriptor pointer as the cache key rather
158+
// than the actual bound method. This allows us to reuse bound methods from
159+
// the cache.
160+
// _PyObject_GetMethod will return true and store a descriptor into pydescr if
161+
// the attribute we are looking up is a method, otherwise it will return false
162+
// and set pydescr to the actual attribute (in particular, I believe that it
163+
// will resolve other types of getter descriptors automatically).
164+
int is_method = _PyObject_GetMethod(pyobj, pykey, &pydescr);
165+
FAIL_IF_NULL(pydescr);
166+
JsVal cached_proxy = proxy_cache_get(proxyCache, pydescr); /* borrowed */
167+
if (!JsvError_Check(cached_proxy)) {
168+
result = cached_proxy;
169+
goto success;
170+
}
171+
if (PyErr_Occurred()) {
172+
FAIL();
173+
}
174+
if (is_method) {
175+
pyresult =
176+
Py_TYPE(pydescr)->tp_descr_get(pydescr, pyobj, (PyObject*)Py_TYPE(pyobj));
177+
FAIL_IF_NULL(pyresult);
178+
} else {
179+
pyresult = pydescr;
180+
Py_INCREF(pydescr);
181+
}
182+
result = python2js(pyresult);
183+
if (PyProxy_Check(result)) {
184+
// If a getter returns a different object every time, this could potentially
185+
// fill up the cache with a lot of junk. If this is a problem, the user will
186+
// have to manually destroy the attributes.
187+
proxy_cache_set(proxyCache, pydescr, result);
188+
}
189+
190+
success:
191+
success = true;
192+
finally:
193+
Py_CLEAR(pykey);
194+
Py_CLEAR(pydescr);
195+
Py_CLEAR(pyresult);
196+
if (!success) {
197+
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
198+
PyErr_Clear();
199+
}
200+
}
201+
return result;
202+
};
203+
204+
EMSCRIPTEN_KEEPALIVE int
205+
_pyproxy_setattr(PyObject* pyobj, JsVal key, JsVal value)
206+
{
207+
bool success = false;
208+
PyObject* pykey = NULL;
209+
PyObject* pyval = NULL;
210+
211+
pykey = js2python(key);
212+
FAIL_IF_NULL(pykey);
213+
pyval = js2python(value);
214+
FAIL_IF_NULL(pyval);
215+
FAIL_IF_MINUS_ONE(PyObject_SetAttr(pyobj, pykey, pyval));
216+
217+
success = true;
218+
finally:
219+
Py_CLEAR(pykey);
220+
Py_CLEAR(pyval);
221+
return success ? 0 : -1;
222+
}
223+
224+
EMSCRIPTEN_KEEPALIVE int
225+
_pyproxy_delattr(PyObject* pyobj, JsVal idkey)
226+
{
227+
bool success = false;
228+
PyObject* pykey = NULL;
229+
230+
pykey = js2python(idkey);
231+
FAIL_IF_NULL(pykey);
232+
FAIL_IF_MINUS_ONE(PyObject_DelAttr(pyobj, pykey));
233+
234+
success = true;
235+
finally:
236+
Py_CLEAR(pykey);
237+
return success ? 0 : -1;
238+
}
89239

90240
EMSCRIPTEN_KEEPALIVE int
91241
_pyproxy_contains(PyObject* pyobj, JsVal idkey)

Modules/_pyodide_core/pyproxy.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@
33

44
JsVal
55
pyproxy_new(PyObject* obj);
6+
7+
bool
8+
PyProxy_Check(JsVal);

0 commit comments

Comments
 (0)