Delete account/sx/sx_components.py — all rendering now in .sx

Phase 1 of zero-Python rendering: account service.

- Auth pages (login, device, check-email) use _render_auth_page() helper
  calling render_to_sx() + full_page_sx() directly in routes
- Newsletter toggle POST renders inline via render_to_sx()
- Newsletter page helper returns data dict; defpage :data slot fetches,
  :content slot renders via ~account-newsletters-content defcomp
- Fragment page uses (frag ...) IO primitive directly in .sx
- Defpage _eval_slot now uses async_eval_slot_to_sx which expands
  component bodies server-side (executing IO) but serializes tags as SX
- Fix pre-existing OOB ParseError: _eval_slot was producing HTML instead
  of s-expressions for component content slots
- Fix market url_for endpoint: defpage_market_home (app-level, not blueprint)
- Fix events calendar nav: wrap multiple SX parts in fragment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 01:16:01 +00:00
parent 44503a7d9b
commit 400667b15a
10 changed files with 173 additions and 228 deletions

View File

@@ -910,6 +910,42 @@ async def async_eval_to_sx(
return serialize(result)
async def async_eval_slot_to_sx(
expr: Any,
env: dict[str, Any],
ctx: RequestContext | None = None,
) -> str:
"""Like async_eval_to_sx but expands component calls.
Used by defpage slot evaluation where the content expression is
typically a component call like ``(~dashboard-content)``. Normal
``async_eval_to_sx`` serializes component calls without expanding;
this variant expands one level so IO primitives in the body execute,
then serializes the result as SX wire format.
"""
if ctx is None:
ctx = RequestContext()
# If expr is a component call, expand it through _aser
if isinstance(expr, list) and expr:
head = expr[0]
if isinstance(head, Symbol) and head.name.startswith("~"):
comp = env.get(head.name)
if isinstance(comp, Component):
result = await _aser_component(comp, expr[1:], env, ctx)
if isinstance(result, SxExpr):
return result.source
if result is None or result is NIL:
return ""
return serialize(result)
# Fall back to normal async_eval_to_sx
result = await _aser(expr, env, ctx)
if isinstance(result, SxExpr):
return result.source
if result is None or result is NIL:
return ""
return serialize(result)
async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
"""Evaluate *expr*, producing SxExpr for rendering forms, raw values
for everything else."""
@@ -1022,6 +1058,33 @@ async def _aser_fragment(children: list, env: dict, ctx: RequestContext) -> SxEx
return SxExpr("(<> " + " ".join(parts) + ")")
async def _aser_component(
comp: Component, args: list, env: dict, ctx: RequestContext,
) -> Any:
"""Expand a component body through _aser — produces SX, not HTML."""
kwargs: dict[str, Any] = {}
children: list[Any] = []
i = 0
while i < len(args):
arg = args[i]
if isinstance(arg, Keyword) and i + 1 < len(args):
kwargs[arg.name] = await async_eval(args[i + 1], env, ctx)
i += 2
else:
children.append(arg)
i += 1
local = dict(comp.closure)
local.update(env)
for p in comp.params:
local[p] = kwargs.get(p, NIL)
if comp.has_children:
child_parts = []
for c in children:
child_parts.append(serialize(await _aser(c, env, ctx)))
local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")")
return await _aser(comp.body, local, ctx)
async def _aser_call(
name: str, args: list, env: dict, ctx: RequestContext,
) -> SxExpr: