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.
Files changed (54) hide show
  1. pyweb/__init__.py +71 -0
  2. pyweb/app.py +37 -0
  3. pyweb/app_loader.py +139 -0
  4. pyweb/asgi.py +71 -0
  5. pyweb/auth.py +701 -0
  6. pyweb/bench.py +101 -0
  7. pyweb/browser.py +232 -0
  8. pyweb/build.py +295 -0
  9. pyweb/cache.py +201 -0
  10. pyweb/cli/__init__.py +465 -0
  11. pyweb/cli/__main__.py +3 -0
  12. pyweb/compiler/__init__.py +5 -0
  13. pyweb/compiler/ast.py +45 -0
  14. pyweb/compiler/codegen/__init__.py +9 -0
  15. pyweb/compiler/codegen/ir.py +50 -0
  16. pyweb/compiler/errors.py +29 -0
  17. pyweb/compiler/lower.py +733 -0
  18. pyweb/compiler/parser.py +532 -0
  19. pyweb/compiler/pipeline.py +158 -0
  20. pyweb/compiler/pyjs.py +1087 -0
  21. pyweb/compiler/rpc.py +35 -0
  22. pyweb/context.py +193 -0
  23. pyweb/css.py +115 -0
  24. pyweb/db/__init__.py +652 -0
  25. pyweb/db/migrate.py +143 -0
  26. pyweb/decorators.py +49 -0
  27. pyweb/deploy.py +102 -0
  28. pyweb/forms.py +108 -0
  29. pyweb/hosting.py +139 -0
  30. pyweb/jobs.py +248 -0
  31. pyweb/live.py +88 -0
  32. pyweb/lsp.py +214 -0
  33. pyweb/models.py +269 -0
  34. pyweb/npm.py +270 -0
  35. pyweb/observability.py +313 -0
  36. pyweb/platform.py +69 -0
  37. pyweb/plugins.py +112 -0
  38. pyweb/py.typed +0 -0
  39. pyweb/realtime.py +189 -0
  40. pyweb/rpc.py +137 -0
  41. pyweb/runtime/browser/runtime.js +1151 -0
  42. pyweb/runtime/server/__init__.py +445 -0
  43. pyweb/security.py +162 -0
  44. pyweb/serve.py +335 -0
  45. pyweb/ssr.py +260 -0
  46. pyweb/sync.py +118 -0
  47. pyweb/testing.py +154 -0
  48. pyweb/uploads.py +94 -0
  49. pyweb_stack-0.1.0.dist-info/METADATA +198 -0
  50. pyweb_stack-0.1.0.dist-info/RECORD +54 -0
  51. pyweb_stack-0.1.0.dist-info/WHEEL +5 -0
  52. pyweb_stack-0.1.0.dist-info/entry_points.txt +2 -0
  53. pyweb_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
  54. 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)