html-tstring 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,3 +8,6 @@ wheels/
8
8
 
9
9
  # Virtual environments
10
10
  .venv
11
+
12
+ # Coverage
13
+ .coverage
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: html-tstring
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A 🤘 rockin' t-string HTML templating system for Python 3.14.
5
5
  Project-URL: Homepage, https://github.com/t-strings/html-tstring
6
6
  Project-URL: Changelog, https://github.com/t-strings/html-tstring/releases
@@ -110,7 +110,6 @@ Boolean attributes are supported too. Just use a boolean value in the attribute
110
110
 
111
111
  ```python
112
112
  form_button = html(t"<button disabled={True} hidden={False}>Submit</button>")
113
- print(form_button)
114
113
  # <button disabled>Submit</button>
115
114
  ```
116
115
 
@@ -310,13 +309,15 @@ Because attributes are passed as keyword arguments, you can explicitly provide t
310
309
  ```python
311
310
  from typing import Any
312
311
 
313
- def Link(*, href: str, text: str, **props: Any) -> Template:
314
- return t'<a href="{href}" {props}>{text}</a>'
312
+ def Link(*, href: str, text: str, data_value: int, **attrs: Any) -> Template:
313
+ return t'<a href="{href}" {attrs}>{text}: {data_value}</a>'
315
314
 
316
- result = html(t'<{Link} href="https://example.com" text="Example" target="_blank" />')
317
- # <a href="https://example.com" target="_blank">Example</a>
315
+ result = html(t'<{Link} href="https://example.com" text="Example" data-value={42} target="_blank" />')
316
+ # <a href="https://example.com" target="_blank">Example: 42</a>
318
317
  ```
319
318
 
319
+ Note that attributes with hyphens (like `data-value`) are converted to underscores (`data_value`) in the function signature.
320
+
320
321
  In addition to returning a `Template` directly, component functions may also return any `Node` type found in [`html_tstring.nodes`](https://github.com/t-strings/html-tstring/blob/main/html_tstring/nodes.py). This allows you to build more complex components that manipulate the HTML structure programmatically.
321
322
 
322
323
  #### SVG Support
@@ -85,7 +85,6 @@ Boolean attributes are supported too. Just use a boolean value in the attribute
85
85
 
86
86
  ```python
87
87
  form_button = html(t"<button disabled={True} hidden={False}>Submit</button>")
88
- print(form_button)
89
88
  # <button disabled>Submit</button>
90
89
  ```
91
90
 
@@ -285,13 +284,15 @@ Because attributes are passed as keyword arguments, you can explicitly provide t
285
284
  ```python
286
285
  from typing import Any
287
286
 
288
- def Link(*, href: str, text: str, **props: Any) -> Template:
289
- return t'<a href="{href}" {props}>{text}</a>'
287
+ def Link(*, href: str, text: str, data_value: int, **attrs: Any) -> Template:
288
+ return t'<a href="{href}" {attrs}>{text}: {data_value}</a>'
290
289
 
291
- result = html(t'<{Link} href="https://example.com" text="Example" target="_blank" />')
292
- # <a href="https://example.com" target="_blank">Example</a>
290
+ result = html(t'<{Link} href="https://example.com" text="Example" data-value={42} target="_blank" />')
291
+ # <a href="https://example.com" target="_blank">Example: 42</a>
293
292
  ```
294
293
 
294
+ Note that attributes with hyphens (like `data-value`) are converted to underscores (`data_value`) in the function signature.
295
+
295
296
  In addition to returning a `Template` directly, component functions may also return any `Node` type found in [`html_tstring.nodes`](https://github.com/t-strings/html-tstring/blob/main/html_tstring/nodes.py). This allows you to build more complex components that manipulate the HTML structure programmatically.
296
297
 
297
298
  #### SVG Support
@@ -95,6 +95,7 @@ class NodeParser(HTMLParser):
95
95
 
96
96
  def get_node(self) -> Node:
97
97
  """Get the Node tree parsed from the input HTML."""
98
+ # CONSIDER: Should we invert things and offer streaming parsing?
98
99
  assert not self.stack, "Did you forget to call close()?"
99
100
  if len(self.root.children) > 1:
100
101
  # The parse structure results in multiple root elements, so we
@@ -83,18 +83,16 @@ def _instrument(
83
83
  yield s
84
84
  # There are always count-1 placeholders between count strings.
85
85
  if i < count - 1:
86
+ placeholder = _placeholder(i)
87
+
86
88
  # Special case for component callables: if the interpolation
87
89
  # is a callable, we need to make sure that any matching closing
88
90
  # tag uses the same placeholder.
89
91
  callable_id = callable_ids[i]
90
- if callable_id is not None:
91
- # This interpolation is a callable, so we need to make sure
92
- # that any matching closing tag uses the same placeholder.
93
- if callable_id not in callable_placeholders:
94
- callable_placeholders[callable_id] = _placeholder(i)
95
- yield callable_placeholders[callable_id]
96
- else:
97
- yield _placeholder(i)
92
+ if callable_id:
93
+ placeholder = callable_placeholders.setdefault(callable_id, placeholder)
94
+
95
+ yield placeholder
98
96
 
99
97
 
100
98
  @lru_cache()
@@ -187,7 +185,9 @@ def _substitute_style_attr(value: object) -> t.Iterable[tuple[str, str | None]]:
187
185
  yield ("style", str(value))
188
186
 
189
187
 
190
- def _substitute_spread_attrs(value: object) -> t.Iterable[tuple[str, str | None]]:
188
+ def _substitute_spread_attrs(
189
+ value: object,
190
+ ) -> t.Iterable[tuple[str, object | None]]:
191
191
  """
192
192
  Substitute a spread attribute based on the interpolated value.
193
193
 
@@ -214,7 +214,7 @@ CUSTOM_ATTR_HANDLERS = {
214
214
  def _substitute_attr(
215
215
  key: str,
216
216
  value: object,
217
- ) -> t.Iterable[tuple[str, str | None]]:
217
+ ) -> t.Iterable[tuple[str, object | None]]:
218
218
  """
219
219
  Substitute a single attribute based on its key and the interpolated value.
220
220
 
@@ -230,21 +230,19 @@ def _substitute_attr(
230
230
 
231
231
  # General handling for all other attributes:
232
232
  match value:
233
- case str():
234
- yield (key, value)
235
233
  case True:
236
234
  yield (key, None)
237
235
  case False | None:
238
236
  pass
239
237
  case _:
240
- yield (key, str(value))
238
+ yield (key, value)
241
239
 
242
240
 
243
241
  def _substitute_attrs(
244
242
  attrs: dict[str, str | None], interpolations: tuple[Interpolation, ...]
245
- ) -> dict[str, str | None]:
243
+ ) -> dict[str, object | None]:
246
244
  """Substitute placeholders in attributes based on the corresponding interpolations."""
247
- new_attrs: dict[str, str | None] = {}
245
+ new_attrs: dict[str, object | None] = {}
248
246
  for key, value in attrs.items():
249
247
  if value and value.startswith(_PLACEHOLDER_PREFIX):
250
248
  index = _placholder_index(value)
@@ -299,14 +297,20 @@ def _node_from_value(value: object) -> Node:
299
297
  children = [_node_from_value(v) for v in value]
300
298
  return Fragment(children=children)
301
299
  case HasHTMLDunder():
300
+ # CONSIDER: could we return a lazy Text?
302
301
  return Text(Markup(value.__html__()))
303
302
  case _:
303
+ # CONSIDER: could we return a lazy Text?
304
304
  return Text(str(value))
305
305
 
306
306
 
307
+ type ComponentReturn = Node | Template | str | HasHTMLDunder
308
+ type ComponentCallable = t.Callable[..., ComponentReturn | t.Iterable[ComponentReturn]]
309
+
310
+
307
311
  def _invoke_component(
308
312
  tag: str,
309
- new_attrs: dict[str, str | None],
313
+ new_attrs: dict[str, object | None],
310
314
  new_children: list[Node],
311
315
  interpolations: tuple[Interpolation, ...],
312
316
  ) -> Node:
@@ -314,12 +318,16 @@ def _invoke_component(
314
318
  index = _placholder_index(tag)
315
319
  interpolation = interpolations[index]
316
320
  value = format_interpolation(interpolation)
321
+ # TODO: consider use of signature() or other approaches to validation.
317
322
  if not callable(value):
318
323
  raise TypeError(
319
324
  f"Expected a callable for component invocation, got {type(value).__name__}"
320
325
  )
326
+ # Replace attr names hyphens with underscores for Python kwargs
327
+ kwargs = {k.replace("-", "_"): v for k, v in new_attrs.items()}
328
+
321
329
  # Call the component and return the resulting node
322
- result = value(*new_children, **new_attrs)
330
+ result = value(*new_children, **kwargs)
323
331
  match result:
324
332
  case Node():
325
333
  return result
@@ -336,6 +344,11 @@ def _invoke_component(
336
344
  )
337
345
 
338
346
 
347
+ def _stringify_attrs(attrs: dict[str, object | None]) -> dict[str, str | None]:
348
+ """Convert all attribute values to strings, preserving None values."""
349
+ return {k: str(v) if v is not None else None for k, v in attrs.items()}
350
+
351
+
339
352
  def _substitute_node(p_node: Node, interpolations: tuple[Interpolation, ...]) -> Node:
340
353
  """Substitute placeholders in a node based on the corresponding interpolations."""
341
354
  match p_node:
@@ -350,6 +363,7 @@ def _substitute_node(p_node: Node, interpolations: tuple[Interpolation, ...]) ->
350
363
  if tag.startswith(_PLACEHOLDER_PREFIX):
351
364
  return _invoke_component(tag, new_attrs, new_children, interpolations)
352
365
  else:
366
+ new_attrs = _stringify_attrs(new_attrs)
353
367
  return Element(tag=tag, attrs=new_attrs, children=new_children)
354
368
  case Fragment(children=children):
355
369
  new_children = _substitute_and_flatten_children(children, interpolations)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "html-tstring"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "A 🤘 rockin' t-string HTML templating system for Python 3.14."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -34,6 +34,7 @@ CI = "https://github.com/t-strings/html-tstring/actions"
34
34
  dev = [
35
35
  "pyright>=1.1.404",
36
36
  "pytest>=8.4.1",
37
+ "pytest-cov>=6.3.0",
37
38
  "pytest-watcher>=0.4.3",
38
39
  "ruff>=0.12.11",
39
40
  ]
File without changes