Skip to content

Commit 312ce2d

Browse files
Clarify attribute interpolation pipeline (#75)
* Add test for component spread attribute type coercion. * Do not coerce general spread attributes until processing for html. * Do not coerce True data attributes until processing back to html. * Relax type restriction _within_ the pipeline. * Check that static special attributes are passed through. * Only process special attributes from interpolations and only process them once during the attrs pipeline. * Add bare attribute test. * Import bare attributes into the pipeline with a value of True.
1 parent 7e8f1ff commit 312ce2d

File tree

2 files changed

+68
-13
lines changed

2 files changed

+68
-13
lines changed

tdom/processor.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,12 @@ def _process_aria_attr(value: object) -> t.Iterable[tuple[str, str | None]]:
235235
yield f"aria-{sub_k}", str(sub_v)
236236

237237

238-
def _process_data_attr(value: object) -> t.Iterable[tuple[str, str | None]]:
238+
def _process_data_attr(value: object) -> t.Iterable[tuple[str, object | None]]:
239239
"""Produce data-* attributes based on the interpolated value for "data"."""
240240
d = _force_dict(value, kind="data")
241241
for sub_k, sub_v in d.items():
242242
if sub_v is True:
243-
yield f"data-{sub_k}", None
243+
yield f"data-{sub_k}", True
244244
elif sub_v not in (False, None):
245245
yield f"data-{sub_k}", str(sub_v)
246246

@@ -305,13 +305,18 @@ def _process_attr(
305305
if custom_processor := CUSTOM_ATTR_PROCESSORS.get(key):
306306
yield from custom_processor(value)
307307
return
308+
yield (key, value)
308309

309-
# General handling for all other attributes:
310+
311+
def _process_static_attr(
312+
key: str, value: str | None
313+
) -> t.Iterable[tuple[str, object | None]]:
314+
"""
315+
Bring static attributes, parsed from html but without interpolations, into the pipeline.
316+
"""
310317
match value:
311-
case True:
312-
yield (key, None)
313-
case False | None:
314-
pass
318+
case None:
319+
yield (key, True)
315320
case _:
316321
yield (key, value)
317322

@@ -330,7 +335,8 @@ def _substitute_interpolated_attrs(
330335
if value is not None:
331336
has_placeholders, new_value = _replace_placeholders(value, interpolations)
332337
if has_placeholders:
333-
new_attrs[key] = new_value
338+
for sub_k, sub_v in _process_attr(key, new_value):
339+
new_attrs[sub_k] = sub_v
334340
continue
335341

336342
if (index := _find_placeholder(key)) is not None:
@@ -341,7 +347,8 @@ def _substitute_interpolated_attrs(
341347
new_attrs[sub_k] = sub_v
342348
else:
343349
# Static attribute
344-
new_attrs[key] = value
350+
for sub_k, sub_v in _process_static_attr(key, value):
351+
new_attrs[sub_k] = sub_v
345352
return new_attrs
346353

347354

@@ -354,9 +361,13 @@ def _process_html_attrs(attrs: dict[str, object]) -> dict[str, str | None]:
354361
"""
355362
processed_attrs: dict[str, str | None] = {}
356363
for key, value in attrs.items():
357-
for sub_k, sub_v in _process_attr(key, value):
358-
# Convert to string, preserving None
359-
processed_attrs[sub_k] = str(sub_v) if sub_v is not None else None
364+
match value:
365+
case True:
366+
processed_attrs[key] = None
367+
case False | None:
368+
pass
369+
case _:
370+
processed_attrs[key] = str(value)
360371
return processed_attrs
361372

362373

tdom/processor_test.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ def test_parse_chain_of_void_elements():
5353
assert str(node) == '<br /><hr /><img src="image.png" /><br /><hr />'
5454

5555

56+
def test_static_boolean_attr_retained():
57+
# Make sure a boolean attribute (bare attribute) is not omitted.
58+
node = html(t"<input disabled>")
59+
assert node == Element("input", {"disabled": None})
60+
assert str(node) == "<input disabled />"
61+
62+
5663
def test_parse_element_with_text():
5764
node = html(t"<p>Hello, world!</p>")
5865
assert node == Element(
@@ -693,6 +700,14 @@ def test_style_attribute_non_str_non_dict():
693700
_ = html(t"<p style={styles}>Warning!</p>")
694701

695702

703+
def test_special_attrs_as_static():
704+
node = html(t'<p aria="aria?" data="data?" class="class?" style="style?"></p>')
705+
assert node == Element(
706+
"p",
707+
attrs={"aria": "aria?", "data": "data?", "class": "class?", "style": "style?"},
708+
)
709+
710+
696711
# --------------------------------------------------------------------------
697712
# Function component interpolation tests
698713
# --------------------------------------------------------------------------
@@ -1068,6 +1083,7 @@ def AttributeTypeComponent(
10681083
data_none: None,
10691084
data_float: float,
10701085
data_dt: datetime.datetime,
1086+
**kws: dict[str, object | None],
10711087
) -> Template:
10721088
"""Component to test that we don't incorrectly convert attribute types."""
10731089
assert isinstance(data_int, int)
@@ -1076,6 +1092,24 @@ def AttributeTypeComponent(
10761092
assert data_none is None
10771093
assert isinstance(data_float, float)
10781094
assert isinstance(data_dt, datetime.datetime)
1095+
for kw, v_type in [
1096+
("spread_true", True),
1097+
("spread_false", False),
1098+
("spread_int", int),
1099+
("spread_none", None),
1100+
("spread_float", float),
1101+
("spread_dt", datetime.datetime),
1102+
("spread_dict", dict),
1103+
("spread_list", list),
1104+
]:
1105+
if v_type in (True, False, None):
1106+
assert kw in kws and kws[kw] is v_type, (
1107+
f"{kw} should be {v_type} but got {kws=}"
1108+
)
1109+
else:
1110+
assert kw in kws and isinstance(kws[kw], v_type), (
1111+
f"{kw} should instance of {v_type} but got {kws=}"
1112+
)
10791113
return t"Looks good!"
10801114

10811115

@@ -1086,10 +1120,20 @@ def test_attribute_type_component():
10861120
a_none: None = None
10871121
a_float: float = 3.14
10881122
a_dt: datetime.datetime = datetime.datetime(2024, 1, 1, 12, 0, 0)
1123+
spread_attrs: dict[str, object | None] = {
1124+
"spread_true": True,
1125+
"spread_false": False,
1126+
"spread_none": None,
1127+
"spread_int": 0,
1128+
"spread_float": 0.0,
1129+
"spread_dt": datetime.datetime(2024, 1, 1, 12, 0, 1),
1130+
"spread_dict": dict(),
1131+
"spread_list": ["eggs", "milk"],
1132+
}
10891133
node = html(
10901134
t"<{AttributeTypeComponent} data-int={an_int} data-true={a_true} "
10911135
t"data-false={a_false} data-none={a_none} data-float={a_float} "
1092-
t"data-dt={a_dt} />"
1136+
t"data-dt={a_dt} {spread_attrs}/>"
10931137
)
10941138
assert node == Text("Looks good!")
10951139
assert str(node) == "Looks good!"

0 commit comments

Comments
 (0)