Skip to content

Commit db7676f

Browse files
committed
string_to_reactpy and reactpy_to_string
1 parent ee51f67 commit db7676f

File tree

8 files changed

+57
-48
lines changed

8 files changed

+57
-48
lines changed

src/reactpy/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from reactpy.core.layout import Layout
2222
from reactpy.core.vdom import vdom
2323
from reactpy.pyscript.components import pyscript_component
24-
from reactpy.utils import Ref, html_to_vdom, vdom_to_html
24+
from reactpy.utils import Ref, reactpy_to_string, string_to_reactpy
2525

2626
__author__ = "The Reactive Python Team"
2727
__version__ = "2.0.0a1"
@@ -35,9 +35,10 @@
3535
"event",
3636
"hooks",
3737
"html",
38-
"html_to_vdom",
3938
"logging",
4039
"pyscript_component",
40+
"reactpy_to_string",
41+
"string_to_reactpy",
4142
"types",
4243
"use_async_effect",
4344
"use_callback",
@@ -52,7 +53,6 @@
5253
"use_scope",
5354
"use_state",
5455
"vdom",
55-
"vdom_to_html",
5656
"web",
5757
"widgets",
5858
]

src/reactpy/executors/asgi/standalone.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
RootComponentConstructor,
3232
VdomDict,
3333
)
34-
from reactpy.utils import html_to_vdom, import_dotted_path
34+
from reactpy.utils import import_dotted_path, string_to_reactpy
3535

3636
_logger = getLogger(__name__)
3737

@@ -74,7 +74,7 @@ def __init__(
7474
extra_py = pyscript_options.get("extra_py", [])
7575
extra_js = pyscript_options.get("extra_js", {})
7676
config = pyscript_options.get("config", {})
77-
pyscript_head_vdom = html_to_vdom(
77+
pyscript_head_vdom = string_to_reactpy(
7878
pyscript_setup_html(extra_py, extra_js, config)
7979
)
8080
pyscript_head_vdom["tagName"] = ""

src/reactpy/executors/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
REACTPY_RECONNECT_MAX_RETRIES,
1414
)
1515
from reactpy.types import ReactPyConfig, VdomDict
16-
from reactpy.utils import import_dotted_path, vdom_to_html
16+
from reactpy.utils import import_dotted_path, reactpy_to_string
1717

1818
logger = logging.getLogger(__name__)
1919

@@ -41,7 +41,7 @@ def check_path(url_path: str) -> str: # nocov
4141

4242
def vdom_head_to_html(head: VdomDict) -> str:
4343
if isinstance(head, dict) and head.get("tagName") == "head":
44-
return vdom_to_html(head)
44+
return reactpy_to_string(head)
4545

4646
raise ValueError(
4747
"Invalid head element! Element must be either `html.head` or a string."

src/reactpy/pyscript/components.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from reactpy import component, hooks
77
from reactpy.pyscript.utils import pyscript_component_html
88
from reactpy.types import ComponentType, Key
9-
from reactpy.utils import html_to_vdom
9+
from reactpy.utils import string_to_reactpy
1010

1111
if TYPE_CHECKING:
1212
from reactpy.types import VdomDict
@@ -22,15 +22,15 @@ def _pyscript_component(
2222
raise ValueError("At least one file path must be provided.")
2323

2424
rendered, set_rendered = hooks.use_state(False)
25-
initial = html_to_vdom(initial) if isinstance(initial, str) else initial
25+
initial = string_to_reactpy(initial) if isinstance(initial, str) else initial
2626

2727
if not rendered:
2828
# FIXME: This is needed to properly re-render PyScript during a WebSocket
2929
# disconnection / reconnection. There may be a better way to do this in the future.
3030
set_rendered(True)
3131
return None
3232

33-
component_vdom = html_to_vdom(
33+
component_vdom = string_to_reactpy(
3434
pyscript_component_html(tuple(str(fp) for fp in file_paths), initial, root)
3535
)
3636
component_vdom["tagName"] = ""

src/reactpy/pyscript/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import reactpy
1919
from reactpy.config import REACTPY_DEBUG, REACTPY_PATH_PREFIX, REACTPY_WEB_MODULES_DIR
2020
from reactpy.types import VdomDict
21-
from reactpy.utils import vdom_to_html
21+
from reactpy.utils import reactpy_to_string
2222

2323
if TYPE_CHECKING:
2424
from collections.abc import Sequence
@@ -77,7 +77,7 @@ def pyscript_component_html(
7777
file_paths: Sequence[str], initial: str | VdomDict, root: str
7878
) -> str:
7979
"""Renders a PyScript component with the user's code."""
80-
_initial = initial if isinstance(initial, str) else vdom_to_html(initial)
80+
_initial = initial if isinstance(initial, str) else reactpy_to_string(initial)
8181
uuid = uuid4().hex
8282
executor_code = pyscript_executor_html(file_paths=file_paths, uuid=uuid, root=root)
8383

src/reactpy/transforms.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88

99
class RequiredTransforms:
10-
"""Performs any necessary transformations related to `html_to_vdom` to automatically prevent
10+
"""Performs any necessary transformations related to `string_to_reactpy` to automatically prevent
1111
issues with React's rendering engine.
1212
"""
1313

@@ -83,7 +83,7 @@ def select_element_to_reactjs(self, vdom: VdomDict) -> None:
8383
def input_element_value_prop_to_defaultValue(vdom: VdomDict) -> None:
8484
"""ReactJS will complain that inputs are uncontrolled if defining the `value` prop,
8585
so we use `defaultValue` instead. This has an added benefit of not deleting/overriding
86-
any user input when a `html_to_vdom` re-renders fields that do not retain their `value`,
86+
any user input when a `string_to_reactpy` re-renders fields that do not retain their `value`,
8787
such as password fields."""
8888
if vdom["tagName"] != "input":
8989
return
@@ -106,7 +106,7 @@ def infer_key_from_attributes(vdom: VdomDict) -> None:
106106

107107
# Infer 'key' from 'attributes.id'
108108
if key is None:
109-
key = attributes.get("id")
109+
key = attributes.get("id")
110110

111111
# Infer 'key' from 'attributes.name'
112112
if key is None and vdom["tagName"] in {"input", "select", "textarea"}:

src/reactpy/utils.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -61,32 +61,31 @@ def __repr__(self) -> str:
6161
return f"{type(self).__name__}({current})"
6262

6363

64-
def vdom_to_html(vdom: VdomDict) -> str:
65-
"""Convert a VDOM dictionary into an HTML string
66-
67-
Only the following keys are translated to HTML:
68-
69-
- ``tagName``
70-
- ``attributes``
71-
- ``children`` (must be strings or more VDOM dicts)
64+
def reactpy_to_string(root: VdomDict | ComponentType) -> str:
65+
"""Convert a ReactPy component or `reactpy.html` element into an HTML string.
7266
7367
Parameters:
74-
vdom: The VdomDict element to convert to HTML
68+
root: The ReactPy element to convert to a string.
7569
"""
76-
temp_root = etree.Element("__temp__")
77-
_add_vdom_to_etree(temp_root, vdom)
78-
html = cast(bytes, tostring(temp_root)).decode() # type: ignore
79-
# strip out temp root <__temp__> element
70+
temp_container = etree.Element("__temp__")
71+
72+
if not isinstance(root, dict):
73+
root = component_to_vdom(root)
74+
75+
_add_vdom_to_etree(temp_container, root)
76+
html = cast(bytes, tostring(temp_container)).decode() # type: ignore
77+
78+
# Strip out temp root <__temp__> element
8079
return html[10:-11]
8180

8281

83-
def html_to_vdom(
82+
def string_to_reactpy(
8483
html: str,
8584
*transforms: _ModelTransform,
8685
strict: bool = True,
8786
intercept_links: bool = True,
8887
) -> VdomDict:
89-
"""Transform HTML into a DOM model. Unique keys can be provided to HTML elements
88+
"""Transform HTML string into a ReactPy DOM model. ReactJS keys can be provided to HTML elements
9089
using a ``key=...`` attribute within your HTML tag.
9190
9291
Parameters:
@@ -126,7 +125,7 @@ def html_to_vdom(
126125
"An error has occurred while parsing the HTML.\n\n"
127126
"This HTML may be malformatted, or may not perfectly adhere to HTML5.\n"
128127
"If you believe the exception above was due to something intentional, you "
129-
"can disable the strict parameter on html_to_vdom().\n"
128+
"can disable the strict parameter on string_to_reactpy().\n"
130129
"Otherwise, repair your broken HTML and try again."
131130
)
132131
raise HTMLParseError(msg) from e
@@ -228,12 +227,17 @@ def _generate_vdom_children(
228227
)
229228

230229

231-
def component_to_vdom(component: ComponentType) -> VdomDict | str | None:
230+
def component_to_vdom(component: ComponentType) -> VdomDict:
232231
"""Convert the first render of a component into a VDOM dictionary"""
233232
result = component.render()
233+
234+
if isinstance(result, dict):
235+
return result
234236
if hasattr(result, "render"):
235-
result = component_to_vdom(cast(ComponentType, result))
236-
return cast(Union[VdomDict, str, None], result)
237+
return component_to_vdom(cast(ComponentType, result))
238+
elif isinstance(result, str):
239+
return make_vdom("div", {}, result)
240+
return make_vdom("")
237241

238242

239243
def _react_attribute_to_html(key: str, value: Any) -> tuple[str, str]:

tests/test_utils.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ def test_ref_repr():
8888
},
8989
],
9090
)
91-
def test_html_to_vdom(case):
92-
assert utils.html_to_vdom(case["source"]) == case["model"]
91+
def test_string_to_reactpy(case):
92+
assert utils.string_to_reactpy(case["source"]) == case["model"]
9393

9494

9595
@pytest.mark.parametrize(
@@ -192,18 +192,18 @@ def test_html_to_vdom(case):
192192
},
193193
],
194194
)
195-
def test_html_to_vdom_default_transforms(case):
196-
assert utils.html_to_vdom(case["source"]) == case["model"]
195+
def test_string_to_reactpy_default_transforms(case):
196+
assert utils.string_to_reactpy(case["source"]) == case["model"]
197197

198198

199-
def test_html_to_vdom_intercept_links():
199+
def test_string_to_reactpy_intercept_links():
200200
source = '<a href="https://example.com">Hello World</a>'
201201
expected = {
202202
"tagName": "a",
203203
"children": ["Hello World"],
204204
"attributes": {"href": "https://example.com"},
205205
}
206-
result = utils.html_to_vdom(source, intercept_links=True)
206+
result = utils.string_to_reactpy(source, intercept_links=True)
207207

208208
# Check if the result equals expected when removing `eventHandlers` from the result dict
209209
event_handlers = result.pop("eventHandlers", {})
@@ -213,7 +213,7 @@ def test_html_to_vdom_intercept_links():
213213
assert "onClick" in event_handlers
214214

215215

216-
def test_html_to_vdom_custom_transform():
216+
def test_string_to_reactpy_custom_transform():
217217
source = "<p>hello <a>world</a> and <a>universe</a>lmao</p>"
218218

219219
def make_links_blue(node):
@@ -241,7 +241,8 @@ def make_links_blue(node):
241241
}
242242

243243
assert (
244-
utils.html_to_vdom(source, make_links_blue, intercept_links=False) == expected
244+
utils.string_to_reactpy(source, make_links_blue, intercept_links=False)
245+
== expected
245246
)
246247

247248

@@ -256,10 +257,10 @@ def test_non_html_tag_behavior():
256257
],
257258
}
258259

259-
assert utils.html_to_vdom(source, strict=False) == expected
260+
assert utils.string_to_reactpy(source, strict=False) == expected
260261

261262
with pytest.raises(utils.HTMLParseError):
262-
utils.html_to_vdom(source, strict=True)
263+
utils.string_to_reactpy(source, strict=True)
263264

264265

265266
SOME_OBJECT = object()
@@ -342,19 +343,23 @@ def example_child():
342343
html.div(example_parent()),
343344
'<div><div id="sample" style="padding:15px"><h1>Sample Application</h1></div></div>',
344345
),
346+
(
347+
example_parent(),
348+
'<div id="sample" style="padding:15px"><h1>Sample Application</h1></div>',
349+
),
345350
(
346351
html.form({"acceptCharset": "utf-8"}),
347352
'<form accept-charset="utf-8"></form>',
348353
),
349354
],
350355
)
351-
def test_vdom_to_html(vdom_in, html_out):
352-
assert utils.vdom_to_html(vdom_in) == html_out
356+
def test_reactpy_to_string(vdom_in, html_out):
357+
assert utils.reactpy_to_string(vdom_in) == html_out
353358

354359

355-
def test_vdom_to_html_error():
360+
def test_reactpy_to_string_error():
356361
with pytest.raises(TypeError, match="Expected a VDOM dict"):
357-
utils.vdom_to_html({"notVdom": True})
362+
utils.reactpy_to_string({"notVdom": True})
358363

359364

360365
def test_invalid_dotted_path():

0 commit comments

Comments
 (0)