Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions environment-wasm.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: pyjs-wasm-run
channels:
- /Users/thorstenbeier/src/recipes/output
#- /Users/thorstenbeier/src/recipes/output
- conda-forge
- https://repo.prefix.dev/emscripten-forge-bot/emscripten-forge-6x

Expand All @@ -12,4 +12,5 @@ dependencies:
- numpy
- zlib-static
- libsqlite-static
- libbzip2-static
- libbzip2-static
- libexpat-static
19 changes: 19 additions & 0 deletions include/pyjs/pre_js/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,25 @@ def _add_resolve_done_callback(future, resolve, reject):
Module._py_to_js = Module.eval("pyjs.to_js")
Module._py_objects.push(Module._py_to_js);

// helpers to iterate over python objects from JavaScript (see make_proxy.js)
Module.exec(`
def _pyjs_is_iterable(obj):
return hasattr(type(obj), "__iter__")

def _pyjs_next(it, done):
try:
return next(it)
except StopIteration:
return done
`)

Module._py_is_iterable = Module.eval("_pyjs_is_iterable")
Module._py_objects.push(Module._py_is_iterable);
Module._py_iter = Module.eval("iter")
Module._py_objects.push(Module._py_iter);
Module._py_next = Module.eval("_pyjs_next")
Module._py_objects.push(Module._py_next);

Module["to_js"] = function(obj){
return Module._py_to_js.py_call(obj)
}
Expand Down
33 changes: 33 additions & 0 deletions include/pyjs/pre_js/make_proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,42 @@ Module['make_proxy'] = function(py_object) {
if (ret !== undefined) {
return ret
}
// python attribute names are strings, so symbols cannot be passed to _getattr
if (typeof property === 'symbol') {
if (property === Symbol.iterator && Module._py_is_iterable.py_call(target)) {
return function() {
return Module._py_object_iterator(target)
}
}
return undefined
}
return target._getattr(property);
}
};

return new Proxy(py_object, handler);
}

// JavaScript iterator over an iterable python object
Module['_py_object_iterator'] = function(py_object) {
// returned by _py_next when the python iterator is exhausted
const done = {}
let py_iter = Module._py_iter.py_call(py_object)
return {
next() {
if (py_iter === null) {
return { done: true, value: undefined }
}
const value = Module._py_next.py_call(py_iter, done)
if (value === done) {
py_iter.delete()
py_iter = null
return { done: true, value: undefined }
}
return { done: false, value: value }
},
[Symbol.iterator]() {
return this
}
}
}
44 changes: 31 additions & 13 deletions module/pyjs/convert_py_to_js.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,58 @@
from .core import new
from pyjs_core import JsValue, js_array, js, js_undefined,internal

def _py_list_like_to_js(value, cache, depth, max_depth):
def _py_list_like_to_js(value, cache, depth, max_depth, dict_converter):
vid = id(value)
if vid in cache:
return cache[vid]
j_value = js_array()
cache[vid] = j_value
for v in value:
js_v = to_js(v, cache=cache, depth=depth + 1, max_depth=max_depth)
js_v = to_js(v, cache=cache, depth=depth + 1, max_depth=max_depth, dict_converter=dict_converter)
j_value.push(js_v)
return j_value


def _py_dict_like_to_js(value, cache, depth, max_depth):
def _py_dict_like_to_js(value, cache, depth, max_depth, dict_converter):
vid = id(value)
if vid in cache:
return cache[vid]
if dict_converter is not None:
# The converted value is only cached after all entries are converted,
# so recursive dicts are not supported.
entries = js_array()
for k, v in value.items():
js_k = to_js(k, cache=cache, depth=depth + 1, max_depth=max_depth, dict_converter=dict_converter)
js_v = to_js(v, cache=cache, depth=depth + 1, max_depth=max_depth, dict_converter=dict_converter)
pair = js_array()
pair.push(js_k)
pair.push(js_v)
entries.push(pair)
j_value = dict_converter(entries)
cache[vid] = j_value
return j_value
j_value = new(js.LiteralMap)
cache[vid] = j_value
for k, v in value.items():
js_k = to_js(k, cache=cache, depth=depth + 1, max_depth=max_depth)
js_v = to_js(v, cache=cache, depth=depth + 1, max_depth=max_depth)
js_k = to_js(k, cache=cache, depth=depth + 1, max_depth=max_depth, dict_converter=dict_converter)
js_v = to_js(v, cache=cache, depth=depth + 1, max_depth=max_depth, dict_converter=dict_converter)
j_value.set(js_k, js_v)
return j_value


def _py_set_like_to_js(value, cache, depth, max_depth):
def _py_set_like_to_js(value, cache, depth, max_depth, dict_converter):
vid = id(value)
if vid in cache:
return cache[vid]
j_value = new(js.Set)
cache[vid] = j_value
for v in value:
js_v = to_js(v, cache=cache, depth=depth + 1, max_depth=max_depth)
js_v = to_js(v, cache=cache, depth=depth + 1, max_depth=max_depth, dict_converter=dict_converter)
j_value.add(js_v)
return j_value


def to_js(value, cache=None, depth=0, max_depth=None):
def to_js(value, cache=None, depth=0, max_depth=None, dict_converter=None):
"""
Convert a Python object to a JavaScript object.

Expand All @@ -48,7 +62,11 @@ def to_js(value, cache=None, depth=0, max_depth=None):
cache: A dictionary to use as a cache for already converted objects.
depth: The current depth of of nested object conversion.
max_depth: The maximum depth of nested object conversion.

dict_converter: Optional function that receives a JavaScript array of
[key, value] pairs and returns the converted dict, e.g.
js.Object.fromEntries. If None, dicts are converted to a LiteralMap.
Recursive dicts are not supported when this is used.

Returns:

a JavaScript stored in a pyjs_core.JsValue
Expand All @@ -63,15 +81,15 @@ def to_js(value, cache=None, depth=0, max_depth=None):
return value
elif isinstance(value, (list, tuple)):
return _py_list_like_to_js(
value=value, cache=cache, depth=depth, max_depth=max_depth
value=value, cache=cache, depth=depth, max_depth=max_depth, dict_converter=dict_converter
)
elif isinstance(value, dict):
return _py_dict_like_to_js(
value=value, cache=cache, depth=depth, max_depth=max_depth
value=value, cache=cache, depth=depth, max_depth=max_depth, dict_converter=dict_converter
)
elif isinstance(value, set):
return _py_set_like_to_js(
value=value, cache=cache, depth=depth, max_depth=max_depth
value=value, cache=cache, depth=depth, max_depth=max_depth, dict_converter=dict_converter
)
elif value is None:
return js_undefined()
Expand All @@ -85,7 +103,7 @@ def to_js(value, cache=None, depth=0, max_depth=None):

elif hasattr(value, "explicit_to_js"):
return value.explicit_to_js()

elif hasattr(value, "implicit_to_js"):
return value.implicit_to_js()

Expand Down
2 changes: 1 addition & 1 deletion src/js_timestamp.cpp
Original file line number Diff line number Diff line change
@@ -1 +1 @@
#define PYJS_JS_UTC_TIMESTAMP "2026-09-07 08:23:38.609625"
#define PYJS_JS_UTC_TIMESTAMP "2026-10-27 15:17:57.728418"
18 changes: 18 additions & 0 deletions tests/tests/test_pyjs.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,24 @@ def test_to_js_dict():
assert jsmap.get(1) == "a"


def test_to_js_dict_converter():
pydict = {"a": 1, "b": [2, {"c": "three"}]}
jsobj = pyjs.to_js(pydict, dict_converter=pyjs.js.Object.fromEntries)
assert pyjs.js.JSON.stringify(jsobj) == '{"a":1,"b":[2,{"c":"three"}]}'


def test_iterate_py_object_in_js():
# python objects passed to js are proxies that implement Symbol.iterator
blob = pyjs.js.Blob.new(["hello", "world"])
assert blob.size == 10

jsarray = getattr(pyjs.js.Array, "from")([1, "two", 3.5])
assert pyjs.js.JSON.stringify(jsarray) == '[1,"two",3.5]'

jsarray = getattr(pyjs.js.Array, "from")({"a": 1, "b": 2})
assert pyjs.js.JSON.stringify(jsarray) == '["a","b"]'


def test_bytes_to_js():
pyjs.to_js(b"\x00").byteLength == 1

Expand Down