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:
2026-03-04 21:47:00 +00:00
parent ad75798ab7
commit 278ae3e8f6
45 changed files with 378 additions and 379 deletions

View File

@@ -73,10 +73,10 @@ def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
if tp is not None:
qty = pending_tickets.get(entry.id, 0)
widget_html = sx_call("events-entry-widget-wrapper",
widget=SxExpr(_ticket_widget_html(entry, qty, ticket_url, ctx={})))
widget=_ticket_widget_html(entry, qty, ticket_url, ctx={}))
return sx_call("events-entry-card",
title=SxExpr(title_html), badges=SxExpr(badges_html),
title=title_html, badges=SxExpr(badges_html),
time_parts=SxExpr(time_parts), cost=SxExpr(cost_html),
widget=SxExpr(widget_html))
@@ -137,10 +137,10 @@ def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict,
if tp is not None:
qty = pending_tickets.get(entry.id, 0)
widget_html = sx_call("events-entry-tile-widget-wrapper",
widget=SxExpr(_ticket_widget_html(entry, qty, ticket_url, ctx={})))
widget=_ticket_widget_html(entry, qty, ticket_url, ctx={}))
return sx_call("events-entry-card-tile",
title=SxExpr(title_html), badges=SxExpr(badges_html),
title=title_html, badges=SxExpr(badges_html),
time=SxExpr(time_html), cost=SxExpr(cost_html),
widget=SxExpr(widget_html))
@@ -199,7 +199,7 @@ def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_
cls="px-3 py-12 text-center text-stone-400")
return sx_call("events-main-panel-body",
toggle=SxExpr(toggle), body=SxExpr(body))
toggle=toggle, body=body)
# ---------------------------------------------------------------------------
@@ -253,7 +253,7 @@ def _entry_main_panel_html(ctx: dict) -> str:
# State
state_html = _field("State", sx_call("events-entry-state-field",
entry_id=str(eid),
badge=SxExpr(_entry_state_badge_html(state))))
badge=_entry_state_badge_html(state)))
# Cost
cost = getattr(entry, "cost", None)
@@ -284,7 +284,7 @@ def _entry_main_panel_html(ctx: dict) -> str:
entry_posts = ctx.get("entry_posts") or []
posts_html = _field("Associated Posts", sx_call("events-entry-posts-field",
entry_id=str(eid),
posts_panel=SxExpr(render_entry_posts_panel(entry_posts, entry, calendar, day, month, year))))
posts_panel=render_entry_posts_panel(entry_posts, entry, calendar, day, month, year)))
# Options and Edit Button
edit_url = url_for(
@@ -295,12 +295,12 @@ def _entry_main_panel_html(ctx: dict) -> str:
return sx_call("events-entry-panel",
entry_id=str(eid), list_container=list_container,
name=SxExpr(name_html), slot=SxExpr(slot_html),
time=SxExpr(time_html), state=SxExpr(state_html),
cost=SxExpr(cost_html), tickets=SxExpr(tickets_html),
buy=SxExpr(buy_html), date=SxExpr(date_html),
posts=SxExpr(posts_html),
options=SxExpr(_entry_options_html(entry, calendar, day, month, year)),
name=name_html, slot=slot_html,
time=time_html, state=state_html,
cost=cost_html, tickets=tickets_html,
buy=SxExpr(buy_html), date=date_html,
posts=posts_html,
options=_entry_options_html(entry, calendar, day, month, year),
pre_action=pre_action, edit_url=edit_url)
@@ -331,13 +331,13 @@ def _entry_header_html(ctx: dict, *, oob: bool = False) -> str:
)
label_html = sx_call("events-entry-label",
entry_id=str(entry.id),
title=SxExpr(_entry_title_html(entry)),
title=_entry_title_html(entry),
times=SxExpr(_entry_times_html(entry)))
nav_html = _entry_nav_html(ctx)
return sx_call("menu-row-sx", id="entry-row", level=5,
link_href=link_href, link_label_content=SxExpr(label_html),
link_href=link_href, link_label_content=label_html,
nav=SxExpr(nav_html) if nav_html else None, child_id="entry-header-child", oob=oob)
@@ -391,7 +391,7 @@ def _entry_nav_html(ctx: dict) -> str:
else:
img_html = sx_call("events-post-img-placeholder")
post_links += sx_call("events-entry-nav-post-link",
href=href, img=SxExpr(img_html), title=title)
href=href, img=img_html, title=title)
parts.append((sx_call("events-entry-posts-nav-oob",
items=SxExpr(post_links))).replace(' :hx-swap-oob "true"', ''))
@@ -420,7 +420,7 @@ def render_entry_optioned(entry, calendar, day, month, year) -> str:
return options + sx_call("events-entry-optioned-oob",
entry_id=str(entry.id),
title=SxExpr(title), state=SxExpr(state))
title=title, state=state)
def _entry_title_html(entry) -> str:
@@ -428,7 +428,7 @@ def _entry_title_html(entry) -> str:
state = getattr(entry, "state", "pending") or "pending"
return sx_call("events-entry-title",
name=entry.name,
badge=SxExpr(_entry_state_badge_html(state)))
badge=_entry_state_badge_html(state))
def _entry_options_html(entry, calendar, day, month, year) -> str:
@@ -550,7 +550,7 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) ->
entry_id=eid, post_id=ep_id,
)
items += sx_call("events-entry-post-item",
img=SxExpr(img_html), title=ep_title,
img=img_html, title=ep_title,
del_url=del_url, entry_id=eid_s,
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
posts_html = sx_call("events-entry-posts-list", items=SxExpr(items))
@@ -563,7 +563,7 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) ->
)
return sx_call("events-entry-posts-panel",
posts=SxExpr(posts_html), search_url=search_url,
posts=posts_html, search_url=search_url,
entry_id=eid_s)
@@ -591,7 +591,7 @@ def render_entry_posts_nav_oob(entry_posts) -> str:
if feat else sx_call("events-post-img-placeholder"))
items += sx_call("events-entry-nav-post",
href=href, nav_btn=nav_btn,
img=SxExpr(img_html), title=title)
img=img_html, title=title)
return sx_call("events-entry-posts-nav-oob", items=SxExpr(items))
@@ -743,7 +743,7 @@ def _entry_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
return sx_call("menu-row-sx", id="entry-admin-row", level=6,
link_href=link_href, link_label="admin", icon="fa fa-cog",
nav=SxExpr(nav_html) if nav_html else None, child_id="entry-admin-header-child", oob=oob)
nav=nav_html or None, child_id="entry-admin-header-child", oob=oob)
def _entry_admin_nav_html(ctx: dict) -> str:
@@ -822,7 +822,7 @@ def render_post_search_results(search_posts, search_query, page, total_pages,
parts.append(sx_call("events-post-search-item",
post_url=post_url, entry_id=str(eid), csrf=csrf,
post_id=str(sp.id), img=SxExpr(img_html), title=title))
post_id=str(sp.id), img=img_html, title=title))
result = "".join(parts)
@@ -882,7 +882,7 @@ def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str:
html = sx_call("events-entry-edit-form",
entry_id=str(eid), list_container=list_container,
put_url=put_url, cancel_url=cancel_url, csrf=csrf,
name_val=entry.name or "", slot_picker=SxExpr(slot_picker_html),
name_val=entry.name or "", slot_picker=slot_picker_html,
start_val=start_val, end_val=end_val, cost_display=cost_display,
ticket_price_val=tp_val, ticket_count_val=tc_val,
action_btn=action_btn, cancel_btn=cancel_btn)
@@ -920,7 +920,7 @@ def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
html = sx_call("events-entry-add-form",
post_url=post_url, csrf=csrf,
slot_picker=SxExpr(slot_picker_html),
slot_picker=slot_picker_html,
action_btn=action_btn, cancel_btn=cancel_btn,
cancel_url=cancel_url)
return html + _SLOT_PICKER_JS
@@ -998,13 +998,13 @@ def render_fragment_account_tickets(tickets) -> str:
items_html += sx_call("events-frag-ticket-item",
href=href, entry_name=ticket.entry_name,
date_str=date_str, calendar_name=cal_name,
type_name=type_name, badge=SxExpr(badge_html))
type_name=type_name, badge=badge_html)
body = sx_call("events-frag-tickets-list", items=SxExpr(items_html))
else:
body = sx_call("empty-state", message="No tickets yet.",
cls="text-sm text-stone-500")
return sx_call("events-frag-tickets-panel", items=SxExpr(body))
return sx_call("events-frag-tickets-panel", items=body)
# ---------------------------------------------------------------------------
@@ -1033,10 +1033,10 @@ def render_fragment_account_bookings(bookings) -> str:
name=booking.name,
date_str=date_str + date_str_extra,
calendar_name=cal_name, cost_str=cost_str,
badge=SxExpr(badge_html))
badge=badge_html)
body = sx_call("events-frag-bookings-list", items=SxExpr(items_html))
else:
body = sx_call("empty-state", message="No bookings yet.",
cls="text-sm text-stone-500")
return sx_call("events-frag-bookings-panel", items=SxExpr(body))
return sx_call("events-frag-bookings-panel", items=body)