Skip to content

Commit f2f8090

Browse files
committed
Add asyncweb wasm module
1 parent 5d171f3 commit f2f8090

2 files changed

Lines changed: 224 additions & 55 deletions

File tree

wasm/demo/snippets/asyncbrowser.py

Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,16 @@
11
import browser
2-
import functools
3-
4-
5-
# just setting up the framework, skip to the bottom to see the real code
6-
7-
ready = object()
8-
go = object()
9-
10-
11-
def run(coro, *, payload=None, error=False):
12-
send = coro.throw if error else coro.send
13-
try:
14-
cmd = send(payload)
15-
except StopIteration:
16-
return
17-
if cmd is ready:
18-
coro.send(
19-
(
20-
lambda *args: run(coro, payload=args),
21-
lambda *args: run(coro, payload=args, error=True),
22-
)
23-
)
24-
elif cmd is go:
25-
pass
26-
else:
27-
raise RuntimeError(f"expected cmd to be ready or go, got {cmd}")
28-
29-
30-
class JSFuture:
31-
def __init__(self, prom):
32-
self._prom = prom
33-
34-
def __await__(self):
35-
done, error = yield ready
36-
self._prom.then(done, error)
37-
res, = yield go
38-
return res
39-
40-
41-
def wrap_prom_func(func):
42-
@functools.wraps(func)
43-
async def wrapper(*args, **kwargs):
44-
return await JSFuture(func(*args, **kwargs))
45-
46-
return wrapper
47-
48-
49-
fetch = wrap_prom_func(browser.fetch)
50-
51-
###################
52-
# Real code start #
53-
###################
54-
2+
import asyncweb
553

564
async def main(delay):
575
url = f"https://httpbin.org/delay/{delay}"
586
print(f"fetching {url}...")
59-
res = await fetch(
7+
res = await browser.fetch(
608
url, response_format="json", headers={"X-Header-Thing": "rustpython is neat!"}
619
)
6210
print(f"got res from {res['url']}:")
6311
print(res, end="\n\n")
6412

6513

6614
for delay in range(3):
67-
run(main(delay))
15+
asyncweb.run(main(delay))
6816
print()

wasm/lib/Lib/asyncweb.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
from _js import Promise
2+
from collections.abc import Coroutine, Awaitable
3+
from abc import ABC, abstractmethod
4+
5+
6+
def is_promise(prom):
7+
return callable(getattr(prom, "then", None))
8+
9+
10+
def run(coro):
11+
"""
12+
Run a coroutine. The coroutine should yield promise objects with a
13+
``.then(on_success, on_error)`` method.
14+
"""
15+
_Runner(coro)
16+
17+
18+
class _Runner:
19+
def __init__(self, coro):
20+
self._send = coro.send
21+
self._throw = coro.throw
22+
# start the coro
23+
self.success(None)
24+
25+
def _run(self, send, arg):
26+
try:
27+
ret = send(arg)
28+
except StopIteration:
29+
return
30+
ret.then(self.success, self.error)
31+
32+
def success(self, res):
33+
self._run(self._send, res)
34+
35+
def error(self, err):
36+
self._run(self._throw, err)
37+
38+
39+
def main(async_func):
40+
"""
41+
A decorator to mark a function as main. This calls run() on the
42+
result of the function, and logs an error that occurs.
43+
"""
44+
run(_main_wrapper(async_func()))
45+
return async_func
46+
47+
48+
async def _main_wrapper(coro):
49+
try:
50+
await coro
51+
except BaseException as e:
52+
for line in _format_exc(e, 1):
53+
print(line)
54+
55+
56+
# TODO: get traceback/linecache working in wasm
57+
58+
59+
def _format_exc(exc, skip_tb=0):
60+
exc_type, exc_value, exc_traceback = type(exc), exc, exc.__traceback__
61+
62+
_str = _some_str(exc_value)
63+
64+
yield "Traceback (most recent call last):"
65+
tb = exc_traceback
66+
while tb:
67+
if skip_tb:
68+
skip_tb -= 1
69+
else:
70+
co = tb.tb_frame.f_code
71+
yield f' File "{co.co_filename}", line {tb.tb_lineno}, in {co.co_name}'
72+
tb = tb.tb_next
73+
74+
stype = exc_type.__qualname__
75+
smod = exc_type.__module__
76+
if smod not in ("__main__", "builtins"):
77+
stype = smod + "." + stype
78+
79+
yield _format_final_exc_line(stype, _str)
80+
81+
82+
def _format_final_exc_line(etype, value):
83+
valuestr = _some_str(value)
84+
if value is None or not valuestr:
85+
line = "%s" % etype
86+
else:
87+
line = "%s: %s" % (etype, valuestr)
88+
return line
89+
90+
91+
def _some_str(value):
92+
try:
93+
return str(value)
94+
except:
95+
return "<unprintable %s object>" % type(value).__name__
96+
97+
98+
def _resolve(prom):
99+
if is_promise(prom):
100+
pass
101+
elif isinstance(prom, Coroutine):
102+
prom = _CoroPromise(prom)
103+
104+
return Promise.resolve(prom)
105+
106+
107+
class _CallbackMap:
108+
def __init__(self):
109+
self.done = 0
110+
self._successes = []
111+
self._errors = []
112+
113+
def then(self, success=None, error=None):
114+
if success and not callable(success):
115+
raise TypeError("success callback must be callable")
116+
if error and not callable(error):
117+
raise TypeError("error callback must be callable")
118+
119+
if self.done == -1:
120+
if error:
121+
return _call_resolve(error, self._error)
122+
else:
123+
return self
124+
elif self.done == 1:
125+
if success:
126+
return _call_resolve(success, self._result)
127+
else:
128+
return self
129+
130+
if success:
131+
# def onsuccess(then=
132+
self._successes.append(success)
133+
if error:
134+
self._errors.append(error)
135+
136+
def resolve(self, value):
137+
self._result = value
138+
self.done = 1
139+
for f in self._successes:
140+
f(value)
141+
del self._successes, self._errors
142+
143+
def reject(self, err):
144+
self._result = err
145+
self.done = -1
146+
for f in self._errors:
147+
f(err)
148+
del self._successes, self._errors
149+
150+
151+
class _CoroPromise:
152+
def __init__(self, coro):
153+
self._cbs = _CallbackMap()
154+
155+
async def run_coro():
156+
try:
157+
res = await coro
158+
except BaseException as e:
159+
self._cbs.reject(e)
160+
else:
161+
self._cbs.resolve(res)
162+
163+
run(run_coro())
164+
165+
def then(self, on_success=None, on_failure=None):
166+
self._cbs.then(on_success, on_failure)
167+
168+
169+
def _call_resolve(f, arg):
170+
try:
171+
ret = f(arg)
172+
except BaseException as e:
173+
return Promise.reject(e)
174+
else:
175+
return _resolve(ret)
176+
177+
178+
def wait_all(proms):
179+
return Promise.resolve(_WaitAll(proms))
180+
181+
182+
# basically an implementation of Promise.all
183+
class _WaitAll:
184+
def __init__(self, proms):
185+
if not isinstance(proms, (list, tuple)):
186+
proms = tuple(proms)
187+
self._completed = 0
188+
self.cbs = _CallbackMap()
189+
num_proms = len(proms)
190+
self._results = [None] * num_proms
191+
192+
# needs to be a separate function for creating a closure in a loop
193+
def register_promise(i, prom):
194+
completed = False
195+
196+
def promise_done(success, res):
197+
nonlocal completed
198+
if completed or self.cbs.done:
199+
return
200+
completed = True
201+
if success:
202+
self._results[i] = res
203+
self._completed += 1
204+
if self._completed == num_proms:
205+
results = tuple(self._results)
206+
del self._results
207+
self.cbs.resolve(results)
208+
else:
209+
del self._results
210+
self.cbs.reject(res)
211+
212+
_resolve(prom).then(
213+
lambda res: promise_done(True, res),
214+
lambda err: promise_done(False, err),
215+
)
216+
217+
for i, prom in enumerate(proms):
218+
register_promise(i, prom)
219+
220+
def then(self, success=None, error=None):
221+
self.cbs.then(success, error)

0 commit comments

Comments
 (0)