Skip to content

Commit 509e5f6

Browse files
committed
Construct fragment via fragment[].
This change makes the fragment consistent with elements by using __getitem__ to specify child nodes. See #93 for discussion.
1 parent f7486d4 commit 509e5f6

File tree

5 files changed

+35
-27
lines changed

5 files changed

+35
-27
lines changed

docs/changelog.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Changelog
22

33
## Next (minor)
4-
- Add a `Fragment` node for explicitly grouping a collection of nodes. Fixes
5-
[#82](https://github.com/pelme/htpy/issues/82)
4+
- Add `fragment` for explicitly grouping a collection of nodes. Fixes
5+
[issue #82](https://github.com/pelme/htpy/issues/82).
6+
See [PR #86](https://github.com/pelme/htpy/pull/86) and [PR #95](https://github.com/pelme/htpy/pull/95). Thanks to [Thomas Scholtes (@geigerzaehler)](https://github.com/geigerzaehler).
67

78
## 25.2.0 - 2025-02-01
89
- Context providers longer require wrapping nodes in a function/lambda. This

docs/usage.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ Fragments allow you to wrap a group of nodes (not necessarily elements) so that
9898
they can be rendered without a wrapping element.
9999

100100
```pycon
101-
>>> from htpy import p, i, Fragment
102-
>>> content = Fragment("Hello ", None, i["world!"])
101+
>>> from htpy import p, i, fragment
102+
>>> content = fragment["Hello ", None, i["world!"]]
103103
>>> print(content)
104104
Hello <i>world!</i>
105105

@@ -404,8 +404,8 @@ Just like [render_node()](#render-elements-without-a-parent-orphans), there is
404404
without a parent:
405405

406406
```pycon
407-
>>> from htpy import li, iter_node
408-
>>> for chunk in iter_node([li["a"], li["b"]]):
407+
>>> from htpy import li, Fragment
408+
>>> for chunk in fragment[li["a"], li["b"]]:
409409
... print(f"got a chunk: {chunk!r}")
410410
...
411411
got a chunk: '<li>'

htpy/__init__.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,7 @@ def _iter_node_context(x: Node, context_dict: dict[Context[t.Any], t.Any]) -> It
197197
)
198198
yield from _iter_node_context(x.func(context_value), context_dict)
199199
elif isinstance(x, Fragment):
200-
for node in x._nodes: # pyright: ignore [reportPrivateUsage]
201-
yield from _iter_node_context(node, context_dict)
200+
yield from _iter_node_context(x._node, context_dict) # pyright: ignore
202201
elif isinstance(x, str | _HasHtml):
203202
yield str(_escape(x))
204203
elif isinstance(x, int):
@@ -348,17 +347,15 @@ def __repr__(self) -> str:
348347

349348

350349
class Fragment:
351-
"""A collection of nodes without a wrapping element.
350+
"""A collection of nodes without a wrapping element."""
352351

353-
>>> content = Fragment("Hello ", None, i["world!"])
354-
>>> print(content)
355-
Hello <i>world!</i>
356-
"""
352+
__slots__ = ("_node",)
357353

358-
__slots__ = ("_nodes",)
359-
360-
def __init__(self, *nodes: Node) -> None:
361-
self._nodes = nodes
354+
def __init__(self) -> None:
355+
# Make it awkward to instantiate a Fragment directly:
356+
# Encourage using fragment[x]. That is why it is not possible to set the
357+
# node directly via the constructor.
358+
self._node: Node = None
362359

363360
def __iter__(self) -> Iterator[str]:
364361
return iter_node(self)
@@ -369,13 +366,23 @@ def __str__(self) -> str:
369366
__html__ = __str__
370367

371368

369+
class _FragmentGetter:
370+
def __getitem__(self, node: Node) -> Fragment:
371+
result = Fragment()
372+
result._node = node # pyright: ignore[reportPrivateUsage]
373+
return result
374+
375+
376+
fragment = _FragmentGetter()
377+
378+
372379
def render_node(node: Node) -> _Markup:
373380
return _Markup("".join(iter_node(node)))
374381

375382

376383
def comment(text: str) -> Fragment:
377384
escaped_text = text.replace("--", "")
378-
return Fragment(_Markup(f"<!-- {escaped_text} -->"))
385+
return fragment[_Markup(f"<!-- {escaped_text} -->")]
379386

380387

381388
@t.runtime_checkable

tests/test_context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import markupsafe
66
import pytest
77

8-
from htpy import Context, Fragment, Node, div
8+
from htpy import Context, Node, div, fragment
99

1010
if t.TYPE_CHECKING:
1111
from .conftest import RenderFixture
@@ -116,6 +116,6 @@ def test_context_passed_via_fragment(render: RenderFixture) -> None:
116116
def echo(value: str) -> str:
117117
return value
118118

119-
result = div[ctx.provider("foo", Fragment(echo()))]
119+
result = div[ctx.provider("foo", fragment[echo()])]
120120

121121
assert render(result) == ["<div>", "foo", "</div>"]

tests/test_fragment.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import markupsafe
22

3-
from htpy import Fragment, i, p
3+
from htpy import fragment, i, p
44

55
from .conftest import RenderFixture
66

77

88
def test_render_direct() -> None:
9-
assert str(Fragment("Hello ", None, i["World"])) == "Hello <i>World</i>"
9+
assert str(fragment["Hello ", None, i["World"]]) == "Hello <i>World</i>"
1010

1111

1212
def test_render_as_child(render: RenderFixture) -> None:
13-
assert render(p["Say: ", Fragment("Hello ", None, i["World"]), "!"]) == [
13+
assert render(p["Say: ", fragment["Hello ", None, i["World"]], "!"]) == [
1414
"<p>",
1515
"Say: ",
1616
"Hello ",
@@ -23,11 +23,11 @@ def test_render_as_child(render: RenderFixture) -> None:
2323

2424

2525
def test_render_nested(render: RenderFixture) -> None:
26-
assert render(Fragment(Fragment("Hel", "lo "), "World")) == ["Hel", "lo ", "World"]
26+
assert render(fragment[fragment["Hel", "lo "], "World"]) == ["Hel", "lo ", "World"]
2727

2828

2929
def test_render_chunks(render: RenderFixture) -> None:
30-
assert render(Fragment("Hello ", None, i["World"])) == [
30+
assert render(fragment["Hello ", None, i["World"]]) == [
3131
"Hello ",
3232
"<i>",
3333
"World",
@@ -36,8 +36,8 @@ def test_render_chunks(render: RenderFixture) -> None:
3636

3737

3838
def test_safe() -> None:
39-
assert markupsafe.escape(Fragment(i["hi"])) == "<i>hi</i>"
39+
assert markupsafe.escape(fragment[i["hi"]]) == "<i>hi</i>"
4040

4141

4242
def test_iter() -> None:
43-
assert list(Fragment("Hello ", None, i["World"])) == ["Hello ", "<i>", "World", "</i>"]
43+
assert list(fragment["Hello ", None, i["World"]]) == ["Hello ", "<i>", "World", "</i>"]

0 commit comments

Comments
 (0)