Skip to content

Commit a7d1d3b

Browse files
committed
add tests
1 parent ab29720 commit a7d1d3b

File tree

6 files changed

+136
-38
lines changed

6 files changed

+136
-38
lines changed

src/idom/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from .core.layout import Layout
1919
from .core.serve import Stop
2020
from .core.vdom import vdom
21-
from .utils import Ref, html_to_vdom
21+
from .utils import Ref, html_to_vdom, vdom_to_html
2222
from .widgets import hotswap
2323

2424

@@ -53,6 +53,7 @@
5353
"use_ref",
5454
"use_scope",
5555
"use_state",
56+
"vdom_to_html",
5657
"vdom",
5758
"web",
5859
]

src/idom/utils.py

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def __repr__(self) -> str:
6262
return f"{type(self).__name__}({current})"
6363

6464

65-
def vdom_to_html(vdom: str | VdomDict) -> str:
65+
def vdom_to_html(value: str | VdomDict, indent: int = 0, depth: int = 0) -> str:
6666
"""Convert a VDOM dictionary into an HTML string
6767
6868
Only the following keys are translated to HTML:
@@ -71,38 +71,68 @@ def vdom_to_html(vdom: str | VdomDict) -> str:
7171
- ``attributes``
7272
- ``children`` (must be strings or more VDOM dicts)
7373
"""
74-
if isinstance(vdom, str):
75-
return vdom
74+
if indent:
75+
close_indent = f"\n{' ' * (indent * depth)}"
76+
open_indent = close_indent if depth else close_indent[1:]
77+
else:
78+
open_indent = close_indent = ""
79+
80+
if isinstance(value, str):
81+
return f"{open_indent}{value}" if depth else value
7682

7783
try:
78-
tag = vdom["tagName"]
79-
except TypeError as error:
80-
raise TypeError(f"Expected a VDOM dictionary or string, not {vdom}") from error
81-
82-
if "attributes" in vdom:
83-
vdom_attributes = dict(vdom["attributes"])
84-
vdom_attributes["style"] = _vdom_to_html_style(vdom_attributes["style"])
85-
attributes = " " + " ".join(
86-
f'{k}="{html_escape(v)}"' for k, v in vdom_attributes.items()
87-
)
84+
tag = value["tagName"]
85+
except TypeError as error: # pragma: no cover
86+
raise TypeError(f"Expected a VDOM dictionary or string, not {value}") from error
87+
88+
if "attributes" in value:
89+
if not tag: # pragma: no cover
90+
warn(f"Ignored attributes from element frament", UserWarning)
91+
else:
92+
vdom_attributes = dict(value["attributes"])
93+
if "style" in vdom_attributes:
94+
vdom_attributes["style"] = _vdom_to_html_style(vdom_attributes["style"])
95+
for k, v in list(vdom_attributes.items()):
96+
if not isinstance(v, (str, int)):
97+
del vdom_attributes[k]
98+
warn(
99+
f"Could not convert attribute of type {type(v).__name__} to HTML attribute - {v}",
100+
UserWarning,
101+
)
102+
attributes = (
103+
f""" {' '.join(f'{k}="{html_escape(v)}"' for k, v in vdom_attributes.items())}"""
104+
if vdom_attributes
105+
else ""
106+
)
88107
else:
89108
attributes = ""
90109

91-
if "children" in vdom:
92-
vdom_children: list[str] = []
93-
for child in vdom["children"]:
94-
if isinstance(child, (str, dict)):
95-
vdom_children.append(vdom_to_html(cast("str | VdomDict", child)))
110+
if "children" in value:
111+
children_list: list[str] = []
112+
113+
child: VdomDict | str
114+
for child in value["children"]:
115+
if isinstance(child, (dict, str)):
116+
children_list.append(vdom_to_html(child, indent, depth + 1))
96117
else:
97118
warn(
98-
f"Could not convert element of type {type(child).__name__!r} to HTML",
119+
f"Could not convert element of type {type(child).__name__!r} to HTML - {child}",
99120
UserWarning,
100121
)
101-
children = "".join(vdom_children)
122+
123+
children = "".join(children_list)
124+
102125
else:
103126
children = ""
104127

105-
return f"<{tag}{attributes}>{children}</{tag}>"
128+
if not children:
129+
return f"{open_indent}<{tag}{attributes} />" if tag else ""
130+
else:
131+
return (
132+
f"{open_indent}<{tag}{attributes}>{children}{close_indent}</{tag}>"
133+
if tag
134+
else children
135+
)
106136

107137

108138
_CAMEL_CASE_SUB_PATTERN = re.compile(r"(?<!^)(?=[A-Z])")
@@ -113,7 +143,7 @@ def _vdom_to_html_style(style: str | dict[str, Any]) -> str:
113143
return style
114144

115145
return ";".join(
116-
f"{_CAMEL_CASE_SUB_PATTERN.sub('-', k)}:{v}" for k, v in style.items()
146+
f"{_CAMEL_CASE_SUB_PATTERN.sub('-', k).lower()}:{v}" for k, v in style.items()
117147
)
118148

119149

tests/test_backend/test__common.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import pytest
2+
3+
from idom.backend._common import traversal_safe_path
4+
5+
6+
@pytest.mark.parametrize(
7+
"bad_path",
8+
[
9+
"../escaped",
10+
"ok/../../escaped",
11+
"ok/ok-again/../../ok-yet-again/../../../escaped",
12+
],
13+
)
14+
def test_catch_unsafe_relative_path_traversal(tmp_path, bad_path):
15+
with pytest.raises(ValueError, match="Unsafe path"):
16+
traversal_safe_path(tmp_path, *bad_path.split("/"))
File renamed without changes.

tests/test_backend/test_utils.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from idom.backend import flask as flask_implementation
99
from idom.backend.utils import find_available_port
1010
from idom.backend.utils import run as sync_run
11-
from idom.backend.utils import traversal_safe_path
1211
from idom.sample import SampleApp as SampleApp
1312

1413

@@ -45,16 +44,3 @@ async def test_run(page: Page, exit_stack: ExitStack):
4544

4645
await page.goto(url)
4746
await page.wait_for_selector("#sample")
48-
49-
50-
@pytest.mark.parametrize(
51-
"bad_path",
52-
[
53-
"../escaped",
54-
"ok/../../escaped",
55-
"ok/ok-again/../../ok-yet-again/../../../escaped",
56-
],
57-
)
58-
def test_catch_unsafe_relative_path_traversal(tmp_path, bad_path):
59-
with pytest.raises(ValueError, match="Unsafe path"):
60-
traversal_safe_path(tmp_path, *bad_path.split("/"))

tests/test_utils.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import pytest
22

33
import idom
4-
from idom.utils import HTMLParseError, html_to_vdom
4+
from idom import html
5+
from idom.utils import HTMLParseError, html_to_vdom, vdom_to_html
56

67

78
def test_basic_ref_behavior():
@@ -149,3 +150,67 @@ def test_html_to_vdom_with_no_parent_node():
149150
}
150151

151152
assert html_to_vdom(source) == expected
153+
154+
155+
@pytest.mark.parametrize(
156+
"vdom_in, html_out",
157+
[
158+
(
159+
html.div("hello"),
160+
"<div>hello</div>",
161+
),
162+
(
163+
html.div(object()), # ignore non-str/vdom children
164+
"<div />",
165+
),
166+
(
167+
html.div({"someAttribute": object()}), # ignore non-str/vdom attrs
168+
"<div />",
169+
),
170+
(
171+
html.div("hello", html.a({"href": "https://example.com"}, "example")),
172+
'<div>hello<a href="https://example.com">example</a></div>',
173+
),
174+
(
175+
html.button({"onClick": lambda event: None}),
176+
"<button />",
177+
),
178+
(
179+
html.div({"style": {"backgroundColor": "blue", "marginLeft": "10px"}}),
180+
'<div style="background-color:blue;margin-left:10px" />',
181+
),
182+
(
183+
html.div({"style": "background-color:blue;margin-left:10px"}),
184+
'<div style="background-color:blue;margin-left:10px" />',
185+
),
186+
(
187+
html._(
188+
html.div("hello"),
189+
html.a({"href": "https://example.com"}, "example"),
190+
),
191+
'<div>hello</div><a href="https://example.com">example</a>',
192+
),
193+
(
194+
html.div(
195+
html._(
196+
html.div("hello"),
197+
html.a({"href": "https://example.com"}, "example"),
198+
),
199+
html.button(),
200+
),
201+
'<div><div>hello</div><a href="https://example.com">example</a><button /></div>',
202+
),
203+
],
204+
)
205+
def test_vdom_to_html(vdom_in, html_out):
206+
assert vdom_to_html(vdom_in) == html_out
207+
208+
209+
def test_vdom_to_html_with_indent():
210+
assert (
211+
vdom_to_html(
212+
html.div("hello", html.a({"href": "https://example.com"}, "example")),
213+
indent=2,
214+
)
215+
== '<div>\n hello\n <a href="https://example.com">\n example\n </a>\n</div>'
216+
)

0 commit comments

Comments
 (0)