run_jsmakePythonFunctionJSProxyJSProxy MetaclassJSProxy Base ClassHAS_GET MixinHAS_SET MixinHAS_HAS MixinHAS_INCLUDES MixinHAS_LENGTH MixinHAS_DISPOSE MixinIS_ARRAY MixinIS_ARRAY_LIKE MixinIS_CALLABLE MixinIS_ERROR MixinIS_ITERABLE MixinIS_ITERATOR MixinIS_GENERATOR MixinIS_MAPPING MixinIS_MUTABLE_MAPPING MixinIS_PY_JSON_SEQUENCE MixinIS_PY_JSON_DICT MixinIS_DOUBLE_PROXY MixinPyProxyPyProxy Base ClassHAS_GET MixinHAS_SET MixinHAS_CONTAINS MixinHAS_LENGTH MixinIS_CALLABLE MixinIS_DICT MixinIS_ITERABLE MixinIS_ITERATOR MixinIS_GENERATOR MixinIS_SEQUENCE MixinIS_MUTABLE_SEQUENCE MixinIS_JS_JSON_DICT MixinIS_JS_JSON_SEQUENCE Mixinjstypes.global_this Modulejstypes packagejson ModulePyodide is a distribution of Python for JavaScript runtimes, including browsers.Browsers are a universal computing platform. As with C for Unix family operatingsystems, in the browser platform all fundamental capabilities are exposedthrough the JavaScript language. For years, Pyodide has included a comprehensiveJavaScript foreign function interface. This provides the equivalent of theos module for the JavaScript world.
This PEP proposes adding the core of the Pyodide foreign function interface toPython.
The Pyodide project is a Python distribution for JavaScript runtimes. Pyodide isa very popular project. In 2025, Pyodide received over a billion requests onJsDelivr. The popularity is rapidly growing: usage has more than doubled in eachof the last two years.
Pyodide includes several components:
In the long run, we would like to upstream the runtime components (1)–(4) ofthe Pyodide project into CPython. In 2022, Christian Heimes upstreamed (1) theEmscripten port of CPython, and Emscripten is currently a tier 3 supportedplatform (seePEP 776).PEP 783 proposes to allow Pyodide-compatiblewheels to be uploaded to PyPI. What is needed for these to beEmscripten-CPython-compatible wheels is to upstream (2) the Python/JavaScriptforeign function interface and (4) the ABI for native extensions. This PEPconcerns partially upstreaming (2) the Python/JavaScript foreign functioninterface.
This interface is similar to theos module for Python on linux: all IOrequires going through libc and theos module provides access to libc callsto Python code. Similarly, in a JavaScript runtime, to do any actual workrequires making calls into #"#rationale" role="doc-backlink">Rationale
Our goal here is to upstream Pyodide’s foreign function interface, withoutbreaking backwards compatibility more than necessary for Pyodide’s largecollection of existing users. On the other hand, the best time to makingbreaking changes is now.
With that in mind, we wish here to justify not that our design is perfect butthat the costs of any changes outweigh the benefits.
The most fundamental decision is how we translate objects from one language tothe other. When translating an object, we can either choose to convert the valueinto a similar object in the target language, or to make a proxy that “wraps”the original object. A few considerations apply here:
JavaScript has the following immutable types:string,undefined,boolean,number andbigint. It also has the special valuenull.
Of these,string andboolean directly correspond tostr andbool. We convert anumber to anint ifNumber.isSafeInteger()returnstrue and otherwise we convert it to afloat. Conversely weconvertfloat tonumber and we convertint tonumber unless itexceeds2**53 in which case we convert it to abigint. We make a newsubclass ofint calledJSBigInt to act as the conversion forbigint.``undefined`` is the default value for a missing argument so itcorresponds toNone. We invent a new falsey singleton Python valuejsnull of typeJSNull to act as the conversion ofnull. All othertypes are proxied.
In particular, even thoughtuples are immutable, they have no equivalent inJavaScript so we proxy them. They can be manually converted to anArray withthetoJs() method if desired.
AJSProxy is a Python object used for accessing a JavaScript object. WhiletheJSProxy exists, the underlying JavaScript object is kept in a tablewhich keeps it from being garbage collected.
APyProxy is a JavaScript object used for accessing a Python object. When aPyProxy is created, the reference count of the underlying Python object isincremented. When the.destroy() method is called, the reference count of theunderlying Python object is decremented and the proxy is disabled. Any furtherattempt to use it raises an error.
The baseJSProxy implements property access, equality checks,__repr__,__eq__,__bool__, and a handful of other convenience methods. We alsodefine a large number of mixins by mapping abstract Python object protocols toabstract JavaScript object protocols (and vice-versa). The mapping described inthis PEP is as follows:
Base proxies (properties common to all objects):
__getattribute__ <==>Reflect.get (proxy handler)__setattr__ <==>Reflect.set (proxy handler)__eq__ <==>=== (object identity)__repr__ <==>toStringFor the__str__ implementation, we inherit the default implementation whichuses__repr__.
We implement the following mappings between protocols as mixins. When we createa proxy, we feature detect which of these abstract and concrete protocols itsupports and create a class for the proxy with the appropriate mixins.
__iter__ <==>[Symbol.iterator]__next__ <==>next__len__ <==>length,size__getitem__ <==>get__setitem__,__delitem__ <==>set,delete__contains__ <==>includes,has__call__ <==>Reflect.apply (proxy handler)Generator <==>GeneratorException <==>ErrorMutableSequence <==>ArrayIf a JavaScript object has a[Symbol.dispose]() method, we make the Pythonobject into a context manager, but we do not presently use context managers toimplement[Symbol.dispose]().
JavaScript also hasReflect.construct (thenew keyword). CallableJSProxies have a method callednew() which corresponds toReflect.construct.
The following additional mappings are defined in Pyodide. It is our intention toeventually add them to Python itself, but they are deferred to a future PEP:
__await__ <==>then__aiter__ <==>[Symbol.asyncIterator]__anext__ <==>next (same as__next__; check for presence of[Symbol.asyncIterator] to distinguish)AsyncGenerator <==>AsyncGenerator[Symbol.asyncDispose].The most fundamental difficulty that we face is the existence of two garbagecollectors, the Python garbage collector and the JavaScript garbage collector.Any reference loop from Python to JavaScript back to Python will be leaked.Furthermore, even if there is no loop, the JavaScript garbage collector has noidea how much memory aPyProxy owns nor how much memory pressure the Pythongarbage collector faces.
For this reason, we need to include a way to manually break references betweenlanguages. In Python, destructors are run eagerly when the reference count of anobject reaches 0. Thus, if a programmer wishes to manually release a JavaScriptobject, they can delete all references to it and after that the JavaScriptgarbage collector will be able to reclaim it.
On the other hand, JavaScript finalizers are not reliable. The proposal thatintroduced them to the language says the following:
If an application or library depends on GC [calling a finalizer] in a timely,predictable manner, it’s likely to be disappointed: the cleanup may happen muchlater than expected, or not at all.…
It’s best if [finalizers] are used as a way to avoid excess memory usage, or as abackstop against certain bugs, rather than as a normal way to clean up externalresources.
https://github.com/tc39/proposal-weakrefs?tab=readme-ov-file#a-note-of-caution
APyProxy has adestroy() method that manually detaches thePyProxyand releases the Python reference. We consider destroying aPyProxy to bethe correct, normal way to clean it up. As recommended by the proposal, thefinalizer is treated as a backstop. In the Pyodide test suite, we require thateveryPyProxy be manually destroyed in the majority of the tests. This helpsto ensure that our APIs are designed in a way that keeps this ergonomic.
To call a callablePyProxy we do the following steps:
PyObject_VectorCall to call the Python object.sys.last_value to the currentexception. Convert the Python exception to a JavaScriptPythonErrorobject. ThisPythonError object records the type, the formatted tracebackof the Python exception, and a weak reference to the original Pythonexception. Throw thisPythonError.Note here that if aJSProxy is created but the Python function does notstore a reference to it, it will be released immediately. The JavaScript errordoesn’t hold a strong reference the Python exception because JavaScript errorsare often leaked and Python error objects hold a reference to frame objectswhich may hold a significant amount of memory.
To call a callableJSProxy we do the following steps:
pyproxiesjsargs. If anyPyProxy isgenerated in this way, don’t register a JavaScript finalizer for it and doappend it topyproxies.jskwargs, translate each keyword argument to JavaScript and assignjskwargs[key]=jskwarg. Appendjskwargs tojsargs. If anyPyProxy is generated in this way, don’t register a JavaScript finalizerfor it and do append it topyproxies.jsresult.
- If the error is a
PythonErrorand the weak reference to the Pythonexception is still alive, raise the referenced Python exception.- Otherwise, convert the exception from JavaScript to Python and raise theresult. Note that the
JSExceptionobject holds a reference to theoriginal JavaScript error.
jsresult is a JavaScript generator, iterate overpyproxies andregister a JavaScript finalizer for each. Wrap the generator with a newgenerator that destroyspyproxies when they are exhausted. Translate thewrapped generator to Python and return it.jsresult to Python and store it inpyresult.pyproxies and destroy them. Ifjsresult is aPyProxy, destroy it too.pyresult.This is modeled on the calling convention for C Python APIs.
JSProxyThe calling convention from JavaScript into Python is uncontroversial so we willnot defend it. The calling convention from Python into JavaScript is morecontroversial so we will explain here why we believe it is a better design thanthe alternatives.
The main disadvantage of this design is that it is not as ergonomic in caseswhere the callee is going to persist its arguments. However, we argue that thebenefits outweigh this.
The biggest advantage of this approach is that it makes it possible to useJavaScript functions that are unaware of the existence of Python without memoryleaks. Another advantage is that registering a finalizer for aPyProxy issomewhat expensive and so avoiding this step can substantially decrease theoverhead for certain Python to JavaScript calls.
We will start by illustrating the common complaint about the Python toJavaScript calling convention. Consider the following example:
fromjstypes.codeimportrun_jsset_x=run_js("(x) => { globalThis.x = x; }")get_x=run_js("(x) => globalThis.x")set_x({})get_x()
This code is broken. Callingset_x creates a PyProxy but it is destroyedwhen the call is done. When we callget_x() the following error is raised:
Thisborrowedproxywasautomaticallydestroyedattheendofafunctioncall.
To fix it to manage memory correctly, we can changeset_x to the followingfunction:
(x)=>{globalThis.x?.destroy?.();globalThis.x=x?.copy?.()??x;}
Or we can manage the memory from Python usingcreate_proxy() as follows:
fromjstypes.ffiimportJSDoubleProxyfromjstypes.codeimportrun_jssetXJs=run_js("(x) => { globalThis.x = x; }")defset_x(x):orig_x=get_x()ifisinstance(orig_x,JSDoubleProxy):orig_x.destroy()xpx=create_proxy(x)setXJs(xpx)
This extra boilerplate is not too hard to get right – it’s roughly equivalentto what is needed to assign an attribute in C. However, it does impose anontrivial complexity cost on the user and so we need to justify why this isbetter than the alternatives.
Suppose we have a Python functionrender() that returns a buffer, and aJavaScript functiondrawImageToCanvas(buffer) that displays the buffer on acanvas. If the buffer is a 1024 by 1024 bitmap with four color channels, then itis a 4 megabyte buffer. Imagine the following code:
@create_proxydefmain_loop():update()buf=render()drawImageToCanvas(buffer)requestAnimationFrame(main_loop)
With the calling convention described here, the buffer is released normallyafter each call and memory usage stays consistent, in my tests it stays at 57megabytes.
If we rely on a JavaScript finalizer to releasebuffer, in my tests theJavaScript finalizer doesn’t run until malloc runs out of space on theWebAssembly heap and requests more memory, with the effect that over severalminutes the WebAssembly heap gradually grows to the maximum allowed 4 gigabytesand then a memory error is raised.
Now a cooperating implementation ofdrawImageToCanvas() could destroy thebuffer when it is done, but my philosophy in designing the callingconvention was that it should be possible to take care of the memory managementfrom Python. This necessitates something like the current approach.
We introduce a new top level package calledjstypes.
Thejstypes package three two modules:jstypes.code andjstypes.ffi.Thejstypes.global_this package is the JavaScript global scopeglobalThis. What set of values are present on thejstypes.global_thismodule depends on the JavaScript runtime and whether the Python runtime is inthe main thread or a worker thread. For instancefromjstypes.global_thisimportBuffer will succeed in Node but fail in a browser.
jstypes.code exposes therun_js function.
jstypes.ffi exposes the following functions:
create_proxyPyProxy from Python. Used to control the lifetime of thePyProxy from Python.jsnullnull value.JSNulljsnull.JSBigIntint that converts to/from JavaScriptbigint.to_jsWe also includeJSProxy and its subtypes:
JSProxytype(run_js("({})"))JSArraytype(run_js("[]")).JSCallabletype(run_js("()=>{}")).JSDoubleProxytype(create_proxy({})).JSExceptiontype(run_js("newError()")).JSGeneratortype(run_js("(function*(){})()")).JSIterabletype(run_js("({[Symbol.iterator](){}})")).JSIteratortype(run_js("({next(){}})")).JSMaptype(run_js("({get(){}})")).JSMutableMaptype(run_js("newMap()")).The pseudocode in this PEP is generally written in Python or JavaScript. Weleave out most resource management and exception handling except when we thinkit is particularly interesting. If an error is raised, we implicitly clean upall resources and propagate the error. A large fraction of the real codeconsists of resource management and exception handling.
For the most part the code works as written but in a few spots we directly calla C API from Python or otherwise write code that wouldn’t run but whose intentwe believe is clear.
In Python code when we want to execute a JavaScript function inline, we write itlike:
jsfunc=run_js("(x, y) => doSomething")jsfunc(x,y)
Conversely, when we want to execute Python code inline in JavaScript we write itlike this:
constpyfunc=makePythonFunction(` def pyfunc(x, y): # do something`);pyfunc(x,y)
For the most part, this code could actually be used if performance was not aconcern. In some places there may be bootstrapping issues.
Our first task is to define the Python callablerun_js and the JavaScriptcallablemakePythonFunction.run_js is aJSProxy andmakePythonFunction is aPyProxy.
To make sense of this, we need to describe
We can directly represent aPyObject* as anumber in JavaScript so wecan describe the process of calling aPyObject* from JavaScript. On theother hand, JavaScript objects are not directly representable in Python, we haveto create aJSProxy of it. We describe first the process of calling aJSProxy, the process of creating it is described in the section onJSProxies.
A few primitive types are implicitly converted between Python and JavaScript.Implicit conversions are supposed to round trip, so that when converting fromPython to JavaScript back to Python or from JavaScript to Python back toJavaScript, the result is the same primitive as we started with. The oneexception to this is that a JavaScriptBigInt that is smaller than2^53round trips to aNumber. We convertundefined toNone and introducethe special falsey singletonjstypes.ffi.jsnull to convertnull. We alsointroduce a subtype ofint calledjstypes.ffi.JSBigInt which converts toand from JavaScriptbigint.
Implicit conversions are done with the C functions_Py_python2js and_Py_js2python(). These functions cannot be called directly from Python codebecause theJSVal type is not representable in Python.
JSVal_Py_python2js_track_proxies(PyObject*pyvalue,JSValpyproxies,boolgc_register)is responsible for implicit conversions from Python to JavaScript. It does thefollowing steps:
pyvalue isNone, returnundefinedpyvalue isjsnull, returnnullpyvalue isTrue, returntruepyvalue isFalse, returnfalsepyvalue is astr, convert the string to JavaScript and return the result.pyvalue is an instance ofJSBigInt, convert it to aBigInt.pyvalue is anint and it is less than2^53, convert it toaNumber. Otherwise, convert it to aBigIntpyvalue is afloat, convert it to aNumber.pyvalue is aJSProxy, convert it to the wrapped JavaScript value.result becreatePyProxy(pyvalue,{gcRegister:gc_register}). Ifpyproxies is an array, appendresult topyproxies.We defineJSVal_Py_python2js(PyObject*pyvalue) to be_Py_python2js_track_proxies(pyvalue,Js_undefined,true).
PyObject*_Py_js2python(JSValjsvalue) is responsible for implicitconversions from JavaScript to Python.
We first define the helper functionPyObject*_Py_js2python_immutable(JSValjsvalue)does the following steps:
jsvalue isundefined, returnNonejsvalue isnull returnjsnulljsvalue istrue returnTruejsvalue isfalse returnFalsejsvalue is astring, convert the string to Python and return theresult.jsvalue is aNumber andNumber.isSafeInteger(jsvalue) returnstrue, then convertjsvalue to anint. Otherwise convert it to afloat.jsvalue is aBigInt then convert it to anJSBigInt.jsvalue is aPyProxy that has not been destroyed, convert it tothe wrapped Python value.jsvalue is aPyProxy that has been destroyed, throw an errorindicating this.NoValue._Py_js2python(JSValjsvalue) does the following steps:
result be_Py_js2python_immutable(jsvalue). Ifresult isnotNoValue, returnresult.create_jsproxy(jsvalue).At the boundary between JavaScript and C, we have to translate errors.
When we execute any JavaScript code from C, we wrap it in a try/catch block. Ifan error is caught, we use_Py_js2python(jserror) to convert it into aPython exception, set the Python error flag to this python exception, and returnthe appropriate error value to signal an error. This makes it ergonomic tocreate JavaScript functions that can be called from C and follow CPython’snormal conventions for C APIs.
Whenever we call into C from JavaScript, we wrap the call in the followingboilerplate:
try{result=some_c_function();}catch(e){// If an error was thrown here, the C runtime state is corrupted.// Signal a fatal error and tear down the interpreter.fatal_error(e);}// Depending on the API, we check for -1, 0, _PyErr_Occurred(), etc to// decide if an error occurred.if(result===-1){// This function takes the error flag and converts it to a JavaScript// exception. It leaves the error flag cleared.throw__Py_pythonexc2js();}
To call aPyObject* from JavaScript we use the following code:
functioncallPyObjectKwargs(pyfuncptr,jsargs,kwargs){constnum_pos_args=jsargs.length;constkwargs_names=Object.keys(kwargs);constkwargs_values=Object.values(kwargs);constnum_kwargs=kwargs_names.length;jsargs.push(...kwargs_values);// apply the usual error handling logic for calling from JavaScript into C.return_PyProxy_apply(pyfuncptr,jsargs,num_pos_args,kwargs_names,num_kwargs);}
_PyProxy_apply(PyObject* callable, JSVal jsargs, Py_ssize_t num_pos_args, JSVal kwargs_names, Py_ssize_t num_kwargs)
total_args benum_pos_args+numkwargs.pyargs of lengthtotal_args.i ranging from0 tototal_args-1:
- Execute the JavaScript code
jsargs[i]and store the result intojsitem.- Set
pyargs[i]to_Py_js2python(jsitem).
pykwnames be a new tuple of lengthnumkwargsi ranging from0 tonumkwargs-1:
- Execute the JavaScript code
jskwnames[i]and store the result intojskey.- Set the ith entry of
pykwnamesto_Py_js2python(jsitem).
pyresult bePyObject_Vectorcall(callable,pyargs,num_pos_args,pykwnames)._Py_python2js(pyresult).``JSMethod_ConvertArgs(posargs, kwargs, pyproxies)``
First we define the functionJSMethod_ConvertArgs to convert the Pythonarguments to a JavaScript array of arguments. AnyPyProxy created at thisstage is not tracked by the finalization registry and is added to the JavaScriptlistpyproxies so we can either destroy it or track it later. This functionperforms the following steps:
jsargs be a new empty JavaScript list.
- Set
JSValjsarg=_Py_python2js_track_proxies(pyarg,proxies,/*gc_register:*/false);.- Call
_PyJsvArray_Push(jsargs,arg);.
- Let
jskwargsbe a new empty JavaScript object.- For each keyword argument pykey, pyvalue:
- Set
JSValjskey=_Py_python2js(pykey)- Set
JSValjsvalue=_Py_python2js_track_proxies(pyvalue,proxies,/*gc_register:*/false)- Set the
jskeyproperty onjskwargstojsvalue.- Call
_PyJsvArray_Push(jsargs,jskwargs);
jsargs``JSMethod_Vectorcall(jsproxy, posargs, kwargs)``
EachJSProxy of a function has an underlying JavaScript function and anunderlyingthis value.
jsfunc be the JavaScript function associated tojsproxy.jsthis be thethis value associated tojsproxy.pyproxies be a new empty JavaScript list.JSMethod_ConvertArgs(posargs,kwargs,pyproxies) and store theresult intojsargs.Function.prototype.apply.apply(jsfunc,[jsthis,jsargs])and store the result intojsresult.(Apply the usual error handling for calling from C into JavaScript.)jsresult is aPyProxy run the JavaScript codepyproxies.push(jsresult)destroy_args totruejsresult is aGenerator setdestroy_args tofalse and setjsresult towrap_generator(jsresult,pyproxies)._Py_js2python(jsresult) and store the result intopyresult.destroy_args istrue, then destroy all the proxies inpyproxies.destroy_args isfalse, gc register all the proxies inpyproxies.pyresult.wrap_generator(jsresult,pyproxies) is a JavaScript function that wraps aJavaScript generator in a new generator that destroys all the proxies inpyproxies when the generator is exhausted.
run_jsThe Python objectjstypes.code.run_js is defined as follows:
eval and store the result intojseval._Py_js2python(jseval) and store the result intorun_js.makePythonFunctionUnlikerun_js, the JavaScript objectmakePythonFunction is strictly forthe sake of our pseudocode and will not be included as part of the API. Wedefine definemakePythonFunction as follows:
defmake_python_function(code):mod=ast.parse(code)ifisinstance(mod.body[0],ast.FunctionDef):d={}exec(code,d)returnd[mod.body[0].name]returneval(code)
make_python_function be the function above._Py_python2js(make_python_function) and store the result intomakePythonFunction.We define 14 different abstract protocols that a JavaScript object can support.These each correspond to aJSProxy type flag. There are also two additionalflagsIS_PY_JSON_DICT andIS_PY_JSON_SEQUENCE which are set by theJSProxy.as_py_json() method and do not reflect properties of the underlyingJavaScript object.
HAS_GETget()method. If present, used to implement__getitem__ on theJSProxy.HAS_HAShas() method. Ifpresent, used to implement__contains__ on theJSProxy.HAS_INCLUDESincludes() method.If present, used to implement__contains__ on theJSProxy. We preferto usehas() toincludes() if both are present.HAS_LENGTHlength orsizeproperty. Used to implement__len__ on theJSProxy.HAS_SETset() method. Ifpresent, used to implement__setitem__ on theJSProxy.HAS_DISPOSE[Symbol.dispose]()method. If present, used to implement__enter__ and__exit__.IS_ARRAYArray.isArray() applied to the JavaScript object returnstrue. If present, theJSProxy will be an instance ofcollections.abc.MutableSequence.IS_ARRAY_LIKEArray.isArray() returnsfalse and the object has alength property andIS_ITERABLE. If present, theJSProxy will bean instance ofcollections.abc.Sequence. This is the case for manyinterfaces defined in the webidl such asNodeListIS_CALLABLEtypeof the JavaScript object is"function". Ifpresent, used to implement__call__ on theJSProxy.IS_ERRORError. If so, theJSProxy it will subclassException so it can be raised.IS_GENERATORJSProxywill be an instance ofcollections.abc.Generator.IS_ITERABLE[Symbol.iterator] method ortheIS_PY_JSON_DICT flag is set. If so, we use it to implement__iter__ on theJSProxy.IS_ITERATORnext() method and no[Symbol.asyncIterator] method. If so, we use it to implement__next__ on theJSProxy. (If there is a[Symbol.asyncIterator]method, we assume that thenext() method should be used to implement__anext__.)IS_PY_JSON_DICTJSProxy by theas_py_json() method if it is not anArray. When this is set,__getitem__ on theJSProxy will turninto attribute access on the JavaScript object. Also, the return values fromiterating over the proxy or indexing it will also haveIS_PY_JSON_DICTorIS_PY_JSON_SEQUENCE set as appropriate.IS_PY_JSON_SEQUENCEJSProxy by theas_py_json() method if it is anArray. When this is set, when indexing or iterating theJSProxywe’ll callas_py_json() on the result.IS_MAPPINGHAS_GET,HAS_LENGTH, andIS_ITERABLEare set, or ifIS_PY_JSON_DICT is set. In this case, theJSProxywill be an instance ofcollections.abc.Mapping.IS_MUTABLE_MAPPINGIS_MAPPING andHAS_SET are set or ifIS_PY_JSON_DICT is set. In this case, theJSProxy will be aninstance ofcollections.abc.MutableMapping.JSProxyTo create aJSProxy from a JavaScript object and a valuejsthis we do thefollowing steps:
JSProxy class with the mixinsappropriate for the set of type flags that are setjsthis value.The valuejsthis is used to determine the value ofthis when calling afunction. Ifjsobj is not callable, is has no effect.
Here is pseudocode for the functionscreate_jsproxy andcreate_jsproxy_with_flags:
defcreate_jsproxy(jsobj,jsthis=Js_undefined):# For the definition of ``compute_type_flags``, see "Determining which flags to set".returncreate_jsproxy_with_flags(compute_type_flags(jsobj),jsobj,jsthis)defcreate_jsproxy_with_flags(type_flags,jsobj,jsthis):cls=get_jsproxy_class(type_flags)returncls.__new__(jsobj,jsthis)
The most important logic is for creating the classes, which works approximatelyas follows:
@functools.cachedefget_jsproxy_class(type_flags):flag_mixin_pairs=[(HAS_GET,JSProxyHasGetMixin),(HAS_HAS,JSProxyHasHasMixin),# ...(IS_PY_JSON_DICT,JSPyJsonDictMixin)]bases=[mixinforflag,mixininflag_mixin_pairsifflag&type_flags]bases.insert(0,JSProxy)iftype_flags&IS_ERROR:# We want JSException to be pickleable so it needs a distinct namename="jstypes.ffi.JSException"bases.append(Exception)else:name="jstypes.ffi.JSProxy"ns={"_js_type_flags":type_flags}# Note: The actual way that we build the class does not result in the# mixins appearing as entries on the mro.returnJSProxyMeta.__new__(JSProxyMeta,name,tuple(bases),ns)
JSProxy MetaclassThis metaclass overrides subclass checks so that if oneJSProxy class has asuperset of the flags of anotherJSProxy class, we report it as a subclass.
class_JSProxyMetaClass(type):def__instancecheck__(cls,instance):returncls.__subclasscheck__(type(instance))def__subclasscheck__(cls,subcls):iftype.__subclasscheck__(cls,subcls):returnTrueifnothasattr(subclass,"_js_type_flags"):returnFalsesubcls_flags=subcls._js_type_flags# Check whether the flags on subcls are a subset of the flags on clsreturncls._js_type_flags&subcls_flags==subcls_flags
JSProxy Base ClassThe most complicated part of theJSProxy base class is the implementation of__getattribute__,__setattr__, and__delattr__. For__getattribute__, we first check if an attribute is defined on the Pythonobject itself by callingobject.__getattribute__(). Otherwise, we look upthe attribute on the JavaScript object.
For__setattr__ and__delattr__, we set the keys “__loader__”,“__name__”, “__package__”, “__path__”, and “__spec__” on the Python objectitself. All other values are set/deleted on the underlying JavaScript object.This is to allow JavaScript objects to serve as Python modules without modifyingthem.
As an odd special case, if the object is anArray, we filter out thekeys method. We also remove it from the results ofdir(). This is toensure thatdict.update() behaves correctly when passed a JavaScriptArray. We want the following behavior:
d={}d.update(run_js("[['a', 'b'], [1, 2]]"))assertd=={"a":"b",1:2}# The result if we didn't filter out Array.keys would be as follows:assertd!={1:['a','b'],2:[1,2]}
A possible alternative would be to teach add special case handling forJavaScript arrays todict.update().
It is common for JavaScript objects to have important methods that are named thesame thing as a Python keyword (for example,Array.from,Promise.then). We access these from Python using the valid identifiersfrom_ andthen_. If we want to access a JavaScript property calledthen_ we access it fromthen__ and so on. So if the attribute is aPython keyword followed by one or more underscores, we remove oneunderscore from the end. The following helper function is used for this:
defnormalize_python_keywords(attr):stripped=attr.strip("_")ifnotkeyword.iskeyword(stripped):returnattrifstripped!=attr:returnattr[:-1]returnattr
We need the following JavaScript function to implement__bool__. InJavaScript, empty containers are truthy but in Python they should be falsey, sowe detect empty containers and returnfalse.
functionjs_bool(val){// if it's a falsey JS object, return falseif(!val){returnfalse;}// We also want to return false on container types with size 0.if(val.size===0){// Return true for HTML elements even if they have a size of zero.if(valinstanceofHTMLElement){returntrue;}returnfalse;}// A function with zero arguments has a length property equal to// zero. Make sure we return true for this.if(val.length===0&&Array.isArray(val)){returnfalse;}// An empty bufferif(val.byteLength===0){returnfalse;}returntrue;}
The following helper function is used to implement__dir__. It walks theprototype chain and accumulates all keys, filtering out keys that start withnumbers (not valid Python identifiers) and reversing thenormalize_python_keywords transform. We also filter out theArray.keys method.
functionjs_dir(jsobj){letresult=[];letorig=jsobj;do{letkeys=Object.getOwnPropertyNames(jsobj);result.push(...keys);}while((jsobj=Object.getPrototypeOf(jsobj)));// Filter out numbersresult=result.filter((s)=>{letc=s.charCodeAt(0);returnc<48||c>57;});// Filter out "keys" key from an arrayif(Array.isArray(orig)){result=result.filter((s)=>{returns!=="keys";});}// If the key is a keyword followed by 0 or more underscores,// add an extra underscore to reverse the transformation applied by// normalize_python_keywords().result=result.map((word)=>iskeyword(word.replace(/_*$/,""))?word+"_":word,);returnresult;};
classJSProxy:def__getattribute__(self,attr):try:returnobject.__getattribute__(self,attr)exceptAttributeError:passifattr=="keys"andArray.isArray(self):raiseAttributeError(attr)attr=normalize_python_keywords(attr)js_getattr=run_js(""" (jsobj, attr) => jsobj[attr] """)js_hasattr=run_js(""" (jsobj, attr) => attr in jsobj """)result=js_getattr(self,attr)ifisjsfunction(result):result=result.__get__(self)ifresultisNoneandnotjs_hasattr(self,attr):raiseAttributeError(attr)returnresultdef__setattr__(self,attr,value):ifattrin["__loader__","__name__","__package__","__path__","__spec__"]:returnobject.__setattr__(self,attr,value)attr=normalize_python_keywords(attr)js_setattr=run_js(""" (jsobj, attr) => { jsobj[attr] = value; } """)js_setattr(self,attr,value)def__delattr__(self,attr):ifattrin["__loader__","__name__","__package__","__path__","__spec__"]:returnobject.__delattr__(self,attr)attr=normalize_python_keywords(attr)js_delattr=run_js(""" (jsobj, attr) => { delete jsobj[attr]; } """)js_delattr(self,attr)def__dir__(self):returnobject.__dir__(self)+js_dir(self)def__eq__(self,other):ifnotisinstance(other,JSProxy):returnFalsejs_eq=run_js("(x, y) => x === y")returnjs_eq(self,other)def__ne__(self,other):ifnotisinstance(other,JSProxy):returnTruejs_neq=run_js("(x, y) => x !== y")returnjs_neq(self,other)def__repr__(self):js_repr=run_js("x => x.toString()")returnjs_repr(self)def__bool__(self):returnjs_bool(self)@propertydefjs_id(self):""" This returns an integer with the property that jsproxy1 == jsproxy2 if and only if jsproxy1.js_id == jsproxy2.js_id. There is no way to express the implementation in pseudocode. """raiseNotImplementedErrordefas_py_json(self):""" This is actually a mixin method. We leave it out if any of the flags IS_CALLABLE, IS_DOUBLE_PROXY, IS_ERROR, or IS_ITERATOR is set. """flags=self._js_type_flagsif(flags&(IS_ARRAY|IS_ARRAY_LIKE)):flags|=IS_PY_JSON_SEQUENCEelse:flags|=IS_PY_JSON_DICTreturncreate_jsproxy_with_flags(flags,self,self.jsthis)defto_py(self,*,depth=-1,default_converter=None):""" See section on deep conversions. """...defobject_entries(self):js_object_entries=run_js("x => Object.entries(x)")returnjs_object_entries(self)defobject_keys(self):js_object_keys=run_js("x => Object.keys(x)")returnjs_object_keys(self)defobject_values(self):js_object_values=run_js("x => Object.values(x)")returnjs_object_values(self)defto_weakref(self):js_weakref=run_js("x => new WeakRef(x)")returnjs_weakref(self)
We need the following function which calls theas_py_json() method onvalue if it is present:
defmaybe_as_py_json(value):if(isinstance(value,JSProxy)andhasattr(value,as_py_json)):returnvalue.as_py_json()returnvalue
We need the helper functiongetTypeTag:
functiongetTypeTag(x){try{returnObject.prototype.toString.call(x);}catch(e){// Catch and ignore errorsreturn"";}}
We use the following function to determine which flags to set:
functioncompute_type_flags(obj,is_py_json){lettype_flags=0;consttypeTag=getTypeTag(obj);consthasLength=isArray||(hasProperty(obj,"length")&&typeofobj!=="function");SET_FLAG_IF_HAS_METHOD(HAS_GET,"get");SET_FLAG_IF_HAS_METHOD(HAS_SET,"set");SET_FLAG_IF_HAS_METHOD(HAS_HAS,"has");SET_FLAG_IF_HAS_METHOD(HAS_INCLUDES,"includes");SET_FLAG_IF(HAS_LENGTH,hasProperty(obj,"size")||hasLength);SET_FLAG_IF_HAS_METHOD(HAS_DISPOSE,Symbol.dispose);SET_FLAG_IF(IS_CALLABLE,typeofobj==="function");SET_FLAG_IF(IS_ARRAY,Array.isArray(obj));SET_FLAG_IF(IS_ARRAY_LIKE,!isArray&&hasLength&&(type_flags&IS_ITERABLE));SET_FLAG_IF(IS_DOUBLE_PROXY,isPyProxy(obj));SET_FLAG_IF(IS_GENERATOR,typeTag==="[object Generator]");SET_FLAG_IF_HAS_METHOD(IS_ITERABLE,Symbol.iterator);SET_FLAG_IF(IS_ERROR,hasProperty(obj,"name")&&hasProperty(obj,"message")&&(hasProperty(obj,"stack")||constructorName==="DOMException")&&!(type_flags&IS_CALLABLE));if(is_py_json&&type_flags&(IS_ARRAY|IS_ARRAY_LIKE)){type_flags|=IS_PY_JSON_SEQUENCE;}elseif(is_py_json&&!(type_flags&(IS_DOUBLE_PROXY|IS_ITERATOR|IS_CALLABLE|IS_ERROR))){type_flags|=IS_PY_JSON_DICT;}constmapping_flags=HAS_GET|HAS_LENGTH|IS_ITERABLE;constmutable_mapping_flags=mapping_flags|HAS_SET;SET_FLAG_IF(IS_MAPPING,type_flags&(mapping_flags===mapping_flags));SET_FLAG_IF(IS_MUTABLE_MAPPING,type_flags&(mutable_mapping_flags===mutable_mapping_flags),);SET_FLAG_IF(IS_MAPPING,type_flags&IS_PY_JSON_DICT);SET_FLAG_IF(IS_MUTABLE_MAPPING,type_flags&IS_PY_JSON_DICT);returntype_flags;}
HAS_GET MixinIf a JavaScriptget() method is present, we define__getitem__ asfollows. If ahas() method is also present, we’ll use it to decide whetheranundefined return value should be treated as a key error or asNone.If nohas() method is present,undefined is treated asNone.
functionjs_get(jsobj,item){constresult=jsobj.get(item);if(result!==undefined){returnresult;}if(hasMethod(obj,"has")&&!obj.has(key)){thrownewPythonKeyError(item);}returnundefined;}
classJSProxyHasGetMixin:def__getitem__(self,item):result=js_get(self,item)ifself._js_type_flags&IS_PY_JSON_DICT:result=maybe_as_py_json(result)returnresult
HAS_SET MixinIf aset() method is present, we assume adelete() method is alsopresent and define__setitem__ and__delitem__ as follows:
classJSProxyHasSetMixin:def__setitem__(self,item,value):js_set=run_js(""" (jsobj, item, value) => { jsobj.set(item, value); } """)js_set(self,item,value)def__delitem__(self,item,value):js_delete=run_js(""" (jsobj, item) => { jsobj.delete(item); } """)js_delete(self,item)
HAS_HAS MixinclassJSProxyHasHasMixin:def__contains__(self,item):js_has=run_js(""" (jsobj, item) => jsobj.has(item); """)returnjs_has(self,item)
HAS_INCLUDES MixinclassJSProxyHasIncludesMixin:def__contains__(self,item):js_includes=run_js(""" (jsobj, item) => jsobj.includes(item); """)returnjs_includes(self,item)
HAS_LENGTH MixinWe prefer to use thesize attribute if present and a number and if not fallback to returning thelength. If a JavaScript error is raised when lookingup either field, we allow it to propagate into Python as aJavaScriptException.
classJSProxyHasLengthMixin:def__len__(self,item):js_len=run_js(""" (jsobj) => { const size = val.size; if (typeof size === "number") { return size; } return val.length } """)result=js_len(self)ifnotisinstance(result,int):raiseTypeError("object does not have a valid length")ifresult<0:raiseValueError("length of object is negative")returnresult
HAS_DISPOSE MixinThis makes theJSProxy into a context manager where__enter__ is a no-opand__exit__ calls the[Symbol.dispose]() method.
classJSProxyContextManagerMixin:def__enter__(self):returnselfdef__exit__(self,type,value,traceback):js_symbol_dispose=run_js(""" (jsobj) => jsobj[Symbol.dispose]() """)js_symbol_dispose(self)
IS_ARRAY Mixinfunctionjs_array_slice(jsobj,length,start,stop,step){letresult;if(step===1){result=obj.slice(start,stop);}else{result=Array.from({length},(_,i)=>obj[start+i*step]);}returnresult;}// we also use this for deletion by setting values to Nonefunctionjs_array_slice_assign(obj,slicelength,start,stop,step,values){if(step===1){obj.splice(start,slicelength,...(values??[]));return;}if(values!==undefined){for(leti=0;i<slicelength;i++){obj.splice(start+i*step,1,values[i]);}}for(leti=slicelength-1;i>=0;i--){obj.splice(start+i*step,1);}}
classJSArrayMixin(MutableSequence,JSProxyHasLengthMixin):def__getitem__(self,index):ifnotisinstance(index,(int,slice)):raiseTypeError("Expected index to be an int or a slice")length=len(self)js_array_get=run_js(""" (jsobj, index) => jsobj[index] """)ifisinstance(index,int):ifindex>=length:raiseIndexError(index)ifindex<-length:raiseIndexError(index)ifindex<0:index+=lengthresult=js_array_get(self,index)ifself._js_type_flags&IS_PY_JSON_SEQUENCE:result=maybe_as_py_json(result)returnresultstart=index.startstop=index.stopstep=index.stepslicelength=PySlice_AdjustIndices(length,&start,&stop,&step)if(slicelength<=0){return_PyJsvArray_New();}result=js_array_slice(self,slicelength,start,stop,step)ifself._js_type_flags&IS_PY_JSON_SEQUENCE:result=result.as_py_json()returnresultdef__setitem__(self,index,value):ifnotisinstance(index,(int,slice)):raiseTypeError("Expected index to be an int or a slice")length=len(self)js_array_set=run_js(""" (jsobj, index, value) => { jsobj[index] = value; } """)ifisinstance(index,int):ifindex>=length:raiseIndexError(index)ifindex<-length:raiseIndexError(index)ifindex<0:index+=lengthresult=js_array_set(self,index,value)returnifnotisinstance(value,Iterable):raiseTypeError("must assign iterable to extended slice")seq=list(value)start=index.startstop=index.stopstep=index.stepslicelength=PySlice_AdjustIndices(length,&start,&stop,&step)ifstep!=1andlen(seq)!=slicelength:raiseTypeError(f"attempted to assign sequence of length{len(seq)} to"f"extended slice of length{slicelength}")ifstep!=1andslicelength==0:returnjs_array_slice_assign(self,slicelength,start,stop,step,seq)def__delitem__(self,index):ifnotisinstance(index,(int,slice)):raiseTypeError("Expected index to be an int or a slice")length=len(self)js_array_delete=run_js(""" (jsobj, index) => { jsobj.splice(index, 1); } """)ifisinstance(index,int):ifindex>=length:raiseIndexError(index)ifindex<-length:raiseIndexError(index)ifindex<0:index+=lengthresult=js_array_delete(self,index)returnstart=index.startstop=index.stopstep=index.stepslicelength=PySlice_AdjustIndices(length,&start,&stop,&step)ifstep!=1andslicelength==0:returnjs_array_slice_assign(self,slicelength,start,stop,step,None)definsert(self,pos,value):ifnotisinstance(pos,int):raiseTypeError("Expected an integer")js_insert=run_js(""" (jsarr, pos, value) => { jsarr.splice(pos, value); } """)js_insert(self,pos,value)
IS_ARRAY_LIKE MixinclassJSArrayLikeMixin(MutableSequence,JSProxyHasLengthMixin):def__getitem__(self,index):ifnotisinstance(index,int):raiseTypeError("Expected index to be an int")JSArrayMixin.__getitem__(self,index)def__setitem__(self,index,value):ifnotisinstance(index,int):raiseTypeError("Expected index to be an int")JSArrayMixin.__setitem__(self,index,value)def__delitem__(self,index):ifnotisinstance(index,int):raiseTypeError("Expected index to be an int")JSArrayMixin.__delitem__(self,index,value)
IS_CALLABLE MixinWe already gave more accurate C code for calling aJSCallable. See inparticular the definition ofJSMethod_ConvertArgs() given there.
classJSCallableMixin:def__get__(self,obj):"""Return a new jsproxy bound to jsthis with the same JS object"""returncreate_jsproxy(self,jsthis=obj)def__call__(self,*args,**kwargs):"""See the description of JSMethod_Vectorcall"""defnew(self,*args,**kwargs):pyproxies=[]jsargs=JSMethod_ConvertArgs(args,kwargs,pyproxies)do_construct=run_js(""" (jsfunc, jsargs) => Reflect.construct(jsfunc, jsargs) """)result=do_construct(self,jsargs)msg=("This borrowed proxy was automatically destroyed ""at the end of a function call.")forpxinpyproxies:px.destroy(msg)returnresult
IS_ERROR MixinIn this case, we inherit from bothException andJSProxy. We also makesure that the resulting class is pickleable.
IS_ITERABLE MixinIf the iterable has theIS_PY_JSON_DICT flag set, we iterate over the objectkeys. Otherwise, callobj[Symbol.iterator](). If eitherIS_PY_JSON_SEQUENCE orIS_PY_JSON_DICT, we callmaybe_as_py_json onthe iteration results.
defwrap_with_maybe_as_py_json(it):try:whileval:=it.next()yieldmaybe_as_py_json(val)exceptStopIteration(result):returnmaybe_as_py_json(result)classJSIterableMixin:def__iter__(self):pyjson=self._js_type_flags&(IS_PY_JSON_SEQUENCE|IS_PY_JSON_DICT)pyjson_dict=self._js_type_flags&IS_PY_JSON_DICTjs_get_iter=run_js(""" (obj) => obj[Symbol.iterator]() """)ifpyjson_dict:result=iter(self.object_keys())else:result=js_get_iter(self)ifpyjson:result=wrap_with_maybe_as_py_json(result)returnresult
IS_ITERATOR MixinThe JavaScriptnext method returns anIteratorResult which has adone field and avalue field. Ifdone istrue, we have to raiseaStopIteration exception to convert to the Python iterator protocol.
classJSIteratorMixin:def__iter__(self):returnselfdefsend(self,arg):js_next=run_js(""" (obj, arg) => obj.next(arg) """)it_result=js_next(self,arg)value=it_result.valueifit_result.done:raiseStopIteration(value)returnvaluedef__next__(self):returnself.send(None)
IS_GENERATOR MixinPython generators have aclose() method which takes no arguments instead ofareturn() method. We also have to translategen.throw(GeneratorExit)intojsgen.return_(). It is possible to calljsgen.return_(val) directlyif there is a need to return a specific value.
classJSGeneratorMixin(JSIteratorMixin):defthrow(self,exc):ifisinstance(exc,GeneratorExit):js_throw=run_js(""" (obj, exc) => obj.return() """)else:js_throw=run_js(""" (obj, exc) => obj.throw(exc) """)it_result=js_throw(self,exc)# if the error wasn't caught it will get raised back out.# now handle the case where the error got caught.value=it_result.valueifself._js_type_flags&IS_PY_JSON_SEQUENCE:value=maybe_as_py_json(value)ifit_result.done:raiseStopIteration(value)returnvaluedefclose(self):self.throw(GeneratorExit)
IS_MAPPING MixinIf theIS_MAPPING flag is set, we implement all of theMapping methods.We only set this flag when there are enough other flags set that the abstractMapping methods are defined. We use the default implementations for all themixin methods.
IS_MUTABLE_MAPPING MixinIf theIS_MUTABLE_MAPPING flag is set, we implement all of theMutableMapping methods. We only set this flag when there are enough otherflags set that the abstractMutableMapping methods are defined. We use thedefault implementations for all the mixin methods.
IS_PY_JSON_SEQUENCE MixinThis flag only ever appears withIS_ARRAY. It changes the behavior ofJSArray.__getitem__ to applymaybe_as_py_json() to the result.
IS_PY_JSON_DICT MixinclassJSPyJsonDictMixin(MutableMapping):def__getitem__(self,key):ifnotisinstance(key,str):raiseKeyError(key)js_get=run_js(""" (jsobj, key) => jsobj[key] """)result=js_get(self,key)ifresultisNoneandnotkeyinself:raiseKeyError(key)returnmaybe_as_py_json(result)def__setitem__(self,key,value):ifnotisinstance(key,str):raiseTypeError("only keys of type string are supported")js_set=run_js(""" (jsobj, key, value) => { jsobj[key] = value; } """)js_set(self,key,value)def__delitem__(self,key):ifnotisinstance(key,str):raiseTypeError("only keys of type string are supported")ifnotkeyinself:raiseKeyError(key)js_delete=run_js(""" (jsobj, key) => { delete jsobj[key]; } """)js_delete(self,key)def__contains__(self,key):ifnotisinstance(key,str):returnFalsejs_contains=run_js(""" (jsobj, key) => key in jsobj """)returnjs_contains(self,key)def__len__(self):returnsum(1for_inself)def__iter__(self):# defined by IS_ITERABLE mixin, see implementation there.
IS_DOUBLE_PROXY MixinIn this case the object is aJSProxy of aPyProxy. We add an extraunwrap() method that returns the inner Python object.
We define 12 mixins that a Python object may support that affect the type of thePyProxy we make from it.
HAS_GET__getitem__ method. Ifpresent, we use it to implement aget() method on thePyProxy.HAS_SET__setitem__ method. Ifpresent, we use it to implement aset() method on thePyProxy.HAS_CONTAINS__contains__ method. Ifpresent, we use it to implement ahas() method on thePyProxy.HAS_LENGTH__len__ method. If present,we use it to implement alength getter on thePyProxy.IS_CALLABLE__call__ method. If present,we make thePyProxy callable.IS_DICTdict. If present,we will make propertypyproxy.some_property fall back topyobj.__getitem__("some_property") ifgetattr(pyobj,"some_property")raises anAttributeError.IS_GENERATORcollections.abc.Generator. If present, we make thePyProxy implementthe methods of a JavaScript generator.IS_ITERABLE__iter__ method. If present,we use it to implement a[Symbol.iterator] method on thePyProxy.IS_ITERATOR__next__ method. If present,we use it to implement anext() method on thePyProxy.IS_SEQUENCEcollections.abc.Sequence. If it is present, we use it to implement allof theArray.prototype methods that don’t mutate on thePyProxy.IS_MUTABLE_SEQUENCEcollections.abc.MutableSequence. If it is present, we use it to implementallArray.prototype methods on thePyProxy.IS_JS_JSON_DICTasJsJson() method is used on a dictionary. Ifthis flag is set, property access on thePyProxy will _only_ look atvalues from__getitem__ and not at attributes on the Python object. Wealso will callasJsJson() on the result of indexing or iteratingthePyProxy.IS_JS_JSON_SEQUENCEasJsJson() is used on aSequence. If thisflag is set, we will callasJsJson() on the result of indexing oriterating thePyProxy.APyProxy is made up of a mixture of a JavaScript class and a collection ofES6Proxy handlers. Depending on which flags are present, we construct ourclass out of an appropriate collection of mixins and an appropriate choice ofhandlers.
When aPyProxy is created, we increment the reference count of the wrappedPython object. When aPyProxy is destroyed, we decrement the reference countand mark it as destroyed. As a result, if we attempt to do anything with thePyProxy, we will call_Py_js2python() on it and an error will be thrown.
PyProxyGiven a collection of type flags, we use the following function to generate thePyProxy class:
letpyproxyClassMap=newMap();functiongetPyProxyClass(flags:number){letresult=pyproxyClassMap.get(flags);if(result){returnresult;}letdescriptors:any={};constFLAG_MIXIN_PAIRS:[number,any][]=[[HAS_CONTAINS,PyContainsMixin],// ... other flag mixin pairs[IS_MUTABLE_SEQUENCE,PyMutableSequenceMixin],];for(let[feature_flag,methods]ofFLAG_MIXIN_PAIRS){if(flags&feature_flag){Object.assign(descriptors,Object.getOwnPropertyDescriptors(methods.prototype),);}}// Use base constructor (just throws an error if construction is attempted).descriptors.constructor=Object.getOwnPropertyDescriptor(PyProxyProto,"constructor",);// $$flags static fieldObject.assign(descriptors,Object.getOwnPropertyDescriptors({$$flags:flags}),);// We either inherit PyProxyFunction as the base class if we're callable or// from PyProxy if we're not.constsuperProto=flags&IS_CALLABLE?PyProxyFunctionProto:PyProxyProto;constsubProto=Object.create(superProto,descriptors);functionNewPyProxyClass(){}NewPyProxyClass.prototype=subProto;pyproxyClassMap.set(flags,NewPyProxyClass);returnNewPyProxyClass;}
To create aPyProxy we also need to be able to get the appropriate handlers:
functiongetPyProxyHandlers(flags){if(flags&IS_JS_JSON_DICT){returnPyProxyJsJsonDictHandlers;}if(flags&IS_DICT){returnPyProxyDictHandlers;}if(flags&IS_SEQUENCE){returnPyProxySequenceHandlers;}returnPyProxyHandlers;}
We use the following function to create the target object for the ES6 proxy:
functioncreateTarget(flags){constpyproxyClass=getPyProxyClass(flags);if(!(flags&IS_CALLABLE)){returnObject.create(cls.prototype);}// In this case we are effectively subclassing Function in order to ensure// that the proxy is callable. With a Content Security Protocol that doesn't// allow unsafe-eval, we can't invoke the Function constructor directly. So// instead we create a function in the universally allowed way and then use// `setPrototypeOf`. The documentation for `setPrototypeOf` says to use// `Object.create` or `Reflect.construct` instead for performance reasons// but neither of those work here.consttarget=function(){};Object.setPrototypeOf(target,cls.prototype);// Remove undesirable properties added by Function constructor. Note: we// can't remove "arguments" or "caller" because they are not configurable// and not writabledeletetarget.length;deletetarget.name;// prototype isn't configurable so we can't delete it but it is writable.target.prototype=undefined;returntarget;}
createPyProxy takes the following options:
constpyproxyAttrsSymbol=Symbol("pyproxy.attrs");functioncreatePyProxy(pyObjectPtr:number,{flags,props,shared,gcRegister,}){if(gcRegister===undefined){// register by defaultgcRegister=true;}// See the section "Determining which flags to set" for the definition of// get_pyproxy_flagsconstpythonGetFlags=makePythonFunction("get_pyproxy_flags");flags??=pythonGetFlags(pyObjectPtr);consttarget=createTarget(flags);consthandlers=getPyProxyHandlers(flags);constproxy=newProxy(target,handlers);props=Object.assign({isBound:false,captureThis:false,boundArgs:[],roundtrip:false},props,);// If shared was passed the new PyProxy will have a shared lifetime// with some other PyProxy.// This happens in asJsJson(), bind(), and captureThis().// It specifically does not happen in copy()if(!shared){shared={pyObjectPtr,destroyed_msg:undefined,gcRegistered:false,};_Py_IncRef(pyObjectPtr);if(gcRegister){gcRegisterPyProxy(shared);}}target[pyproxyAttrsSymbol]={shared,props};returnproxy;}
PyProxy Base ClassThe default handlers are as follows:
functionfilteredHasKey(jsobj,jskey,filterProto){letresult=jskeyinjsobj;if(jsobjinstanceofFunction){// If we are a PyProxy of a callable we have to subclass function so that if// someone feature detects callables with `instanceof Function` it works// correctly. But the callable might have attributes `name` and `length` and// we don't want to shadow them with the values from `Function.prototype`.result&&=!(["name","length","caller","arguments"].includes(jskey)||// we are required by JS law to return `true` for `"prototype" in pycallable`// but we are allowed to return the value of `getattr(pycallable, "prototype")`.// So we filter prototype out of the "get" trap but not out of the "has" trap(filterProto&&jskey==="prototype"));}returnresult;}constPyProxyHandlers={isExtensible(){returntrue;},has(jsobj,jskey){// Must report "prototype" in proxy when we are callable.// (We can return the wrong value from "get" handler though.)if(filteredHasKey(jsobj,jskey,false)){returntrue;}// hasattr will crash if given a Symbol.if(typeofjskey==="symbol"){returnfalse;}if(jskey.startsWith("$")){jskey=jskey.slice(1);}constpythonHasAttr=makePythonFunction("hasattr");returnpythonHasAttr(jsobj,jskey);},get(jsobj,jskey){// Preference order:// 1. stuff from JavaScript// 2. the result of Python getattr// pythonGetAttr will crash if given a Symbol.if(typeofjskey==="symbol"||filteredHasKey(jsobj,jskey,true)){returnReflect.get(jsobj,jskey);}if(jskey.startsWith("$")){jskey=jskey.slice(1);}// 2. The result of getattrconstpythonGetAttr=makePythonFunction("getattr");returnpythonGetAttr(jsobj,jskey);},set(jsobj,jskey,jsval){letdescr=Object.getOwnPropertyDescriptor(jsobj,jskey);if(descr&&!descr.writable&&!descr.set){returnfalse;}// pythonSetAttr will crash if given a Symbol.if(typeofjskey==="symbol"||filteredHasKey(jsobj,jskey,true)){returnReflect.set(jsobj,jskey,jsval);}if(jskey.startsWith("$")){jskey=jskey.slice(1);}constpythonSetAttr=makePythonFunction("setattr");pythonSetAttr(jsobj,jskey,jsval);returntrue;},deleteProperty(jsobj,jskey:string|symbol):boolean{letdescr=Object.getOwnPropertyDescriptor(jsobj,jskey);if(descr&&!descr.configurable){// Must return "false" if "jskey" is a nonconfigurable own property.// Strict mode JS will throw an error here saying that the property cannot// be deleted.returnfalse;}if(typeofjskey==="symbol"||filteredHasKey(jsobj,jskey,true)){returnReflect.deleteProperty(jsobj,jskey);}if(jskey.startsWith("$")){jskey=jskey.slice(1);}constpythonDelAttr=makePythonFunction("delattr");pythonDelAttr(jsobj,jskey);returntrue;},ownKeys(jsobj){constpythonDir=makePythonFunction("dir");constresult=pythonDir(jsobj).toJs();result.push(...Reflect.ownKeys(jsobj));returnresult;},apply(jsobj:PyProxy&Function,jsthis:any,jsargs:any):any{returnjsobj.apply(jsthis,jsargs);},};
And the base class has the following methods:
classPyProxy{constructor(){thrownewTypeError("PyProxy is not a constructor");}get[Symbol.toStringTag](){return"PyProxy";}static[Symbol.hasInstance](obj:any):objisPyProxy{return[PyProxy,PyProxyFunction].some((cls)=>Function.prototype[Symbol.hasInstance].call(cls,obj),);}gettype(){constpythonType=makePythonFunction(` def python_type(obj): ty = type(obj) if ty.__module__ in ['builtins', 'main']: return ty.__name__ return ty.__module__ + "." + ty.__name__ `);returnpythonType(this);}toString(){constpythonStr=makePythonFunction("str");returnpythonStr(this);}destroy(options){const{shared}=proxy[pyproxyAttrsSymbol];if(!shared.pyObjectPtr){// already destroyedreturn;}shared.pyObjectPtr=0;shared.destroyed_msg=options.message??"Object has already been destroyed";_Py_DecRef(shared.pyObjectPtr);}[Symbol.dispose](){this.destroy();}copy(){const{shared,props}=proxy[pyproxyAttrsSymbol];// Don't pass shared as an option since we want this new PyProxy to// have a distinct lifetime from the one we are copying.returncreatePyProxy(shared.pyObjectPtr,{flags:this.$$flags,props:attrs.props,});}toJs(options){// See the definition of to_js in "Deep conversions".}}
We separate this out into a componentget_type_flags that computes flagswhich only depends on the type and a component that also depends on whether thePyProxy has beenJsJson
defget_type_flags(ty):fromcollections.abcimportGenerator,MutableSequence,Sequenceflags=0ifhasattr(ty,"__len__"):flags|=HAS_LENGTHifhasattr(ty,"__getitem__"):flags|=HAS_GETifhasattr(ty,"__setitem__"):flags|=HAS_SETifhasattr(ty,"__contains__"):flags|=HAS_CONTAINSiftyisdict:# Currently we don't set this on subclasses.flags|=IS_DICTifhasattr(ty,"__call__"):flags|=IS_CALLABLEifhasattr(ty,"__iter__"):flags|=IS_ITERABLEifhasattr(ty,"__next__"):flags|=IS_ITERATORifissubclass(ty,Generator):flags|=IS_GENERATORifissubclass(ty,Sequence):flags|=IS_SEQUENCEifissubclass(ty,MutableSequence):flags|=IS_MUTABLE_SEQUENCEreturnflagsdefget_pyproxy_flags(obj,is_js_json):flags=get_type_flags(type(obj))ifnotis_js_json:returnflagsifflags&IS_SEQUENCE:flags|=IS_JS_JSON_SEQUENCEelifflags&HAS_GET:flags|=IS_JS_JSON_DICTreturnflags
HAS_GET MixinconstpythonGetItem=makePythonFunction(` def getitem(obj, key): return obj[key]`);classPyProxyGetItemMixin{get(key){letresult=pythonGetItem(this,key);constisJsJson=!!(this.$$flags&(IS_JS_JSON_DICT|IS_JS_JSON_SEQUENCE));if(isJsJson&&result.asJsJson){result=result.asJsJson();}returnresult;}asJsJson(){constflags=this.$$flags|IS_JS_JSON_DICT;const{shared,props}=this[pyproxyAttrsSymbol];// Note: The PyProxy created here has the same lifetime as the PyProxy it is// created from. Destroying either destroys both.returncreatePyProxy(shared.ptr,{flags,shared,props});}}
HAS_SET MixinclassPyProxySetItemMixin{set(key,value){constpythonSetItem=makePythonFunction(` def setitem(obj, key, value): obj[key] = value `);pythonSetItem(this,key,value);}delete(key){constpythonDelItem=makePythonFunction(` def delitem(obj, key): del obj[key] `);pythonDelItem(this,key);}}
HAS_CONTAINS MixinconstpythonHasItem=makePythonFunction(` def hasitem(obj, key): return key in obj`);classPyContainsMixin{has(key){returnpythonHasItem(this,key);}}
HAS_LENGTH MixinconstpythonLength=makePythonFunction("len");classPyLengthMixin{getlength():number{returnpythonLength(this);}}
IS_CALLABLE MixinWe have to make a custom prototype and class so that this inherits from bothPyProxy andFunction:
constPyProxyFunctionProto=Object.create(Function.prototype,Object.getOwnPropertyDescriptors(PyProxy.prototype),);functionPyProxyFunction(){}PyProxyFunction.prototype=PyProxyFunctionProto;
We use the following helper function which insertsthis as the firstargument ifcaptureThis istrue and adds any bound arguments.
function_adjustArgs(pyproxy,jsthis,jsargs){const{props}=this[pyproxyAttrsSymbol];const{captureThis,boundArgs,boundThis,isBound}=props;if(captureThis){if(isBound){return[boundThis].concat(boundArgs,jsargs);}else{return[jsthis].concat(jsargs);}}if(isBound){returnboundArgs.concat(jsargs);}returnjsargs;}
Then we implement the following methods.apply(),call(), andbind()are methods fromFunction.prototype.callKwargs() andcaptureThis()are special toPyProxy
exportclassPyCallableMixin{apply(thisArg,jsargs){// Convert jsargs to an array using ordinary .apply in order to match the// behavior of .apply very accurately.jsargs=function(...args){returnargs;}.apply(undefined,jsargs);jsargs=_adjustArgs(this,thisArg,jsargs);constpyObjectPtr=this[pyproxyAttrsSymbol].shared.pyObjectPtr;returncallPyObjectKwargs(pyObjectPtr,jsargs,{});}call(thisArg,...jsargs){jsargs=_adjustArgs(this,thisArg,jsargs);constpyObjectPtr=this[pyproxyAttrsSymbol].shared.pyObjectPtr;returncallPyObjectKwargs(pyObjectPtr,jsargs,{});}/** * Call the function with keyword arguments. The last argument must be an * object with the keyword arguments. */callKwargs(...jsargs){jsargs=_adjustArgs(this,thisArg,jsargs);if(jsargs.length===0){thrownewTypeError("callKwargs requires at least one argument (the kwargs object)",);}letkwargs=jsargs.pop();if(kwargs.constructor!==undefined&&kwargs.constructor.name!=="Object"){thrownewTypeError("kwargs argument is not an object");}constpyObjectPtr=this[pyproxyAttrsSymbol].shared.pyObjectPtr;returncallPyObjectKwargs(pyObjectPtr,jsargs,kwargs);}/** * This is our implementation of Function.prototype.bind(). */bind(thisArg,...jsargs){let{shared,props}=this[pyproxyAttrsSymbol];const{boundArgs:boundArgsOld,boundThis:boundThisOld,isBound}=props;letboundThis=thisArg;if(isBound){boundThis=boundThisOld;}constboundArgs=boundArgsOld.concat(jsargs);props=Object.assign({},props,{boundArgs,isBound:true,boundThis,});returncreatePyProxy(shared.ptr,{shared,flags:this.$$flags,props,});}/** * This method makes a new PyProxy where ``this`` is passed as the * first argument to the Python function. The new PyProxy has the * same lifetime as the original. */captureThis(){let{props,shared}=this[pyproxyAttrsSymbol];props=Object.assign({},props,{captureThis:true,});returncreatePyProxy(shared.ptr,{shared,flags:this.$$flags,props,});}}
IS_DICT MixinTheIS_DICT mixin does not include any extra methods but it uses a specialset of handlers. These handlers are a hybrid between the normal handlers and theJS_JSON_DICT handlers. We first check whetherhasattr(d,property) andif so returnd.property. If not, we returnd.get(property,None). Theother methods all work similarly. See theIS_JS_JSON_DICT flag for thedefinitions of those handlers.
constPyProxyDictHandlers={isExtensible():boolean{returntrue;},has(jsobj:PyProxy,jskey:string|symbol):boolean{if(PyProxyHandlers.has(jsobj,jskey)){returntrue;}returnPyProxyJsJsonDictHandlers.has(jsobj,jskey);},get(jsobj:PyProxy,jskey:string|symbol):any{letresult=PyProxyHandlers.get(jsobj,jskey);if(result!==undefined||PyProxyHandlers.has(jsobj,jskey)){returnresult;}returnPyProxyJsJsonDictHandlers.get(jsobj,jskey);},set(jsobj:PyProxy,jskey:string|symbol,jsval:any):boolean{if(PyProxyHandlers.has(jsobj,jskey)){returnPyProxyHandlers.set(jsobj,jskey,jsval);}returnPyProxyJsJsonDictHandlers.set(jsobj,jskey,jsval);},deleteProperty(jsobj:PyProxy,jskey:string|symbol):boolean{if(PyProxyHandlers.has(jsobj,jskey)){returnPyProxyHandlers.deleteProperty(jsobj,jskey);}returnPyProxyJsJsonDictHandlers.deleteProperty(jsobj,jskey);},getOwnPropertyDescriptor(jsobj:PyProxy,prop:any){return(Reflect.getOwnPropertyDescriptor(jsobj,prop)??PyProxyJsJsonDictHandlers.getOwnPropertyDescriptor(jsobj,prop));},ownKeys(jsobj:PyProxy):(string|symbol)[]{constresult=[...PyProxyHandlers.ownKeys(jsobj),...PyProxyJsJsonDictHandlers.ownKeys(jsobj)];// deduplicatereturnArray.from(newSet(result));},};
IS_ITERABLE MixinconstpythonNext=makePythonFunction("next");constgetStopIterationValue=makePythonFunction(` def get_stop_iteration_value(): import sys err = sys.last_value return err.value`);function*iterHelper(iter,isJsJson){try{while(true){letitem=pythonNext(iter);if(isJsJson&&item.asJsJson){item=item.asJsJson();}yielditem;}}catch(e){if(e.type==="StopIteration"){returngetStopIterationValue();}throwe;}}constpythonIter=makePythonFunction("iter");classPyIterableMixin{[Symbol.iterator](){constisJsJson=!!(this.$$flags&(IS_JS_JSON_DICT|IS_JS_JSON_SEQUENCE));returniterHelper(pythonIter(this),isJsJson);}}
IS_ITERATOR MixinconstpythonSend=makePythonFunction(` def python_send(it, val): return gen.send(val)`);classPyIteratorMixin{next(x){try{constresult=pythonSend(this,x);return{done:false,value:result};}catch(e){if(e.type==="StopIteration"){constresult=getStopIterationValue();return{done:true,value:result};}throwe;}}}
IS_GENERATOR MixinconstpythonThrow=makePythonFunction(` def python_throw(gen, val): return gen.throw(val)`);constpythonClose=makePythonFunction(` def python_close(gen): return gen.close()`);classPyGeneratorMixinextendsPyIteratorMixin{throw(exc){try{constresult=pythonThrow(this,exc);return{done:false,value:result};}catch(e){if(e.type==="StopIteration"){constresult=getStopIterationValue();return{done:true,value:result};}throwe;}}return(value){pythonClose(this);return{done:true,value}}}
IS_SEQUENCE MixinWe define all of theArray.prototype methods that don’t mutate the sequenceonPySequenceMixin. For most of them, theArray prototype method workswithout changes. All of these we define with boilerplate of the form:
[methodName](...args){returnArray.prototype[methodName].call(this,...args)}
These includejoin,slice,indexOf,lastIndexOf,forEach,map,filter,some,every,reduce,reduceRight,at,concat,includes,entries,keys,values,find, andfindIndex. Other than these boilerplate methods, the remaining attributes onPySequenceMixin are as follows.
classPySequenceMixin{get[Symbol.isConcatSpreadable](){returntrue;}toJSON(){returnArray.from(this);}asJsJson(){constflags=this.$$flags|IS_JS_JSON_SEQUENCE;const{shared,props}=this[pyproxyAttrsSymbol];// Note: Because we pass shared down, the PyProxy created here has// the same lifetime as the PyProxy it is created from. Destroying// either destroys both.returncreatePyProxy(shared.ptr,{flags,shared,props});}// ... boilerplate methods}
Instead of the default proxy handlers, we use the following handlers forsequences. We don’t
constPyProxySequenceHandlers={isExtensible(){returntrue;},has(jsobj,jskey){if(typeofjskey==="string"&&/^[0-9]+$/.test(jskey)){// Note: if the number was negative it didn't match the patternreturnNumber(jskey)<jsobj.length;}returnPyProxyHandlers.has(jsobj,jskey);},get(jsobj,jskey){if(jskey==="length"){returnjsobj.length;}if(typeofjskey==="string"&&/^[0-9]+$/.test(jskey)){try{returnPyProxyGetItemMixin.prototype.get.call(jsobj,Number(jskey));}catch(e){if(isPythonError(e)&&e.type=="IndexError"){returnundefined;}throwe;}}returnPyProxyHandlers.get(jsobj,jskey);},set(jsobj:PyProxy,jskey:any,jsval:any):boolean{if(typeofjskey==="string"&&/^[0-9]+$/.test(jskey)){try{PyProxySetItemMixin.prototype.set.call(jsobj,Number(jskey),jsval);returntrue;}catch(e){if(isPythonError(e)&&e.type=="IndexError"){returnfalse;}throwe;}}returnPyProxyHandlers.set(jsobj,jskey,jsval);},deleteProperty(jsobj:PyProxy,jskey:any):boolean{if(typeofjskey==="string"&&/^[0-9]+$/.test(jskey)){try{PyProxySetItemMixin.prototype.delete.call(jsobj,Number(jskey));returntrue;}catch(e){if(isPythonError(e)&&e.type=="IndexError"){returnfalse;}throwe;}}returnPyProxyHandlers.deleteProperty(jsobj,jskey);},ownKeys(jsobj:PyProxy):(string|symbol)[]{constresult=PyProxyHandlers.ownKeys(jsobj);result.push(...Array.from({length:jsobj.length},(_,k)=>k.toString()),);result.push("length");returnresult;},};
IS_MUTABLE_SEQUENCE MixinThis adds some additionalArray methods that mutate the sequence.
classPyMutableSequenceMixin{reverse(){// Same as the Python reverse method except it returns this instead of undefinedthis.$reverse();returnthis;}push(...elts:any[]){for(consteltofelts){this.append(elt);}returnthis.length;}splice(start,deleteCount,...items){if(deleteCount===undefined){// Max signed sizedeleteCount=(1<<31)-1;}letstop=start+deleteCount;if(stop>this.length){stop=this.length;}constpythonSplice=makePythonFunction(` def splice(array, start, stop, items): from jstypes.ffi import to_js result = to_js(array[start:stop], depth=1) array[start:stop] = items return result `);returnpythonSplice(this,start,stop,items);}pop(){constpythonPop=makePythonFunction(` def pop(array): return array.pop() `);returnpythonPop(this);}shift(){constpythonShift=makePythonFunction(` def pop(array): return array.pop(0) `);returnpythonShift(this);}unshift(...elts){elts.forEach((elt,idx)=>{this.insert(idx,elt);});returnthis.length;}// Boilerplate methodscopyWithin(...args):any{Array.prototype.copyWithin.apply(this,args);returnthis;}fill(...args){Array.prototype.fill.apply(this,args);returnthis;}}
IS_JS_JSON_DICT MixinThere are no methods special to theIS_JS_JSON_DICT flag, but we use thefollowing proxy handlers. We prefer to look up a property as an item in thedictionary with two exceptions:
PyProxy itself.$$flags,copy(),constructor,destroy andtoString on thePyProxy.All Python dictionary methods will be shadowed by a key of the same name.
constPyProxyJsJsonDictHandlers={isExtensible():boolean{returntrue;},has(jsobj:PyProxy,jskey:string|symbol):boolean{if(PyContainsMixin.prototype.has.call(jsobj,jskey)){returntrue;}// If it doesn't exist as a string key and it looks like a number,// try again with the numberif(typeofjskey==="string"&&/^-?[0-9]+$/.test(jskey)){returnPyContainsMixin.prototype.has.call(jsobj,Number(jskey));}returnfalse;},get(jsobj,jskey):any{if(typeofjskey==="symbol"||["$$flags","copy","constructor","destroy","toString"].includes(jskey)){returnReflect.get(...arguments);}constresult=PyProxyGetItemMixin.prototype.get.call(jsobj,jskey);if(result!==undefined||PyContainsMixin.prototype.has.call(jsobj,jskey)){returnresult;}if(typeofjskey==="string"&&/^-?[0-9]+$/.test(jskey)){returnPyProxyGetItemMixin.prototype.get.call(jsobj,Number(jskey));}returnReflect.get(...arguments);},set(jsobj,jskey,jsval):boolean{if(typeofjskey==="symbol"){returnfalse;}if(!PyContainsMixin.prototype.has.call(jsobj,jskey)&&typeofjskey==="string"&&/^-?[0-9]+$/.test(jskey)){jskey=Number(jskey);}try{PyProxySetItemMixin.prototype.set.call(jsobj,jskey,jsval);returntrue;}catch(e){if(isPythonError(e)&&e.type==="KeyError"){returnfalse;}throwe;}},deleteProperty(jsobj:PyProxy,jskey:string|symbol|number):boolean{if(typeofjskey==="symbol"){returnfalse;}if(!PyContainsMixin.prototype.has.call(jsobj,jskey)&&typeofjskey==="string"&&/^-?[0-9]+$/.test(jskey)){jskey=Number(jskey);}try{PyProxySetItemMixin.prototype.delete.call(jsobj,jskey);returntrue;}catch(e){if(isPythonError(e)&&e.type==="KeyError"){returnfalse;}throwe;}},getOwnPropertyDescriptor(jsobj:PyProxy,prop:any){if(!PyProxyJsJsonDictHandlers.has(jsobj,prop)){returnundefined;}constvalue=PyProxyJsJsonDictHandlers.get(jsobj,prop);return{configurable:true,enumerable:true,value,writable:true,};},ownKeys(jsobj:PyProxy):(string|symbol)[]{constpythonDictOwnKeys=makePythonFunction(` def dict_own_keys(d): from jstypes.ffi import to_js result = set() for key in d: if isinstance(key, str): result.add(key) elif isinstance(key, (int, float)): result.add(str(key)) return to_js(result) `);returnpythonDictOwnKeys(jsobj);},};
IS_JS_JSON_SEQUENCE MixinThis has no direct impact on the prototype or handlers of the proxy. However,when indexing the list or iterating over the list we will applyasJsJson()to the results.
We defineJSProxy.to_py() to make deep conversions from JavaScript to Pythonandjstypes.ffi.to_js() to make deep conversions from Python to JavaScript.Note that it is not intended that these are inverse functions to each other.
TheJSProxy.to_py() method makes the following conversions:
Array ==>listMap ==>dictSet ==>setObject ==>dict but only if theconstructor is eitherObjectorundefined. Other objects we leave alone.It takes the following optional arguments:
depthdepth=1 allows converting exactly one level.default_converterThe default converter takes three arguments:
jsobjconvertcache_conversionFor example, if we have a JavaScriptPair class and want to convert it to alist, we can use the followingdefault_converter:
defpair_converter(jsobj,convert,cache_conversion):ifjsobj.constructor.name!="Pair":returnjsobjresult=[]cache_conversion(jsobj,result)result.append(convert(jsobj.first))result.append(convert(jsobj.second))returnresult
By first caching the result before making any recursive calls toconvert, weensure that ifjsobj.first has a transitive reference tojsobj, weconvert it correctly.
Complete pseudocode for theto_py method is as follows:
defto_py(jsobj,*,depth=-1,default_converter=None):cache={}returnToPyConverter(depth,default_converter).convert(jsobj)classToPyConverter:def__init__(self,depth,default_converter):self.cache={}self.depth=depthself.default_converter=default_converterdefcache_conversion(self,jsobj,pyobj):self.cache[jsobj.js_id]=pyobjdefconvert(self,jsobj):ifself.depth==0ornotisinstance(jsobj,JSProxy):returnjsobjifresult:=self.cache.get(jsobj.js_id):returnresultfromjstypes.global_thisimportArray,Objecttype_tag=getTypeTag(jsobj)self.depth-=1try:ifArray.isArray(jsobj):returnself.convert_list(jsobj)iftype_tag=="[object Map]":returnself.convert_map(jsobj,jsobj.entries())iftype_tag=="[object Set]":returnself.convert_set(jsobj)iftype_tag=="[object Object]"and(jsobj.constructorin[None,Object]):returnself.convert_map(jsobj,Object.entries(jsobj))ifself.default_converterisnotNone:returnself.default_converter(jsobj,self.convert,self.cache_conversion)returnjsobjfinally:self.depth+=1defconvert_list(self,jsobj):result=[]self.cache_conversion(jsobj,result)foriteminjsobj:result.append(self.convert(item))returnresultdefconvert_map(self,jsobj,entries):result={}self.cache_conversion(jsobj,result)for[key,val]inentries:result[key]=self.convert(val)returnresultdefconvert_set(self,jsobj):result=set()self.cache_conversion(jsobj,result)forkeyinjsobj:result.add(self.convert(key))returnresult
defto_js(obj,/,*,depth=-1,pyproxies=None,create_pyproxies=True,dict_converter=None,default_converter=None,eager_converter=None,):converter=ToJsConverter(depth,pyproxies,create_pyproxies,dict_converter,default_converter,eager_converter,)result=converter.convert(obj)converter.postprocess()returnresultclassToJsConverter:def__init__(self,depth,pyproxies,create_pyproxies,dict_converter,default_converter,eager_converter,):self.depth=depthself.pyproxies=pyproxiesself.create_pyproxies=create_pyproxiesifdict_converterisNone:dict_converter=Object.fromEntriesself.dict_converter=dict_converterself.default_converter=default_converterself.eager_converter=eager_converterself.cache={}self.post_process_list=[]self.pairs_to_dict_map={}defcache_conversion(self,pyobj,jsobj):self.cache[id(pyobj)]=jsobjdefpostprocess(self):# Replace any NoValue's that appear once we've certainly computed# their correct conversionsforparent,key,pyobj_idinself.post_process_list:real_value=self.cache[pyobj_id]# If it was a dictionary, we need to lookup the actual result objectreal_parent=self.pairs_to_dict_map.get(parent.js_id,parent)real_parent[key]=real_value@contextmanagerdefdecrement_depth(self):self.depth-=1try:yieldfinally:self.depth+=1defconvert(self,pyobj):ifself.depth==0orisinstance(pyobj,JSProxy):returnpyobjifresult:=self.cache.get(id(pyobj)):returnresultwithself.decrement_depth():ifself.eager_converter:returnself.eager_converter(pyobj,self.convert_no_eager_public,self.cache_conversion)returnself.convert_no_eager(pyobj)defconvert_no_eager_public(self,pyobj):withself.decrement_depth():returnself.convert_no_eager(pyobj)defconvert_no_eager(self,pyobj):ifisinstance(pyobj,(tuple,list)):returnself.convert_sequence(pyobj)ifisinstance(pyobj,dict):returnself.convert_dict(pyobj)ifisinstance(pyobj,set):returnself.convert_set(pyobj)ifself.default_converter:returnself.default_converter(pyobj,self.convert_no_eager_public,self.cache_conversion)ifnotself.create_pyproxies:raiseConversionError(f"No conversion available for{pyobj!r} and create_pyproxies=False passed")result=create_proxy(pyobj)ifself.pyproxiesisnotNone:self.pyproxies.append(result)returnresultdefconvert_sequence(self,pyobj):fromjstypes.global_thisimportArrayresult=Array.new()self.cache_conversion(pyobj,result)foridx,valinenumerate(pyobj):converted=self.convert(val)ifconvertedisNoValue:self.post_process_list.append((result,idx,id(val)))result.push(converted)returnresultdefconvert_dict(self,pyobj):fromjstypes.global_thisimportArray# Temporarily store NoValue in the cache since we only get the# actual value from dict_converter. We'll replace these with the# correct values in the postprocess stepself.cache_conversion(pyobj,NoValue)pairs=Array.new()for[key,value]inpyobj.items():converted=self.convert(value)ifconvertedisNoValue:self.post_process_list.append((pairs,key,id(value)))pairs.push(Array.new(key,converted))result=self.dict_converter(pairs)self.pairs_to_dict_map[pairs.js_id]=result# Update the cache to point to the actual resultself.cache_conversion(pyobj,result)returnresultdefconvert_set(self,pyobj):fromjstypes.global_thisimportSetresult=Set.new()self.cache_conversion(pyobj,result)forkeyinpyobj:ifisinstance(key,JSProxy):raiseConversionError(f"Cannot use{key!r} as a key for a JavaScript Set")result.add(key)returnresult
jstypes.global_this ModuleThejstypes.global_this module allows us to import objects from JavaScript. The definition isas follows:
importsysfromjstypes.codeimportrun_jsfromjstypes.ffiimportJSProxyfromimportlib.abcimportLoader,MetaPathFinderfromimportlib.utilimportspec_from_loaderclassJSLoader(Loader):def__init__(self,jsproxy):self.jsproxy=jsproxydefcreate_module(self,spec):returnself.jsproxydefexec_module(self,module):passdefis_package(self,fullname):returnTrueclassJSFinder(MetaPathFinder):def_get_object(self,fullname):[parent,_,child]=fullname.rpartition(".")ifnotparent:ifchild=="jstypes":returnrun_js("globalThis")returnNoneparent_module=sys.modules[parent]ifnotisinstance(parent_module,JSProxy):# Not one of us.returnNonejsproxy=getattr(parent_module,child,None)ifnotisinstance(jsproxy,JSProxy):raiseModuleNotFoundError(f"No module named{fullname!r}",name=fullname)returnjsproxydeffind_spec(self,fullname,path,target,):jsproxy=self._get_object(fullname)loader=JSLoader(jsproxy)returnspec_from_loader(fullname,loader,origin="javascript")finder=JSFinder()sys.meta_path.insert(0,finder)delsys.modules["jstypes.global_this"]importjstypes.global_thissys.meta_path.remove(finder)sys.meta_path.append(finder)
jstypes packageThis has an empty__init__.py and two submodules.
jstypes.ffi ModuleThis has the following properties:
create_proxy(x): This returnscreate_jsproxy(createPyProxy(x)).
jsnull: Special value that converts to/from the JavaScriptnull value.
JSNull: The type ofjsnull.
defdestroy_proxies(proxies):forproxyinproxies:proxy.destroy()
to_js: See definition in the section on deep conversions.
JSArray: This istype(run_js("[]")).
JSCallable: This istype(run_js("()=>{}")).
JSDoubleProxy: This istype(create_proxy({})).
JSException: This istype(run_js("newError()")).
JSGenerator: This istype(run_js("(function*(){})()")).
JSIterable: This istype(run_js("({[Symbol.iterator](){}})")).
JSIterator: This istype(run_js("({next(){}})")).
JSMap: This istype(run_js("({get(){}})")).
JSMutableMap: This istype(run_js("newMap()")).
JSProxy: This istype(run_js("({})"))
JSBigInt: This is defined as follows:
def_int_to_bigint(x):ifisinstance(x,int):returnJSBigInt(x)returnxclassJSBigInt(int):# unary opsdef__abs__(self):returnJSBigInt(int.__abs__(self))def__invert__(self):returnJSBigInt(int.__invert__(self))def__neg__(self):returnJSBigInt(int.__neg__(self))def__pos__(self):returnJSBigInt(int.__pos__(self))# binary opsdef__add__(self,other):return_int_to_bigint(int.__add__(self,other))def__and__(self,other):return_int_to_bigint(int.__and__(self,other))def__floordiv__(self,other):return_int_to_bigint(int.__floordiv__(self,other))def__lshift__(self,other):return_int_to_bigint(int.__lshift__(self,other))def__mod__(self,other):return_int_to_bigint(int.__mod__(self,other))def__or__(self,other):return_int_to_bigint(int.__or__(self,other))def__pow__(self,other,modulus=None):return_int_to_bigint(int.__pow__(self,other,modulus))def__rshift__(self,other):return_int_to_bigint(int.__rshift__(self,other))def__sub__(self,other):return_int_to_bigint(int.__sub__(self,other))def__xor__(self,other):return_int_to_bigint(int.__xor__(self,other))
jstypes.code ModuleThis exposes therun_js function.
json ModuleThejson module will be updated to serializejsnull tonull.
This is strictly adding new APIs. There are backwards compatibility concerns forPyodide. We have changed the names of several modules and types compared toPyodide:
pyodide package is changed tojstypesjs module is changed tojstypes.global_thisJSProxy variants are capitalized likeJSProxy.In the next release, Pyodide will add support for both the changed names and theoriginal names. We will also upload a package to PyPI that includes backwardscompatibility shims for the old names.
It improves support for one of the few fully sandboxed platforms that Python canrun on.
Mike Droettboom, Roman Yurchak, Gyeongjae Choi, Andrea Giammarchi
This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0818.rst
Last modified:2026-02-09 19:35:47 GMT