pyweb-stack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyweb/__init__.py +71 -0
- pyweb/app.py +37 -0
- pyweb/app_loader.py +139 -0
- pyweb/asgi.py +71 -0
- pyweb/auth.py +701 -0
- pyweb/bench.py +101 -0
- pyweb/browser.py +232 -0
- pyweb/build.py +295 -0
- pyweb/cache.py +201 -0
- pyweb/cli/__init__.py +465 -0
- pyweb/cli/__main__.py +3 -0
- pyweb/compiler/__init__.py +5 -0
- pyweb/compiler/ast.py +45 -0
- pyweb/compiler/codegen/__init__.py +9 -0
- pyweb/compiler/codegen/ir.py +50 -0
- pyweb/compiler/errors.py +29 -0
- pyweb/compiler/lower.py +733 -0
- pyweb/compiler/parser.py +532 -0
- pyweb/compiler/pipeline.py +158 -0
- pyweb/compiler/pyjs.py +1087 -0
- pyweb/compiler/rpc.py +35 -0
- pyweb/context.py +193 -0
- pyweb/css.py +115 -0
- pyweb/db/__init__.py +652 -0
- pyweb/db/migrate.py +143 -0
- pyweb/decorators.py +49 -0
- pyweb/deploy.py +102 -0
- pyweb/forms.py +108 -0
- pyweb/hosting.py +139 -0
- pyweb/jobs.py +248 -0
- pyweb/live.py +88 -0
- pyweb/lsp.py +214 -0
- pyweb/models.py +269 -0
- pyweb/npm.py +270 -0
- pyweb/observability.py +313 -0
- pyweb/platform.py +69 -0
- pyweb/plugins.py +112 -0
- pyweb/py.typed +0 -0
- pyweb/realtime.py +189 -0
- pyweb/rpc.py +137 -0
- pyweb/runtime/browser/runtime.js +1151 -0
- pyweb/runtime/server/__init__.py +445 -0
- pyweb/security.py +162 -0
- pyweb/serve.py +335 -0
- pyweb/ssr.py +260 -0
- pyweb/sync.py +118 -0
- pyweb/testing.py +154 -0
- pyweb/uploads.py +94 -0
- pyweb_stack-0.1.0.dist-info/METADATA +198 -0
- pyweb_stack-0.1.0.dist-info/RECORD +54 -0
- pyweb_stack-0.1.0.dist-info/WHEEL +5 -0
- pyweb_stack-0.1.0.dist-info/entry_points.txt +2 -0
- pyweb_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyweb_stack-0.1.0.dist-info/top_level.txt +1 -0
pyweb/compiler/pyjs.py
ADDED
|
@@ -0,0 +1,1087 @@
|
|
|
1
|
+
"""Python → JavaScript translation for browser-placed code.
|
|
2
|
+
|
|
3
|
+
PyWeb compiles a *defined subset* of Python to JavaScript: the code in
|
|
4
|
+
event handlers, `{expressions}` in markup, derived values and module-level
|
|
5
|
+
helper functions that browser code calls. Anything outside the subset is
|
|
6
|
+
a :class:`CompileError` with the `.pyweb` line, never silently wrong JS.
|
|
7
|
+
|
|
8
|
+
Python semantics are kept where JavaScript differs (truthiness of empty
|
|
9
|
+
lists, ``==`` on lists/dicts, negative indexing, ``//`` and ``%``, string
|
|
10
|
+
and list methods, ``KeyError``/``IndexError``) through small helpers in
|
|
11
|
+
the browser runtime (``$py.*``).
|
|
12
|
+
|
|
13
|
+
State is reactive: reading a page signal emits ``name()``; assigning it
|
|
14
|
+
emits ``name(value)``; mutating it in place (``todos.append(x)``,
|
|
15
|
+
``todos[i]["done"] = True``) becomes a copy-on-write update so every
|
|
16
|
+
dependent DOM node refreshes.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
import json
|
|
23
|
+
import math
|
|
24
|
+
|
|
25
|
+
from .errors import CompileError
|
|
26
|
+
|
|
27
|
+
#: Python builtins available in browser code (→ ``$py.<name>``).
|
|
28
|
+
BUILTINS = frozenset({
|
|
29
|
+
"len", "str", "int", "float", "bool", "abs", "min", "max", "sum", "round",
|
|
30
|
+
"range", "sorted", "reversed", "enumerate", "zip", "list", "dict", "set",
|
|
31
|
+
"tuple", "any", "all", "chr", "ord", "isinstance", "print", "repr",
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
#: Real JavaScript globals usable directly from browser code.
|
|
35
|
+
JS_GLOBALS = frozenset({
|
|
36
|
+
"window", "document", "console", "Math", "JSON", "Date", "localStorage",
|
|
37
|
+
"sessionStorage", "navigator", "location", "history", "setTimeout",
|
|
38
|
+
"setInterval", "clearTimeout", "clearInterval", "fetch", "alert", "confirm",
|
|
39
|
+
"prompt", "requestAnimationFrame", "Intl", "URL", "URLSearchParams",
|
|
40
|
+
"Promise", "Number", "String", "Array", "Object", "Event", "CustomEvent",
|
|
41
|
+
"FormData", "Blob", "crypto", "performance", "structuredClone",
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
#: Python methods dispatched through ``$py.m`` (strings, lists, dicts, sets).
|
|
45
|
+
PY_METHODS = frozenset({
|
|
46
|
+
"append", "extend", "insert", "pop", "remove", "index", "count", "clear",
|
|
47
|
+
"sort", "reverse", "copy", "get", "keys", "values", "items", "update",
|
|
48
|
+
"setdefault", "add", "discard", "upper", "lower", "strip", "lstrip",
|
|
49
|
+
"rstrip", "split", "splitlines", "join", "replace", "startswith",
|
|
50
|
+
"endswith", "find", "rfind", "title", "capitalize", "isdigit",
|
|
51
|
+
"isnumeric", "isalpha", "isalnum", "isspace", "isupper", "islower",
|
|
52
|
+
"zfill", "ljust", "rjust", "center", "format",
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
#: Methods that mutate their receiver (→ copy-on-write on state).
|
|
56
|
+
MUTATING = frozenset({
|
|
57
|
+
"append", "extend", "insert", "pop", "remove", "clear", "sort", "reverse",
|
|
58
|
+
"update", "setdefault", "add", "discard",
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
#: Calls that always produce a boolean (skip the truthiness helper).
|
|
62
|
+
BOOL_CALLS = frozenset({"bool", "isinstance", "any", "all", "callable"})
|
|
63
|
+
BOOL_METHODS = frozenset({
|
|
64
|
+
"startswith", "endswith", "isdigit", "isnumeric", "isalpha", "isalnum",
|
|
65
|
+
"isspace", "isupper", "islower", "includes",
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
JS_RESERVED = frozenset({
|
|
69
|
+
"arguments", "await", "case", "catch", "const", "debugger", "default",
|
|
70
|
+
"delete", "do", "enum", "eval", "export", "extends", "function",
|
|
71
|
+
"implements", "instanceof", "interface", "let", "new", "package",
|
|
72
|
+
"private", "protected", "public", "static", "super", "switch", "this",
|
|
73
|
+
"throw", "typeof", "var", "void", "undefined", "NaN", "Infinity",
|
|
74
|
+
"yield", "null", "true", "false",
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
# Name kinds in a Scope.
|
|
78
|
+
SIGNAL = "signal" # read: name() write: name(v)
|
|
79
|
+
COMPUTED = "computed" # read: name() write: error
|
|
80
|
+
PROP = "prop" # component prop getter: read name()
|
|
81
|
+
VALUE = "value" # plain JS binding (locals, loop vars, params)
|
|
82
|
+
CONST = "const" # read-only plain binding
|
|
83
|
+
HANDLER = "handler" # browser function defined in the page
|
|
84
|
+
SERVER = "server" # @server function: call → await RPC
|
|
85
|
+
HELPER = "helper" # module-level function compiled on demand
|
|
86
|
+
MODCONST = "modconst" # module-level literal constant
|
|
87
|
+
COMPONENT = "component"
|
|
88
|
+
SERVER_ONLY = "server_only" # exists only on the server (imports, models, ...)
|
|
89
|
+
|
|
90
|
+
REACTIVE_KINDS = (SIGNAL, COMPUTED, PROP)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def jsname(name):
|
|
94
|
+
return name + "_" if name in JS_RESERVED else name
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Scope:
|
|
98
|
+
def __init__(self, parent=None, names=None, kind="block"):
|
|
99
|
+
self.parent = parent
|
|
100
|
+
self.names = dict(names or {})
|
|
101
|
+
self.kind = kind
|
|
102
|
+
|
|
103
|
+
def lookup(self, name):
|
|
104
|
+
s = self
|
|
105
|
+
while s is not None:
|
|
106
|
+
if name in s.names:
|
|
107
|
+
return s.names[name]
|
|
108
|
+
s = s.parent
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def child(self, names=None, kind="block"):
|
|
112
|
+
return Scope(self, names, kind)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ModuleContext:
|
|
116
|
+
"""Module-level facts the translator needs (filled by the lowering pass)."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, filename="<pyweb>"):
|
|
119
|
+
self.filename = filename
|
|
120
|
+
self.app_config = {} # literal App(...) keyword arguments
|
|
121
|
+
self.server_fns = {} # name -> [param names]
|
|
122
|
+
self.helpers = {} # name -> FunctionDef
|
|
123
|
+
self.modconsts = {} # name -> python value
|
|
124
|
+
self.components = {} # name -> component info
|
|
125
|
+
self.server_only = {} # name -> reason
|
|
126
|
+
self.browser_globals = {} # name -> js expression (pyweb.browser imports)
|
|
127
|
+
# results of on-demand helper compilation
|
|
128
|
+
self.helper_js = {} # name -> js source
|
|
129
|
+
self.helper_async = {} # name -> bool
|
|
130
|
+
self.used_consts = set()
|
|
131
|
+
self.used_helpers = []
|
|
132
|
+
self.rpc_calls = {} # rpc name -> set of caller names
|
|
133
|
+
|
|
134
|
+
def module_scope(self):
|
|
135
|
+
names = {}
|
|
136
|
+
for n in self.server_only:
|
|
137
|
+
names[n] = SERVER_ONLY
|
|
138
|
+
for n in self.modconsts:
|
|
139
|
+
names[n] = MODCONST
|
|
140
|
+
for n in self.helpers:
|
|
141
|
+
names[n] = HELPER
|
|
142
|
+
for n in self.server_fns:
|
|
143
|
+
names[n] = SERVER
|
|
144
|
+
for n in self.components:
|
|
145
|
+
names[n] = COMPONENT
|
|
146
|
+
for n in self.browser_globals:
|
|
147
|
+
names[n] = "browser_global"
|
|
148
|
+
return Scope(None, names, kind="module")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class _Fn:
|
|
152
|
+
"""Per-function translation state."""
|
|
153
|
+
|
|
154
|
+
def __init__(self, name):
|
|
155
|
+
self.name = name
|
|
156
|
+
self.is_async = False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Translator:
|
|
160
|
+
def __init__(self, ctx: ModuleContext):
|
|
161
|
+
self.ctx = ctx
|
|
162
|
+
self.fn_stack = []
|
|
163
|
+
self._tmp = 0
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------ errors
|
|
166
|
+
def error(self, node, msg):
|
|
167
|
+
return CompileError(msg, getattr(node, "lineno", None), self.ctx.filename)
|
|
168
|
+
|
|
169
|
+
def tmp(self, base="t"):
|
|
170
|
+
self._tmp += 1
|
|
171
|
+
return f"${base}{self._tmp}"
|
|
172
|
+
|
|
173
|
+
def mark_async(self):
|
|
174
|
+
if self.fn_stack:
|
|
175
|
+
self.fn_stack[-1].is_async = True
|
|
176
|
+
|
|
177
|
+
# ------------------------------------------------------------- names
|
|
178
|
+
def read_name(self, node, scope):
|
|
179
|
+
name = node.id
|
|
180
|
+
kind = scope.lookup(name)
|
|
181
|
+
js = jsname(name)
|
|
182
|
+
if kind in REACTIVE_KINDS:
|
|
183
|
+
return f"{js}()"
|
|
184
|
+
if kind in (VALUE, CONST, HANDLER):
|
|
185
|
+
return js
|
|
186
|
+
if kind == MODCONST:
|
|
187
|
+
self.ctx.used_consts.add(name)
|
|
188
|
+
return js
|
|
189
|
+
if kind == HELPER:
|
|
190
|
+
self.compile_helper(name, node)
|
|
191
|
+
return js
|
|
192
|
+
if kind == COMPONENT:
|
|
193
|
+
return js
|
|
194
|
+
if kind == "browser_global":
|
|
195
|
+
return self.ctx.browser_globals[name]
|
|
196
|
+
if kind == SERVER:
|
|
197
|
+
raise self.error(node, f"server function {name!r} must be called, e.g. {name}(...); "
|
|
198
|
+
"it cannot be passed around in browser code")
|
|
199
|
+
if kind == SERVER_ONLY:
|
|
200
|
+
raise self.error(node, f"{name!r} only exists on the server "
|
|
201
|
+
f"({self.ctx.server_only[name]}). Browser code cannot use it; "
|
|
202
|
+
"wrap the work in an @server function and call that instead")
|
|
203
|
+
if name in BUILTINS:
|
|
204
|
+
return f"$py.{name}"
|
|
205
|
+
if name in JS_GLOBALS:
|
|
206
|
+
return name
|
|
207
|
+
raise self.error(node, f"name {name!r} is not defined in browser code")
|
|
208
|
+
|
|
209
|
+
def is_reactive(self, node, scope):
|
|
210
|
+
"""Does evaluating ``node`` read a signal, computed or prop?"""
|
|
211
|
+
for sub in ast.walk(node):
|
|
212
|
+
if isinstance(sub, ast.Name) and isinstance(sub.ctx, ast.Load):
|
|
213
|
+
if scope.lookup(sub.id) in REACTIVE_KINDS:
|
|
214
|
+
return True
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
# ------------------------------------------------------- expressions
|
|
218
|
+
def expr(self, node, scope):
|
|
219
|
+
m = getattr(self, "x_" + type(node).__name__, None)
|
|
220
|
+
if m is None:
|
|
221
|
+
raise self.error(node, f"{type(node).__name__} expressions are not supported in browser code")
|
|
222
|
+
return m(node, scope)
|
|
223
|
+
|
|
224
|
+
def x_Constant(self, node, scope):
|
|
225
|
+
v = node.value
|
|
226
|
+
if v is None:
|
|
227
|
+
return "null"
|
|
228
|
+
if v is True:
|
|
229
|
+
return "true"
|
|
230
|
+
if v is False:
|
|
231
|
+
return "false"
|
|
232
|
+
if isinstance(v, str):
|
|
233
|
+
return json.dumps(v)
|
|
234
|
+
if isinstance(v, int):
|
|
235
|
+
if abs(v) > 2 ** 53:
|
|
236
|
+
raise self.error(node, "integers beyond 2**53 cannot be represented exactly in the browser")
|
|
237
|
+
return str(v)
|
|
238
|
+
if isinstance(v, float):
|
|
239
|
+
if math.isinf(v):
|
|
240
|
+
return "Infinity" if v > 0 else "-Infinity"
|
|
241
|
+
if math.isnan(v):
|
|
242
|
+
return "NaN"
|
|
243
|
+
return repr(v)
|
|
244
|
+
if v is Ellipsis:
|
|
245
|
+
return "null"
|
|
246
|
+
raise self.error(node, f"{type(v).__name__} literals are not supported in browser code")
|
|
247
|
+
|
|
248
|
+
def x_Name(self, node, scope):
|
|
249
|
+
if node.id == "self":
|
|
250
|
+
raise self.error(node, "classes are not supported in browser code")
|
|
251
|
+
return self.read_name(node, scope)
|
|
252
|
+
|
|
253
|
+
def x_JoinedStr(self, node, scope):
|
|
254
|
+
parts = []
|
|
255
|
+
for v in node.values:
|
|
256
|
+
if isinstance(v, ast.Constant):
|
|
257
|
+
parts.append(str(v.value).replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${"))
|
|
258
|
+
else:
|
|
259
|
+
parts.append("${" + self.formatted(v, scope) + "}")
|
|
260
|
+
return "`" + "".join(parts) + "`"
|
|
261
|
+
|
|
262
|
+
def formatted(self, v, scope):
|
|
263
|
+
inner = self.expr(v.value, scope)
|
|
264
|
+
if v.conversion == ord("r"):
|
|
265
|
+
inner = f"$py.repr({inner})"
|
|
266
|
+
if v.format_spec is not None:
|
|
267
|
+
spec = v.format_spec
|
|
268
|
+
if (isinstance(spec, ast.JoinedStr) and len(spec.values) == 1
|
|
269
|
+
and isinstance(spec.values[0], ast.Constant)):
|
|
270
|
+
return f"$py.format({inner}, {json.dumps(spec.values[0].value)})"
|
|
271
|
+
return f"$py.format({inner}, {self.expr(spec, scope)})"
|
|
272
|
+
return f"$py.str({inner})"
|
|
273
|
+
|
|
274
|
+
def x_List(self, node, scope):
|
|
275
|
+
return "[" + ", ".join(self.elt(e, scope) for e in node.elts) + "]"
|
|
276
|
+
|
|
277
|
+
x_Tuple = x_List
|
|
278
|
+
|
|
279
|
+
def elt(self, e, scope):
|
|
280
|
+
if isinstance(e, ast.Starred):
|
|
281
|
+
return f"...$py.iter({self.expr(e.value, scope)})"
|
|
282
|
+
return self.expr(e, scope)
|
|
283
|
+
|
|
284
|
+
def x_Set(self, node, scope):
|
|
285
|
+
return "$py.set([" + ", ".join(self.elt(e, scope) for e in node.elts) + "])"
|
|
286
|
+
|
|
287
|
+
def x_Dict(self, node, scope):
|
|
288
|
+
items = []
|
|
289
|
+
for k, v in zip(node.keys, node.values):
|
|
290
|
+
if k is None:
|
|
291
|
+
items.append(f"...{self.expr(v, scope)}")
|
|
292
|
+
elif isinstance(k, ast.Constant) and isinstance(k.value, str):
|
|
293
|
+
items.append(f"{json.dumps(k.value)}: {self.expr(v, scope)}")
|
|
294
|
+
else:
|
|
295
|
+
items.append(f"[{self.expr(k, scope)}]: {self.expr(v, scope)}")
|
|
296
|
+
return "{" + ", ".join(items) + "}"
|
|
297
|
+
|
|
298
|
+
def x_Attribute(self, node, scope):
|
|
299
|
+
return f"{self.expr(node.value, scope)}.{node.attr}"
|
|
300
|
+
|
|
301
|
+
def x_Subscript(self, node, scope):
|
|
302
|
+
base = self.expr(node.value, scope)
|
|
303
|
+
sl = node.slice
|
|
304
|
+
if isinstance(sl, ast.Slice):
|
|
305
|
+
parts = [self.expr(p, scope) if p is not None else "null" for p in (sl.lower, sl.upper, sl.step)]
|
|
306
|
+
return f"$py.slice({base}, {', '.join(parts)})"
|
|
307
|
+
return f"$py.at({base}, {self.expr(sl, scope)})"
|
|
308
|
+
|
|
309
|
+
def x_IfExp(self, node, scope):
|
|
310
|
+
return f"({self.test(node.test, scope)} ? {self.expr(node.body, scope)} : {self.expr(node.orelse, scope)})"
|
|
311
|
+
|
|
312
|
+
def x_UnaryOp(self, node, scope):
|
|
313
|
+
if isinstance(node.op, ast.Not):
|
|
314
|
+
return f"!{self.test(node.operand, scope)}"
|
|
315
|
+
op = {ast.USub: "-", ast.UAdd: "+", ast.Invert: "~"}[type(node.op)]
|
|
316
|
+
return f"({op}{self.expr(node.operand, scope)})"
|
|
317
|
+
|
|
318
|
+
def x_BinOp(self, node, scope):
|
|
319
|
+
return self.binop(node.op, node.left, node.right, scope)
|
|
320
|
+
|
|
321
|
+
def binop(self, op, lnode, rnode, scope, ljs=None):
|
|
322
|
+
a = ljs if ljs is not None else self.expr(lnode, scope)
|
|
323
|
+
b = self.expr(rnode, scope)
|
|
324
|
+
num_l = _is_num(lnode)
|
|
325
|
+
num_r = _is_num(rnode)
|
|
326
|
+
if isinstance(op, ast.Add):
|
|
327
|
+
if (_is_numeric(lnode) and _is_numeric(rnode)) or (_is_str(lnode) and _is_str(rnode)):
|
|
328
|
+
return f"({a} + {b})"
|
|
329
|
+
return f"$py.add({a}, {b})"
|
|
330
|
+
if isinstance(op, ast.Sub):
|
|
331
|
+
return f"({a} - {b})"
|
|
332
|
+
if isinstance(op, ast.Mult):
|
|
333
|
+
if num_l and num_r:
|
|
334
|
+
return f"({a} * {b})"
|
|
335
|
+
return f"$py.mul({a}, {b})"
|
|
336
|
+
if isinstance(op, ast.Div):
|
|
337
|
+
return f"$py.div({a}, {b})" if not num_r else f"({a} / {b})"
|
|
338
|
+
if isinstance(op, ast.FloorDiv):
|
|
339
|
+
return f"$py.floordiv({a}, {b})"
|
|
340
|
+
if isinstance(op, ast.Mod):
|
|
341
|
+
return f"$py.mod({a}, {b})"
|
|
342
|
+
if isinstance(op, ast.Pow):
|
|
343
|
+
return f"({a} ** {b})"
|
|
344
|
+
bit = {ast.BitAnd: "&", ast.BitOr: "|", ast.BitXor: "^", ast.LShift: "<<", ast.RShift: ">>"}
|
|
345
|
+
if type(op) in bit:
|
|
346
|
+
return f"({a} {bit[type(op)]} {b})"
|
|
347
|
+
raise self.error(lnode, f"operator {type(op).__name__} is not supported in browser code")
|
|
348
|
+
|
|
349
|
+
def x_BoolOp(self, node, scope):
|
|
350
|
+
if all(self.is_bool(v, scope) for v in node.values):
|
|
351
|
+
js_op = " && " if isinstance(node.op, ast.And) else " || "
|
|
352
|
+
return "(" + js_op.join(self.expr(v, scope) for v in node.values) + ")"
|
|
353
|
+
if self._calls_server(node, scope):
|
|
354
|
+
raise self.error(node, "server calls inside `and`/`or` are not supported; "
|
|
355
|
+
"assign the result to a variable first")
|
|
356
|
+
helper = "$py.and" if isinstance(node.op, ast.And) else "$py.or"
|
|
357
|
+
out = self.expr(node.values[-1], scope)
|
|
358
|
+
for v in reversed(node.values[:-1]):
|
|
359
|
+
out = f"{helper}({self.expr(v, scope)}, () => {out})"
|
|
360
|
+
return out
|
|
361
|
+
|
|
362
|
+
def x_Compare(self, node, scope):
|
|
363
|
+
parts = []
|
|
364
|
+
left = node.left
|
|
365
|
+
for op, right in zip(node.ops, node.comparators):
|
|
366
|
+
parts.append(self.compare(op, left, right, scope))
|
|
367
|
+
left = right
|
|
368
|
+
return parts[0] if len(parts) == 1 else "(" + " && ".join(parts) + ")"
|
|
369
|
+
|
|
370
|
+
def compare(self, op, lnode, rnode, scope):
|
|
371
|
+
a = self.expr(lnode, scope)
|
|
372
|
+
b = self.expr(rnode, scope)
|
|
373
|
+
none_l = isinstance(lnode, ast.Constant) and lnode.value is None
|
|
374
|
+
none_r = isinstance(rnode, ast.Constant) and rnode.value is None
|
|
375
|
+
if isinstance(op, (ast.Is, ast.IsNot)):
|
|
376
|
+
if none_l or none_r:
|
|
377
|
+
return f"({a} {'==' if isinstance(op, ast.Is) else '!='} {b})"
|
|
378
|
+
return f"({a} {'===' if isinstance(op, ast.Is) else '!=='} {b})"
|
|
379
|
+
if isinstance(op, (ast.Eq, ast.NotEq)):
|
|
380
|
+
prim = _is_prim(lnode) or _is_prim(rnode)
|
|
381
|
+
if none_l or none_r:
|
|
382
|
+
return f"({a} {'==' if isinstance(op, ast.Eq) else '!='} {b})"
|
|
383
|
+
if prim:
|
|
384
|
+
return f"({a} {'===' if isinstance(op, ast.Eq) else '!=='} {b})"
|
|
385
|
+
return f"$py.eq({a}, {b})" if isinstance(op, ast.Eq) else f"!$py.eq({a}, {b})"
|
|
386
|
+
if isinstance(op, ast.In):
|
|
387
|
+
return f"$py.contains({b}, {a})"
|
|
388
|
+
if isinstance(op, ast.NotIn):
|
|
389
|
+
return f"!$py.contains({b}, {a})"
|
|
390
|
+
sym = {ast.Lt: "<", ast.LtE: "<=", ast.Gt: ">", ast.GtE: ">="}[type(op)]
|
|
391
|
+
if isinstance(lnode, (ast.List, ast.Tuple)) or isinstance(rnode, (ast.List, ast.Tuple)):
|
|
392
|
+
return f"($py.cmp({a}, {b}) {sym} 0)"
|
|
393
|
+
return f"({a} {sym} {b})"
|
|
394
|
+
|
|
395
|
+
def is_bool(self, node, scope):
|
|
396
|
+
if isinstance(node, ast.Compare):
|
|
397
|
+
return True
|
|
398
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, bool):
|
|
399
|
+
return True
|
|
400
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
401
|
+
return True
|
|
402
|
+
if isinstance(node, ast.BoolOp):
|
|
403
|
+
return all(self.is_bool(v, scope) for v in node.values)
|
|
404
|
+
if isinstance(node, ast.Call):
|
|
405
|
+
f = node.func
|
|
406
|
+
if isinstance(f, ast.Name) and f.id in BOOL_CALLS:
|
|
407
|
+
return True
|
|
408
|
+
if isinstance(f, ast.Attribute) and f.attr in BOOL_METHODS:
|
|
409
|
+
return True
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
def test(self, node, scope):
|
|
413
|
+
"""Translate ``node`` for use as a condition (Python truthiness)."""
|
|
414
|
+
if isinstance(node, ast.BoolOp):
|
|
415
|
+
js_op = " && " if isinstance(node.op, ast.And) else " || "
|
|
416
|
+
return "(" + js_op.join(self.test(v, scope) for v in node.values) + ")"
|
|
417
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
418
|
+
return f"!{self.test(node.operand, scope)}"
|
|
419
|
+
js = self.expr(node, scope)
|
|
420
|
+
return js if self.is_bool(node, scope) else f"$py.truth({js})"
|
|
421
|
+
|
|
422
|
+
def x_Lambda(self, node, scope):
|
|
423
|
+
params, inner = self.params(node.args, scope)
|
|
424
|
+
self.fn_stack.append(_Fn("<lambda>"))
|
|
425
|
+
try:
|
|
426
|
+
body = self.expr(node.body, inner)
|
|
427
|
+
finally:
|
|
428
|
+
fn = self.fn_stack.pop()
|
|
429
|
+
prefix = "async " if fn.is_async else ""
|
|
430
|
+
return f"({prefix}({', '.join(params)}) => ({body}))"
|
|
431
|
+
|
|
432
|
+
def params(self, args, scope):
|
|
433
|
+
if args.vararg or args.kwarg or args.kwonlyargs or getattr(args, "posonlyargs", []):
|
|
434
|
+
raise self.error(args, "*args, **kwargs and keyword-only parameters are not supported in browser code")
|
|
435
|
+
names = {}
|
|
436
|
+
out = []
|
|
437
|
+
defaults = [None] * (len(args.args) - len(args.defaults)) + list(args.defaults)
|
|
438
|
+
for a, d in zip(args.args, defaults):
|
|
439
|
+
names[a.arg] = VALUE
|
|
440
|
+
out.append(jsname(a.arg) + (f" = {self.expr(d, scope)}" if d is not None else ""))
|
|
441
|
+
return out, scope.child(names, kind="function")
|
|
442
|
+
|
|
443
|
+
def x_Await(self, node, scope):
|
|
444
|
+
self.mark_async()
|
|
445
|
+
return f"(await {self.expr(node.value, scope)})"
|
|
446
|
+
|
|
447
|
+
def x_NamedExpr(self, node, scope):
|
|
448
|
+
raise self.error(node, "the walrus operator is not supported in browser code")
|
|
449
|
+
|
|
450
|
+
def x_Starred(self, node, scope):
|
|
451
|
+
raise self.error(node, "starred expressions are only supported inside list/tuple literals and calls")
|
|
452
|
+
|
|
453
|
+
# comprehensions --------------------------------------------------
|
|
454
|
+
def comp(self, node, scope, elt_fn, wrap):
|
|
455
|
+
for g in node.generators:
|
|
456
|
+
if g.is_async:
|
|
457
|
+
raise self.error(node, "async comprehensions are not supported")
|
|
458
|
+
return wrap(self._gen(node.generators, 0, scope, elt_fn))
|
|
459
|
+
|
|
460
|
+
def _gen(self, gens, i, scope, elt_fn):
|
|
461
|
+
g = gens[i]
|
|
462
|
+
it = self.expr(g.iter, scope)
|
|
463
|
+
inner = scope.child({n: VALUE for n in _target_names(g.target)})
|
|
464
|
+
target = self.target_pattern(g.target)
|
|
465
|
+
seq = f"$py.iter({it})"
|
|
466
|
+
for cond in g.ifs:
|
|
467
|
+
seq = f"{seq}.filter(({target}) => {self.test(cond, inner)})"
|
|
468
|
+
if i == len(gens) - 1:
|
|
469
|
+
elt = elt_fn(inner)
|
|
470
|
+
if elt == target and seq != f"$py.iter({it})":
|
|
471
|
+
return seq
|
|
472
|
+
return f"{seq}.map(({target}) => ({elt}))"
|
|
473
|
+
return f"{seq}.flatMap(({target}) => {self._gen(gens, i + 1, inner, elt_fn)})"
|
|
474
|
+
|
|
475
|
+
def x_ListComp(self, node, scope):
|
|
476
|
+
return self.comp(node, scope, lambda s: self.expr(node.elt, s), lambda s: s)
|
|
477
|
+
|
|
478
|
+
x_GeneratorExp = x_ListComp
|
|
479
|
+
|
|
480
|
+
def x_SetComp(self, node, scope):
|
|
481
|
+
return self.comp(node, scope, lambda s: self.expr(node.elt, s), lambda s: f"$py.set({s})")
|
|
482
|
+
|
|
483
|
+
def x_DictComp(self, node, scope):
|
|
484
|
+
return self.comp(node, scope,
|
|
485
|
+
lambda s: f"[{self.expr(node.key, s)}, {self.expr(node.value, s)}]",
|
|
486
|
+
lambda s: f"Object.fromEntries({s})")
|
|
487
|
+
|
|
488
|
+
def target_pattern(self, t):
|
|
489
|
+
if isinstance(t, ast.Name):
|
|
490
|
+
return jsname(t.id)
|
|
491
|
+
if isinstance(t, (ast.Tuple, ast.List)):
|
|
492
|
+
return "[" + ", ".join(self.target_pattern(e) for e in t.elts) + "]"
|
|
493
|
+
raise self.error(t, "only names and tuples can be loop targets in browser code")
|
|
494
|
+
|
|
495
|
+
# calls -------------------------------------------------------------
|
|
496
|
+
def args(self, node, scope):
|
|
497
|
+
out = []
|
|
498
|
+
for a in node.args:
|
|
499
|
+
if isinstance(a, ast.Starred):
|
|
500
|
+
out.append(f"...$py.iter({self.expr(a.value, scope)})")
|
|
501
|
+
else:
|
|
502
|
+
out.append(self.expr(a, scope))
|
|
503
|
+
return out
|
|
504
|
+
|
|
505
|
+
def kwargs_obj(self, node, scope):
|
|
506
|
+
if not node.keywords:
|
|
507
|
+
return None
|
|
508
|
+
items = []
|
|
509
|
+
for k in node.keywords:
|
|
510
|
+
if k.arg is None:
|
|
511
|
+
items.append(f"...{self.expr(k.value, scope)}")
|
|
512
|
+
else:
|
|
513
|
+
items.append(f"{json.dumps(k.arg)}: {self.expr(k.value, scope)}")
|
|
514
|
+
return "$py.kw({" + ", ".join(items) + "})"
|
|
515
|
+
|
|
516
|
+
def x_Call(self, node, scope):
|
|
517
|
+
f = node.func
|
|
518
|
+
if isinstance(f, ast.Name):
|
|
519
|
+
kind = scope.lookup(f.id)
|
|
520
|
+
if kind == SERVER:
|
|
521
|
+
return self.server_call(node, scope)
|
|
522
|
+
if kind == COMPONENT:
|
|
523
|
+
raise self.error(node, f"components are used as tags: <{f.id} ... />, not called")
|
|
524
|
+
if kind is None and f.id == "isinstance" and len(node.args) == 2:
|
|
525
|
+
types = node.args[1].elts if isinstance(node.args[1], ast.Tuple) else [node.args[1]]
|
|
526
|
+
names = []
|
|
527
|
+
for t in types:
|
|
528
|
+
if not (isinstance(t, ast.Name) and t.id in ("str", "int", "float", "bool", "list",
|
|
529
|
+
"dict", "set", "tuple", "type")):
|
|
530
|
+
raise self.error(node, "isinstance() in browser code supports builtin types only")
|
|
531
|
+
names.append(t.id)
|
|
532
|
+
return f"$py.isinstance({self.expr(node.args[0], scope)}, {json.dumps(names)})"
|
|
533
|
+
if kind is None and f.id in BUILTINS:
|
|
534
|
+
args = self.args(node, scope)
|
|
535
|
+
kw = self.kwargs_obj(node, scope)
|
|
536
|
+
if kw:
|
|
537
|
+
args.append(kw)
|
|
538
|
+
return f"$py.{f.id}({', '.join(args)})"
|
|
539
|
+
if kind == HELPER:
|
|
540
|
+
self.compile_helper(f.id, f)
|
|
541
|
+
call = self.positional_call(node, scope, jsname(f.id), self._params_of(self.ctx.helpers[f.id]))
|
|
542
|
+
if self.ctx.helper_async.get(f.id):
|
|
543
|
+
self.mark_async()
|
|
544
|
+
return f"(await {call})"
|
|
545
|
+
return call
|
|
546
|
+
if kind == HANDLER:
|
|
547
|
+
fn_node = scope.lookup("$fn:" + f.id)
|
|
548
|
+
params = self._params_of(fn_node) if isinstance(fn_node, ast.AST) else None
|
|
549
|
+
call = self.positional_call(node, scope, jsname(f.id), params)
|
|
550
|
+
if isinstance(fn_node, ast.AST) and getattr(fn_node, "_pyweb_will_be_async", False):
|
|
551
|
+
self.mark_async()
|
|
552
|
+
return f"(await {call})"
|
|
553
|
+
return call
|
|
554
|
+
callee = self.expr(f, scope)
|
|
555
|
+
if node.keywords:
|
|
556
|
+
raise self.error(node, f"keyword arguments are not supported when calling {f.id!r} from browser code")
|
|
557
|
+
return f"{callee}({', '.join(self.args(node, scope))})"
|
|
558
|
+
if isinstance(f, ast.Attribute):
|
|
559
|
+
self.expr(f.value, scope) # surfaces server-only names before other errors
|
|
560
|
+
root = _root_name(f.value)
|
|
561
|
+
root_kind = scope.lookup(root) if root else None
|
|
562
|
+
js_root = root_kind in ("browser_global",) or (root_kind is None and root in JS_GLOBALS)
|
|
563
|
+
if f.attr in PY_METHODS and not js_root:
|
|
564
|
+
args = self.args(node, scope)
|
|
565
|
+
kw = self.kwargs_obj(node, scope)
|
|
566
|
+
if kw:
|
|
567
|
+
args.append(kw)
|
|
568
|
+
argstr = "".join(", " + a for a in args)
|
|
569
|
+
path = self.state_path(f.value, scope)
|
|
570
|
+
if path is not None and f.attr in MUTATING:
|
|
571
|
+
sig, keys = path
|
|
572
|
+
return f"$py.mut({sig}, [{', '.join(keys)}], ($v) => $py.m($v, {json.dumps(f.attr)}{argstr}))"
|
|
573
|
+
return f"$py.m({self.expr(f.value, scope)}, {json.dumps(f.attr)}{argstr})"
|
|
574
|
+
if node.keywords:
|
|
575
|
+
raise self.error(node, f"keyword arguments are not supported for .{f.attr}() in browser code")
|
|
576
|
+
return f"{self.expr(f, scope)}({', '.join(self.args(node, scope))})"
|
|
577
|
+
if node.keywords:
|
|
578
|
+
raise self.error(node, "keyword arguments are not supported here in browser code")
|
|
579
|
+
return f"{self.expr(f, scope)}({', '.join(self.args(node, scope))})"
|
|
580
|
+
|
|
581
|
+
def _params_of(self, fn_node):
|
|
582
|
+
if fn_node is None:
|
|
583
|
+
return None
|
|
584
|
+
a = fn_node.args
|
|
585
|
+
return [p.arg for p in a.args]
|
|
586
|
+
|
|
587
|
+
def positional_call(self, node, scope, callee, params):
|
|
588
|
+
args = self.args(node, scope)
|
|
589
|
+
if node.keywords:
|
|
590
|
+
if params is None:
|
|
591
|
+
raise self.error(node, f"keyword arguments are not supported when calling {callee!r}")
|
|
592
|
+
slots = list(args) + ["undefined"] * (len(params) - len(args))
|
|
593
|
+
for k in node.keywords:
|
|
594
|
+
if k.arg is None or k.arg not in params:
|
|
595
|
+
raise self.error(node, f"{callee}() got an unexpected keyword argument {k.arg!r}")
|
|
596
|
+
slots[params.index(k.arg)] = self.expr(k.value, scope)
|
|
597
|
+
while slots and slots[-1] == "undefined":
|
|
598
|
+
slots.pop()
|
|
599
|
+
args = slots
|
|
600
|
+
return f"{callee}({', '.join(args)})"
|
|
601
|
+
|
|
602
|
+
def server_call(self, node, scope):
|
|
603
|
+
name = node.func.id
|
|
604
|
+
params = self.ctx.server_fns[name]
|
|
605
|
+
if any(isinstance(a, ast.Starred) for a in node.args) or any(k.arg is None for k in node.keywords):
|
|
606
|
+
raise self.error(node, f"*args/**kwargs are not supported when calling server function {name!r}")
|
|
607
|
+
if len(node.args) > len(params):
|
|
608
|
+
raise self.error(node, f"{name}() takes {len(params)} arguments but {len(node.args)} were given")
|
|
609
|
+
fields = []
|
|
610
|
+
for p, a in zip(params, node.args):
|
|
611
|
+
fields.append(f"{json.dumps(p)}: {self.expr(a, scope)}")
|
|
612
|
+
for k in node.keywords:
|
|
613
|
+
if k.arg not in params:
|
|
614
|
+
raise self.error(node, f"{name}() got an unexpected keyword argument {k.arg!r}")
|
|
615
|
+
fields.append(f"{json.dumps(k.arg)}: {self.expr(k.value, scope)}")
|
|
616
|
+
self.mark_async()
|
|
617
|
+
caller = self.fn_stack[-1].name if self.fn_stack else "<expr>"
|
|
618
|
+
self.ctx.rpc_calls.setdefault(name, set()).add(caller)
|
|
619
|
+
return f"(await $rpc({json.dumps(name)}, {{{', '.join(fields)}}}))"
|
|
620
|
+
|
|
621
|
+
def _calls_server(self, node, scope):
|
|
622
|
+
for sub in ast.walk(node):
|
|
623
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name) \
|
|
624
|
+
and scope.lookup(sub.func.id) == SERVER:
|
|
625
|
+
return True
|
|
626
|
+
return False
|
|
627
|
+
|
|
628
|
+
def state_path(self, node, scope):
|
|
629
|
+
"""``todos[i]["done"]`` rooted at a signal → (signal_js, [key_js...])."""
|
|
630
|
+
keys = []
|
|
631
|
+
cur = node
|
|
632
|
+
while True:
|
|
633
|
+
if isinstance(cur, ast.Name):
|
|
634
|
+
if scope.lookup(cur.id) == SIGNAL:
|
|
635
|
+
return jsname(cur.id), list(reversed(keys))
|
|
636
|
+
return None
|
|
637
|
+
if isinstance(cur, ast.Subscript):
|
|
638
|
+
if isinstance(cur.slice, ast.Slice):
|
|
639
|
+
return None
|
|
640
|
+
keys.append(self.expr(cur.slice, scope))
|
|
641
|
+
cur = cur.value
|
|
642
|
+
elif isinstance(cur, ast.Attribute):
|
|
643
|
+
keys.append(json.dumps(cur.attr))
|
|
644
|
+
cur = cur.value
|
|
645
|
+
else:
|
|
646
|
+
return None
|
|
647
|
+
|
|
648
|
+
# -------------------------------------------------------- statements
|
|
649
|
+
def block(self, stmts, scope, ind):
|
|
650
|
+
out = []
|
|
651
|
+
for s in stmts:
|
|
652
|
+
out.extend(self.stmt(s, scope, ind))
|
|
653
|
+
return out
|
|
654
|
+
|
|
655
|
+
def stmt(self, node, scope, ind):
|
|
656
|
+
m = getattr(self, "s_" + type(node).__name__, None)
|
|
657
|
+
if m is None:
|
|
658
|
+
raise self.error(node, f"{type(node).__name__} statements are not supported in browser code")
|
|
659
|
+
return m(node, scope, ind)
|
|
660
|
+
|
|
661
|
+
def s_Expr(self, node, scope, ind):
|
|
662
|
+
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
663
|
+
return [] # docstring
|
|
664
|
+
if _is_ui_placeholder(node):
|
|
665
|
+
return []
|
|
666
|
+
return [f"{ind}{self.expr(node.value, scope)};"]
|
|
667
|
+
|
|
668
|
+
def s_Pass(self, node, scope, ind):
|
|
669
|
+
return []
|
|
670
|
+
|
|
671
|
+
def s_Break(self, node, scope, ind):
|
|
672
|
+
return [f"{ind}break;"]
|
|
673
|
+
|
|
674
|
+
def s_Continue(self, node, scope, ind):
|
|
675
|
+
return [f"{ind}continue;"]
|
|
676
|
+
|
|
677
|
+
def s_Global(self, node, scope, ind):
|
|
678
|
+
return []
|
|
679
|
+
|
|
680
|
+
s_Nonlocal = s_Global
|
|
681
|
+
|
|
682
|
+
def s_Return(self, node, scope, ind):
|
|
683
|
+
if node.value is None:
|
|
684
|
+
return [f"{ind}return;"]
|
|
685
|
+
return [f"{ind}return {self.expr(node.value, scope)};"]
|
|
686
|
+
|
|
687
|
+
def s_If(self, node, scope, ind):
|
|
688
|
+
out = [f"{ind}if ({self.test(node.test, scope)}) {{"]
|
|
689
|
+
out += self.block(node.body, scope, ind + " ")
|
|
690
|
+
orelse = node.orelse
|
|
691
|
+
while len(orelse) == 1 and isinstance(orelse[0], ast.If):
|
|
692
|
+
nxt = orelse[0]
|
|
693
|
+
out.append(f"{ind}}} else if ({self.test(nxt.test, scope)}) {{")
|
|
694
|
+
out += self.block(nxt.body, scope, ind + " ")
|
|
695
|
+
orelse = nxt.orelse
|
|
696
|
+
if orelse:
|
|
697
|
+
out.append(f"{ind}}} else {{")
|
|
698
|
+
out += self.block(orelse, scope, ind + " ")
|
|
699
|
+
out.append(f"{ind}}}")
|
|
700
|
+
return out
|
|
701
|
+
|
|
702
|
+
def s_While(self, node, scope, ind):
|
|
703
|
+
if node.orelse:
|
|
704
|
+
raise self.error(node, "while/else is not supported in browser code")
|
|
705
|
+
out = [f"{ind}while ({self.test(node.test, scope)}) {{"]
|
|
706
|
+
out += self.block(node.body, scope, ind + " ")
|
|
707
|
+
out.append(f"{ind}}}")
|
|
708
|
+
return out
|
|
709
|
+
|
|
710
|
+
def s_For(self, node, scope, ind):
|
|
711
|
+
if node.orelse:
|
|
712
|
+
raise self.error(node, "for/else is not supported in browser code")
|
|
713
|
+
for n in _target_names(node.target):
|
|
714
|
+
self.ensure_assignable(n, scope, node)
|
|
715
|
+
it = self.expr(node.iter, scope)
|
|
716
|
+
out = [f"{ind}for ({self.target_pattern(node.target)} of $py.iter({it})) {{"]
|
|
717
|
+
out += self.block(node.body, scope, ind + " ")
|
|
718
|
+
out.append(f"{ind}}}")
|
|
719
|
+
return out
|
|
720
|
+
|
|
721
|
+
def ensure_assignable(self, name, scope, node):
|
|
722
|
+
kind = scope.lookup(name)
|
|
723
|
+
if kind in (COMPUTED, CONST, PROP, MODCONST):
|
|
724
|
+
raise self.error(node, f"cannot assign to {name!r}: it is derived/read-only here")
|
|
725
|
+
if kind in (SERVER, HELPER, COMPONENT, SERVER_ONLY):
|
|
726
|
+
raise self.error(node, f"cannot assign to {name!r} in browser code")
|
|
727
|
+
|
|
728
|
+
def s_Assign(self, node, scope, ind):
|
|
729
|
+
value = self.expr(node.value, scope)
|
|
730
|
+
if len(node.targets) == 1:
|
|
731
|
+
return self.assign(node.targets[0], value, scope, ind, node)
|
|
732
|
+
t = self.tmp()
|
|
733
|
+
out = [f"{ind}{{ const {t} = {value};"]
|
|
734
|
+
for tgt in node.targets:
|
|
735
|
+
out += self.assign(tgt, t, scope, ind + " ", node)
|
|
736
|
+
out.append(f"{ind}}}")
|
|
737
|
+
return out
|
|
738
|
+
|
|
739
|
+
def s_AnnAssign(self, node, scope, ind):
|
|
740
|
+
if node.value is None:
|
|
741
|
+
return []
|
|
742
|
+
return self.assign(node.target, self.expr(node.value, scope), scope, ind, node)
|
|
743
|
+
|
|
744
|
+
def assign(self, target, value, scope, ind, node):
|
|
745
|
+
if isinstance(target, ast.Name):
|
|
746
|
+
self.ensure_assignable(target.id, scope, node)
|
|
747
|
+
kind = scope.lookup(target.id)
|
|
748
|
+
js = jsname(target.id)
|
|
749
|
+
if kind == SIGNAL:
|
|
750
|
+
return [f"{ind}{js}({value});"]
|
|
751
|
+
return [f"{ind}{js} = {value};"]
|
|
752
|
+
if isinstance(target, (ast.Tuple, ast.List)):
|
|
753
|
+
t = self.tmp()
|
|
754
|
+
out = [f"{ind}{{ const {t} = $py.list({value});"]
|
|
755
|
+
if any(isinstance(e, ast.Starred) for e in target.elts):
|
|
756
|
+
raise self.error(node, "starred assignment is not supported in browser code")
|
|
757
|
+
out.append(f"{ind} if ({t}.length !== {len(target.elts)}) throw $py.error(\"ValueError\", "
|
|
758
|
+
f"\"expected {len(target.elts)} values to unpack\");")
|
|
759
|
+
for i, e in enumerate(target.elts):
|
|
760
|
+
out += self.assign(e, f"{t}[{i}]", scope, ind + " ", node)
|
|
761
|
+
out.append(f"{ind}}}")
|
|
762
|
+
return out
|
|
763
|
+
if isinstance(target, (ast.Subscript, ast.Attribute)):
|
|
764
|
+
path = self.state_path(target, scope)
|
|
765
|
+
if path is not None:
|
|
766
|
+
sig, keys = path
|
|
767
|
+
return [f"{ind}$py.setp({sig}, [{', '.join(keys)}], {value});"]
|
|
768
|
+
if isinstance(target, ast.Subscript):
|
|
769
|
+
if isinstance(target.slice, ast.Slice):
|
|
770
|
+
raise self.error(node, "slice assignment is not supported in browser code")
|
|
771
|
+
return [f"{ind}$py.setitem({self.expr(target.value, scope)}, "
|
|
772
|
+
f"{self.expr(target.slice, scope)}, {value});"]
|
|
773
|
+
return [f"{ind}{self.expr(target.value, scope)}.{target.attr} = {value};"]
|
|
774
|
+
raise self.error(node, f"cannot assign to {type(target).__name__} in browser code")
|
|
775
|
+
|
|
776
|
+
def s_AugAssign(self, node, scope, ind):
|
|
777
|
+
t = node.target
|
|
778
|
+
if isinstance(t, ast.Name):
|
|
779
|
+
self.ensure_assignable(t.id, scope, node)
|
|
780
|
+
js = jsname(t.id)
|
|
781
|
+
kind = scope.lookup(t.id)
|
|
782
|
+
cur = f"{js}()" if kind == SIGNAL else js
|
|
783
|
+
new = self.binop(node.op, t, node.value, scope, ljs=cur)
|
|
784
|
+
if kind == SIGNAL:
|
|
785
|
+
return [f"{ind}{js}({_strip_parens(new)});"]
|
|
786
|
+
return [f"{ind}{js} = {_strip_parens(new)};"]
|
|
787
|
+
path = self.state_path(t, scope)
|
|
788
|
+
if path is not None:
|
|
789
|
+
sig, keys = path
|
|
790
|
+
k = self.tmp("k")
|
|
791
|
+
new = self.binop(node.op, t, node.value, scope, ljs=f"$py.getp({sig}.peek(), {k})")
|
|
792
|
+
return [f"{ind}{{ const {k} = [{', '.join(keys)}]; $py.setp({sig}, {k}, {new}); }}"]
|
|
793
|
+
if isinstance(t, ast.Subscript):
|
|
794
|
+
obj, key = self.tmp("o"), self.tmp("k")
|
|
795
|
+
new = self.binop(node.op, t, node.value, scope, ljs=f"$py.at({obj}, {key})")
|
|
796
|
+
return [f"{ind}{{ const {obj} = {self.expr(t.value, scope)}, {key} = {self.expr(t.slice, scope)}; "
|
|
797
|
+
f"$py.setitem({obj}, {key}, {new}); }}"]
|
|
798
|
+
if isinstance(t, ast.Attribute):
|
|
799
|
+
obj = self.tmp("o")
|
|
800
|
+
new = self.binop(node.op, t, node.value, scope, ljs=f"{obj}.{t.attr}")
|
|
801
|
+
return [f"{ind}{{ const {obj} = {self.expr(t.value, scope)}; {obj}.{t.attr} = {new}; }}"]
|
|
802
|
+
raise self.error(node, "unsupported augmented assignment target")
|
|
803
|
+
|
|
804
|
+
def s_Delete(self, node, scope, ind):
|
|
805
|
+
out = []
|
|
806
|
+
for t in node.targets:
|
|
807
|
+
path = self.state_path(t, scope) if isinstance(t, (ast.Subscript, ast.Attribute)) else None
|
|
808
|
+
if path is not None and path[1]:
|
|
809
|
+
sig, keys = path
|
|
810
|
+
out.append(f"{ind}$py.delp({sig}, [{', '.join(keys)}]);")
|
|
811
|
+
elif isinstance(t, ast.Subscript) and not isinstance(t.slice, ast.Slice):
|
|
812
|
+
out.append(f"{ind}$py.delitem({self.expr(t.value, scope)}, {self.expr(t.slice, scope)});")
|
|
813
|
+
else:
|
|
814
|
+
raise self.error(node, "only `del x[key]` is supported in browser code")
|
|
815
|
+
return out
|
|
816
|
+
|
|
817
|
+
def s_Raise(self, node, scope, ind):
|
|
818
|
+
if node.exc is None:
|
|
819
|
+
return [f"{ind}throw $err;"]
|
|
820
|
+
exc = node.exc
|
|
821
|
+
if isinstance(exc, ast.Call) and isinstance(exc.func, ast.Name) \
|
|
822
|
+
and scope.lookup(exc.func.id) is None and exc.func.id[:1].isupper():
|
|
823
|
+
msg = self.expr(exc.args[0], scope) if exc.args else "null"
|
|
824
|
+
return [f"{ind}throw $py.error({json.dumps(exc.func.id)}, {msg});"]
|
|
825
|
+
if isinstance(exc, ast.Name) and scope.lookup(exc.id) is None and exc.id[:1].isupper():
|
|
826
|
+
return [f"{ind}throw $py.error({json.dumps(exc.id)}, null);"]
|
|
827
|
+
return [f"{ind}throw {self.expr(exc, scope)};"]
|
|
828
|
+
|
|
829
|
+
def s_Assert(self, node, scope, ind):
|
|
830
|
+
msg = self.expr(node.msg, scope) if node.msg is not None else "null"
|
|
831
|
+
return [f"{ind}if (!{self.test(node.test, scope)}) throw $py.error(\"AssertionError\", {msg});"]
|
|
832
|
+
|
|
833
|
+
def s_Try(self, node, scope, ind):
|
|
834
|
+
if node.orelse:
|
|
835
|
+
raise self.error(node, "try/else is not supported in browser code")
|
|
836
|
+
out = [f"{ind}try {{"]
|
|
837
|
+
out += self.block(node.body, scope, ind + " ")
|
|
838
|
+
if node.handlers:
|
|
839
|
+
out.append(f"{ind}}} catch ($err) {{")
|
|
840
|
+
first = True
|
|
841
|
+
for h in node.handlers:
|
|
842
|
+
types = _exc_names(h.type)
|
|
843
|
+
if types is None:
|
|
844
|
+
raise self.error(h, "except clauses must name exception classes")
|
|
845
|
+
cond = "true" if not types else f"$py.exc($err, {json.dumps(types)})"
|
|
846
|
+
kw = "if" if first else "} else if"
|
|
847
|
+
out.append(f"{ind} {kw} ({cond}) {{")
|
|
848
|
+
if h.name:
|
|
849
|
+
self.ensure_assignable(h.name, scope, h)
|
|
850
|
+
out.append(f"{ind} {jsname(h.name)} = $err;")
|
|
851
|
+
out += self.block(h.body, scope, ind + " ")
|
|
852
|
+
first = False
|
|
853
|
+
out.append(f"{ind} }} else {{ throw $err; }}")
|
|
854
|
+
if node.finalbody:
|
|
855
|
+
out.append(f"{ind}}} finally {{")
|
|
856
|
+
out += self.block(node.finalbody, scope, ind + " ")
|
|
857
|
+
out.append(f"{ind}}}")
|
|
858
|
+
return out
|
|
859
|
+
|
|
860
|
+
def s_FunctionDef(self, node, scope, ind):
|
|
861
|
+
return self.function(node, scope, ind).splitlines()
|
|
862
|
+
|
|
863
|
+
s_AsyncFunctionDef = s_FunctionDef
|
|
864
|
+
|
|
865
|
+
def s_With(self, node, scope, ind):
|
|
866
|
+
raise self.error(node, "`with` is not supported in browser code")
|
|
867
|
+
|
|
868
|
+
def s_Import(self, node, scope, ind):
|
|
869
|
+
raise self.error(node, "imports inside browser code are not supported; import at module level")
|
|
870
|
+
|
|
871
|
+
s_ImportFrom = s_Import
|
|
872
|
+
|
|
873
|
+
def s_ClassDef(self, node, scope, ind):
|
|
874
|
+
raise self.error(node, "classes are not supported in browser code")
|
|
875
|
+
|
|
876
|
+
# --------------------------------------------------------- functions
|
|
877
|
+
def function(self, node, scope, ind="", name=None):
|
|
878
|
+
"""Translate a ``def`` to a JS function declaration."""
|
|
879
|
+
if node.decorator_list:
|
|
880
|
+
raise self.error(node, f"decorators are not supported on browser function {node.name!r}")
|
|
881
|
+
params, inner = self.params(node.args, scope)
|
|
882
|
+
local_names = _local_names(node) - {a.arg for a in node.args.args}
|
|
883
|
+
for n in sorted(local_names):
|
|
884
|
+
kind = scope.lookup(n)
|
|
885
|
+
if kind in (SIGNAL,):
|
|
886
|
+
continue # PyWeb rule: page state is shared with handlers
|
|
887
|
+
if kind in (COMPUTED, CONST, PROP, MODCONST):
|
|
888
|
+
raise self.error(node, f"{node.name}() assigns to {n!r}, which is derived/read-only")
|
|
889
|
+
inner.names[n] = VALUE
|
|
890
|
+
for sub in node.body:
|
|
891
|
+
if isinstance(sub, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
892
|
+
inner.names[sub.name] = HANDLER
|
|
893
|
+
inner.names["$fn:" + sub.name] = sub
|
|
894
|
+
fn = _Fn(node.name)
|
|
895
|
+
self.fn_stack.append(fn)
|
|
896
|
+
try:
|
|
897
|
+
body = self.block(node.body, inner, ind + " ")
|
|
898
|
+
finally:
|
|
899
|
+
self.fn_stack.pop()
|
|
900
|
+
decl = [n for n in sorted(local_names) if inner.names.get(n) == VALUE
|
|
901
|
+
and not any(isinstance(s, (ast.FunctionDef, ast.AsyncFunctionDef)) and s.name == n
|
|
902
|
+
for s in node.body)]
|
|
903
|
+
is_async = fn.is_async or isinstance(node, ast.AsyncFunctionDef) \
|
|
904
|
+
or getattr(node, "_pyweb_will_be_async", False)
|
|
905
|
+
head = f"{ind}{'async ' if is_async else ''}function {jsname(name or node.name)}({', '.join(params)}) {{"
|
|
906
|
+
lines = [head]
|
|
907
|
+
if decl:
|
|
908
|
+
lines.append(f"{ind} let {', '.join(jsname(d) for d in decl)};")
|
|
909
|
+
lines += body
|
|
910
|
+
lines.append(f"{ind}}}")
|
|
911
|
+
node._pyweb_async = is_async
|
|
912
|
+
return "\n".join(lines)
|
|
913
|
+
|
|
914
|
+
def compile_helper(self, name, at_node):
|
|
915
|
+
if name in self.ctx.helper_js:
|
|
916
|
+
return
|
|
917
|
+
fn = self.ctx.helpers[name]
|
|
918
|
+
self.ctx.helper_js[name] = None # in progress (recursion guard)
|
|
919
|
+
try:
|
|
920
|
+
saved = self.fn_stack
|
|
921
|
+
self.fn_stack = []
|
|
922
|
+
try:
|
|
923
|
+
js = self.function(fn, self.ctx.module_scope())
|
|
924
|
+
finally:
|
|
925
|
+
self.fn_stack = saved
|
|
926
|
+
except CompileError as exc:
|
|
927
|
+
del self.ctx.helper_js[name]
|
|
928
|
+
raise CompileError(
|
|
929
|
+
f"{name}() is called from browser code but cannot run in the browser: {exc.msg}. "
|
|
930
|
+
f"Mark it @server to run it on the server instead",
|
|
931
|
+
exc.lineno or getattr(at_node, "lineno", None), self.ctx.filename) from None
|
|
932
|
+
self.ctx.helper_js[name] = js
|
|
933
|
+
self.ctx.helper_async[name] = getattr(fn, "_pyweb_async", False)
|
|
934
|
+
self.ctx.used_helpers.append(name)
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
# ----------------------------------------------------------------- utils
|
|
938
|
+
|
|
939
|
+
def _is_num(node):
|
|
940
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)) and not isinstance(node.value, bool):
|
|
941
|
+
return True
|
|
942
|
+
return isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and _is_num(node.operand)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
_NUMERIC_CALLS = {"len", "int", "float", "abs", "round", "ord"}
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def _is_numeric(node):
|
|
949
|
+
"""Provably a number (so JS ``+`` matches Python)."""
|
|
950
|
+
if _is_num(node):
|
|
951
|
+
return True
|
|
952
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in _NUMERIC_CALLS:
|
|
953
|
+
return True
|
|
954
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Sub, ast.Div, ast.FloorDiv, ast.Pow)):
|
|
955
|
+
return True
|
|
956
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mult, ast.Mod)):
|
|
957
|
+
return _is_numeric(node.left) and _is_numeric(node.right)
|
|
958
|
+
return isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)) and _is_numeric(node.operand)
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def _is_str(node):
|
|
962
|
+
return (isinstance(node, ast.Constant) and isinstance(node.value, str)) or isinstance(node, ast.JoinedStr)
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def _is_prim(node):
|
|
966
|
+
return (isinstance(node, ast.Constant) and node.value is not None) or _is_num(node) or isinstance(node, ast.JoinedStr)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _strip_parens(js):
|
|
970
|
+
return js[1:-1] if js.startswith("(") and js.endswith(")") and _balanced(js[1:-1]) else js
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def _balanced(s):
|
|
974
|
+
depth = 0
|
|
975
|
+
for c in s:
|
|
976
|
+
if c == "(":
|
|
977
|
+
depth += 1
|
|
978
|
+
elif c == ")":
|
|
979
|
+
depth -= 1
|
|
980
|
+
if depth < 0:
|
|
981
|
+
return False
|
|
982
|
+
return depth == 0
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _root_name(node):
|
|
986
|
+
while isinstance(node, (ast.Attribute, ast.Subscript, ast.Call)):
|
|
987
|
+
node = node.func if isinstance(node, ast.Call) else node.value
|
|
988
|
+
return node.id if isinstance(node, ast.Name) else None
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def _target_names(t):
|
|
992
|
+
if isinstance(t, ast.Name):
|
|
993
|
+
return [t.id]
|
|
994
|
+
if isinstance(t, (ast.Tuple, ast.List)):
|
|
995
|
+
out = []
|
|
996
|
+
for e in t.elts:
|
|
997
|
+
out += _target_names(e)
|
|
998
|
+
return out
|
|
999
|
+
return []
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def _exc_names(t):
|
|
1003
|
+
if t is None:
|
|
1004
|
+
return []
|
|
1005
|
+
if isinstance(t, ast.Name):
|
|
1006
|
+
return [t.id]
|
|
1007
|
+
if isinstance(t, ast.Attribute):
|
|
1008
|
+
return [t.attr]
|
|
1009
|
+
if isinstance(t, ast.Tuple):
|
|
1010
|
+
out = []
|
|
1011
|
+
for e in t.elts:
|
|
1012
|
+
sub = _exc_names(e)
|
|
1013
|
+
if sub is None:
|
|
1014
|
+
return None
|
|
1015
|
+
out += sub
|
|
1016
|
+
return out
|
|
1017
|
+
return None
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def _is_ui_placeholder(node):
|
|
1021
|
+
v = node.value if isinstance(node, ast.Expr) else None
|
|
1022
|
+
return (isinstance(v, ast.Call) and isinstance(v.func, ast.Name)
|
|
1023
|
+
and v.func.id == "__pyweb_ui__")
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _local_names(fn):
|
|
1027
|
+
"""Names a function assigns (excluding nested function/lambda/comprehension scopes)."""
|
|
1028
|
+
out = set()
|
|
1029
|
+
|
|
1030
|
+
def visit(node):
|
|
1031
|
+
for child in ast.iter_child_nodes(node):
|
|
1032
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
1033
|
+
out.add(child.name)
|
|
1034
|
+
continue
|
|
1035
|
+
if isinstance(child, (ast.Lambda, ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
|
|
1036
|
+
continue
|
|
1037
|
+
if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store):
|
|
1038
|
+
out.add(child.id)
|
|
1039
|
+
if isinstance(child, ast.ExceptHandler) and child.name:
|
|
1040
|
+
out.add(child.name)
|
|
1041
|
+
visit(child)
|
|
1042
|
+
|
|
1043
|
+
for stmt in fn.body:
|
|
1044
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
1045
|
+
out.add(stmt.name)
|
|
1046
|
+
continue
|
|
1047
|
+
if isinstance(stmt, ast.Name):
|
|
1048
|
+
continue
|
|
1049
|
+
tmp = ast.Module(body=[stmt], type_ignores=[])
|
|
1050
|
+
visit(tmp)
|
|
1051
|
+
return out
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def mark_async_handlers(handlers, scope):
|
|
1055
|
+
"""Pre-pass: flag handlers that will compile to ``async`` functions.
|
|
1056
|
+
|
|
1057
|
+
A handler is async if it awaits, calls a server function, or calls
|
|
1058
|
+
another async handler (fixpoint). Callers then ``await`` it.
|
|
1059
|
+
"""
|
|
1060
|
+
is_async = {}
|
|
1061
|
+
for name, fn in handlers.items():
|
|
1062
|
+
direct = isinstance(fn, ast.AsyncFunctionDef)
|
|
1063
|
+
for sub in ast.walk(fn):
|
|
1064
|
+
if isinstance(sub, ast.Await):
|
|
1065
|
+
direct = True
|
|
1066
|
+
elif isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name) \
|
|
1067
|
+
and scope.lookup(sub.func.id) == SERVER:
|
|
1068
|
+
direct = True
|
|
1069
|
+
is_async[name] = direct
|
|
1070
|
+
changed = True
|
|
1071
|
+
while changed:
|
|
1072
|
+
changed = False
|
|
1073
|
+
for name, fn in handlers.items():
|
|
1074
|
+
if is_async[name]:
|
|
1075
|
+
continue
|
|
1076
|
+
for sub in ast.walk(fn):
|
|
1077
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name) \
|
|
1078
|
+
and is_async.get(sub.func.id):
|
|
1079
|
+
is_async[name] = changed = True
|
|
1080
|
+
break
|
|
1081
|
+
for name, fn in handlers.items():
|
|
1082
|
+
fn._pyweb_will_be_async = is_async[name]
|
|
1083
|
+
return is_async
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def ui_placeholder(node):
|
|
1087
|
+
return _is_ui_placeholder(node)
|