Skip to content

Commit eab60ce

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 eab60ce

File tree

4 files changed

+46
-24
lines changed

4 files changed

+46
-24
lines changed

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: 21 additions & 11 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", "_allow_init")
357353

358-
__slots__ = ("_nodes",)
354+
def __init__(self, node: Node) -> None:
355+
if not hasattr(self, "_allow_init"):
356+
raise TypeError("Cannot instantiate Fragment directly. Use fragment[x] instead.")
359357

360-
def __init__(self, *nodes: Node) -> None:
361-
self._nodes = nodes
358+
self._node = node
362359

363360
def __iter__(self) -> Iterator[str]:
364361
return iter_node(self)
@@ -368,14 +365,27 @@ def __str__(self) -> str:
368365

369366
__html__ = __str__
370367

368+
def __getitem__(self, node: Node) -> Fragment:
369+
return _create_fragment(node)
370+
371+
372+
def _create_fragment(node: Node) -> Fragment:
373+
instance = Fragment.__new__(Fragment)
374+
instance._allow_init = True # type: ignore
375+
instance.__init__(node) # type: ignore
376+
return instance
377+
378+
379+
fragment = _create_fragment(None)
380+
371381

372382
def render_node(node: Node) -> _Markup:
373383
return _Markup("".join(iter_node(node)))
374384

375385

376386
def comment(text: str) -> Fragment:
377387
escaped_text = text.replace("--", "")
378-
return Fragment(_Markup(f"<!-- {escaped_text} -->"))
388+
return fragment[_Markup(f"<!-- {escaped_text} -->")]
379389

380390

381391
@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: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import markupsafe
2+
import pytest
23

3-
from htpy import Fragment, i, p
4+
from htpy import Fragment, fragment, i, p
45

56
from .conftest import RenderFixture
67

78

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

1112

1213
def test_render_as_child(render: RenderFixture) -> None:
13-
assert render(p["Say: ", Fragment("Hello ", None, i["World"]), "!"]) == [
14+
assert render(p["Say: ", fragment["Hello ", None, i["World"]], "!"]) == [
1415
"<p>",
1516
"Say: ",
1617
"Hello ",
@@ -22,12 +23,16 @@ def test_render_as_child(render: RenderFixture) -> None:
2223
]
2324

2425

26+
def test_plain_fragment(render: RenderFixture) -> None:
27+
assert render(fragment) == []
28+
29+
2530
def test_render_nested(render: RenderFixture) -> None:
26-
assert render(Fragment(Fragment("Hel", "lo "), "World")) == ["Hel", "lo ", "World"]
31+
assert render(fragment[fragment["Hel", "lo "], "World"]) == ["Hel", "lo ", "World"]
2732

2833

2934
def test_render_chunks(render: RenderFixture) -> None:
30-
assert render(Fragment("Hello ", None, i["World"])) == [
35+
assert render(fragment["Hello ", None, i["World"]]) == [
3136
"Hello ",
3237
"<i>",
3338
"World",
@@ -36,8 +41,15 @@ def test_render_chunks(render: RenderFixture) -> None:
3641

3742

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

4146

4247
def test_iter() -> None:
43-
assert list(Fragment("Hello ", None, i["World"])) == ["Hello ", "<i>", "World", "</i>"]
48+
assert list(fragment["Hello ", None, i["World"]]) == ["Hello ", "<i>", "World", "</i>"]
49+
50+
51+
def test_direct_init_typeerror() -> None:
52+
with pytest.raises(
53+
TypeError, match=r"Cannot instantiate Fragment directly\. Use fragment\[x\] instead"
54+
):
55+
Fragment(None)

0 commit comments

Comments
 (0)