Make SxExpr a str subclass, sx_call/render functions return SxExpr
SxExpr is now a str subclass so it works everywhere a plain string does (join, isinstance, f-strings) while serialize() still emits it unquoted. sx_call() and all internal render functions (_render_to_sx, async_eval_to_sx, etc.) return SxExpr, eliminating the "forgot to wrap" bug class that caused the sx_content leak and list serialization bugs. - Phase 0: SxExpr(str) with .source property, __add__/__radd__ - Phase 1: sx_call returns SxExpr (drop-in, all 200+ sites unchanged) - Phase 2: async_eval_to_sx, async_eval_slot_to_sx, _render_to_sx, mobile_menu_sx return SxExpr; remove isinstance(str) workaround - Phase 3: Remove ~150 redundant SxExpr() wrappings across 45 files - Phase 4: serialize() docstring, handler return docs, ;; returns: sx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,31 +25,37 @@ from .types import Keyword, Symbol, NIL
|
||||
# SxExpr — pre-built sx source marker
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class SxExpr:
|
||||
class SxExpr(str):
|
||||
"""Pre-built sx source that serialize() outputs unquoted.
|
||||
|
||||
``SxExpr`` is a ``str`` subclass, so it works everywhere a plain
|
||||
string does (join, startswith, f-strings, isinstance checks). The
|
||||
only difference: ``serialize()`` emits it unquoted instead of
|
||||
wrapping it in double-quotes.
|
||||
|
||||
Use this to nest sx call strings inside other sx_call() invocations
|
||||
without them being quoted as strings::
|
||||
|
||||
sx_call("parent", child=SxExpr(sx_call("child", x=1)))
|
||||
sx_call("parent", child=sx_call("child", x=1))
|
||||
# => (~parent :child (~child :x 1))
|
||||
"""
|
||||
__slots__ = ("source",)
|
||||
|
||||
def __init__(self, source: str):
|
||||
self.source = source
|
||||
def __new__(cls, source: str = "") -> "SxExpr":
|
||||
return str.__new__(cls, source)
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""The raw SX source string (backward compat)."""
|
||||
return str.__str__(self)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SxExpr({self.source!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.source
|
||||
return f"SxExpr({str.__repr__(self)})"
|
||||
|
||||
def __add__(self, other: object) -> "SxExpr":
|
||||
return SxExpr(self.source + str(other))
|
||||
return SxExpr(str.__add__(self, str(other)))
|
||||
|
||||
def __radd__(self, other: object) -> "SxExpr":
|
||||
return SxExpr(str(other) + self.source)
|
||||
return SxExpr(str.__add__(str(other), self))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -283,7 +289,26 @@ def _parse_map(tok: Tokenizer) -> dict[str, Any]:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def serialize(expr: Any, indent: int = 0, pretty: bool = False) -> str:
|
||||
"""Serialize a value back to s-expression text."""
|
||||
"""Serialize a value back to s-expression text.
|
||||
|
||||
Type dispatch order (first match wins):
|
||||
|
||||
- ``SxExpr`` → emitted unquoted (pre-built sx source)
|
||||
- ``list`` → ``(head ...)`` (s-expression list)
|
||||
- ``Symbol`` → bare name
|
||||
- ``Keyword`` → ``:name``
|
||||
- ``str`` → ``"quoted"`` (with escapes)
|
||||
- ``bool`` → ``true`` / ``false``
|
||||
- ``int/float`` → numeric literal
|
||||
- ``None/NIL`` → ``nil``
|
||||
- ``dict`` → ``{:key val ...}``
|
||||
|
||||
List serialization conventions (for ``sx_call`` kwargs):
|
||||
|
||||
- ``(list ...)`` — data array: client gets iterable for map/filter
|
||||
- ``(<> ...)`` — rendered content: client treats as DocumentFragment
|
||||
- ``(head ...)`` — AST: head is called as function (never use for data)
|
||||
"""
|
||||
if isinstance(expr, SxExpr):
|
||||
return expr.source
|
||||
|
||||
|
||||
Reference in New Issue
Block a user