Skip to content

Commit 18a7576

Browse files
Add class component example and add runtime checks to component return value.
1 parent 285ab75 commit 18a7576

File tree

2 files changed

+98
-5
lines changed

2 files changed

+98
-5
lines changed

tdom/transformer.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,16 @@ def interpolate_component(
174174
# @DESIGN: Determine return signature from callable info (cached inspection) ?
175175
callable_info = get_callable_info(component_callable)
176176
kwargs = _prep_component_kwargs(callable_info, resolved_attrs, system=system_dict)
177+
178+
# @DESIGN: We can try to wrap this type resolution up but it is
179+
# hard to tell component authors what should be returned if we have this
180+
# union, and this is actually the simplified one without iterable, bool or
181+
# Node.
182+
183+
# Component() ->
184+
# None | Template | tuple[None | Template, dict[str, object]] |
185+
# Callable[[], None | Template | tuple[None | Template, dict[str, object]]]
186+
177187
res = component_callable(**kwargs)
178188

179189
# @DESIGN: callable or has_attr('__call__') for class components?
@@ -184,17 +194,34 @@ def interpolate_component(
184194

185195
# @DESIGN: Determine return signature via runtime inspection?
186196
if isinstance(res, tuple):
187-
result_template, comp_info = res
197+
if len(res) != 2:
198+
raise ValueError(
199+
f"Tuple form of component return value must be len() 2, not: {len(res)}"
200+
)
201+
else:
202+
result_template, comp_info = res
203+
204+
# @DESIGN: Use open-ended dict for opt-in second return argument?
188205
context_values = comp_info.get("context_values", ()) if comp_info else ()
206+
for entry in context_values:
207+
if not isinstance(entry, tuple):
208+
raise ValueError(
209+
f"Entries for context_values must be 2-tuples but found type: {type(entry)}."
210+
)
211+
elif len(entry) != 2:
212+
raise ValueError(
213+
f"Entries for context_values must be 2-tuples but found len(): {len(entry)}."
214+
)
215+
elif not isinstance(entry[0], ContextVar):
216+
raise ValueError(
217+
f"Invalid context variable in component return value: {type(entry[0])}"
218+
)
189219
else:
190220
result_template = res
191221
comp_info = None
192222
context_values = ()
193223

194-
# @DESIGN: Use open-ended dict for opt-in second return argument?
195-
context_values = comp_info.get("context_values", ()) if comp_info else ()
196-
197-
if result_template:
224+
if isinstance(result_template, Template):
198225
result_struct = render_api.transform_api.transform_template(result_template)
199226
if context_values:
200227
walker = render_api.walk_template_with_context(
@@ -203,6 +230,10 @@ def interpolate_component(
203230
else:
204231
walker = render_api.walk_template(bf, result_template, result_struct)
205232
return (container_tag, iter(walker))
233+
elif result_template is None:
234+
pass
235+
else:
236+
raise ValueError(f"Unknown component return value: {type(result_template)}")
206237

207238

208239
type InterpolateRawTextInfo = tuple[str, Template]

tdom/transformer_test.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from markupsafe import Markup, escape as markupsafe_escape
44
import typing as t
55
import pytest
6+
from dataclasses import dataclass
7+
from collections.abc import Callable
8+
from itertools import chain
69

710
from .transformer import (
811
render_service_factory,
@@ -340,3 +343,62 @@ def test_escape_structured_text():
340343
render_api.render_template(content_t)
341344
== f"<div>{LT}script{GT}console.log({DQ}123!{DQ});{LT}/script{GT}</div>"
342345
)
346+
347+
348+
@dataclass
349+
class Pager:
350+
left_pages: tuple = ()
351+
page: int = 0
352+
right_pages: tuple = ()
353+
prev_page: int | None = None
354+
next_page: int | None = None
355+
356+
357+
@dataclass
358+
class PagerDisplay:
359+
pager: Pager
360+
paginate_url: Callable[[int], str]
361+
root_classes: tuple[str, ...] = ("cb", "tc", "w-100")
362+
part_classes: tuple[str, ...] = ("dib", "pa1")
363+
364+
def __call__(self) -> Template:
365+
parts = [t"<div class={self.root_classes}>"]
366+
if self.pager.prev_page:
367+
parts.append(
368+
t"<a class={self.part_classes} href={self.paginate_url(self.pager.prev_page)}>Prev</a>"
369+
)
370+
for left_page in self.pager.left_pages:
371+
parts.append(
372+
t'<a class={self.part_classes} href="{self.paginate_url(left_page)}">{left_page}</a>'
373+
)
374+
parts.append(t"<span class={self.part_classes}>{self.pager.page}</span>")
375+
for right_page in self.pager.right_pages:
376+
parts.append(
377+
t'<a class={self.part_classes} href="{self.paginate_url(right_page)}">{right_page}</a>'
378+
)
379+
if self.pager.next_page:
380+
parts.append(
381+
t"<a class={self.part_classes} href={self.paginate_url(self.pager.next_page)}>Next</a>"
382+
)
383+
parts.append(t"</div>")
384+
return Template(*chain.from_iterable(parts))
385+
386+
387+
def test_class_component():
388+
def paginate_url(page):
389+
return f"/pages?{page}"
390+
391+
def Footer(pager, paginate_url, footer_classes=("footer",)):
392+
return t"<div class={footer_classes}><{PagerDisplay} pager={pager} paginate_url={paginate_url} /></div>"
393+
394+
pager = Pager(
395+
left_pages=(1, 2), page=3, right_pages=(4, 5), next_page=6, prev_page=None
396+
)
397+
content_t = t"<{Footer} pager={pager} paginate_url={paginate_url} />"
398+
render_api = render_service_factory()
399+
res = render_api.render_template(content_t)
400+
print(res)
401+
assert (
402+
res
403+
== '<div class="footer"><div class="cb tc w-100"><a href="/pages?1" class="dib pa1">1</a><a href="/pages?2" class="dib pa1">2</a><span class="dib pa1">3</span><a href="/pages?4" class="dib pa1">4</a><a href="/pages?5" class="dib pa1">5</a><a href="/pages?6" class="dib pa1">Next</a></div></div>'
404+
)

0 commit comments

Comments
 (0)