From 6d63e97feffb878da24bc49e539884a36a7de1fb Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:15:26 -0800 Subject: [PATCH 1/6] Add support for `ComponentType` children in `vdom_to_html` --- src/reactpy/utils.py | 52 +++++++++++++++++++++++++------------------- tests/test_utils.py | 18 ++++++++++----- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/reactpy/utils.py b/src/reactpy/utils.py index a20194902..46743c657 100644 --- a/src/reactpy/utils.py +++ b/src/reactpy/utils.py @@ -8,7 +8,7 @@ from lxml import etree from lxml.html import fromstring, tostring -from reactpy.core.types import VdomDict +from reactpy.core.types import ComponentType, VdomDict from reactpy.core.vdom import vdom _RefValue = TypeVar("_RefValue") @@ -156,42 +156,42 @@ def _etree_to_vdom( return el -def _add_vdom_to_etree(parent: etree._Element, vdom: VdomDict | dict[str, Any]) -> None: +def _add_vdom_to_etree(parent: etree._Element, node: VdomDict | dict[str, Any]) -> None: try: - tag = vdom["tagName"] + tag = node["tagName"] except KeyError as e: - msg = f"Expected a VDOM dict, not {vdom}" + msg = f"Expected a VDOM dict, not {type(node)}" raise TypeError(msg) from e else: - vdom = cast(VdomDict, vdom) + node = cast(VdomDict, node) if tag: element = etree.SubElement(parent, tag) element.attrib.update( - _vdom_attr_to_html_str(k, v) for k, v in vdom.get("attributes", {}).items() + _vdom_attr_to_html_str(k, v) for k, v in node.get("attributes", {}).items() ) else: element = parent - for c in vdom.get("children", []): + for c in node.get("children", []): + if hasattr(c, "render"): + c = _component_to_vdom(cast(ComponentType, c)) if isinstance(c, dict): _add_vdom_to_etree(element, c) else: - """ - LXML handles string children by storing them under `text` and `tail` - attributes of Element objects. The `text` attribute, if present, effectively - becomes that element's first child. Then the `tail` attribute, if present, - becomes a sibling that follows that element. For example, consider the - following HTML: - -

helloworld

- - In this code sample, "hello" is the `text` attribute of the `` element - and "world" is the `tail` attribute of that same `` element. It's for - this reason that, depending on whether the element being constructed has - non-string a child element, we need to assign a `text` vs `tail` attribute - to that element or the last non-string child respectively. - """ + # LXML handles string children by storing them under `text` and `tail` + # attributes of Element objects. The `text` attribute, if present, effectively + # becomes that element's first child. Then the `tail` attribute, if present, + # becomes a sibling that follows that element. For example, consider the + # following HTML: + + #

helloworld

+ + # In this code sample, "hello" is the `text` attribute of the `` element + # and "world" is the `tail` attribute of that same `` element. It's for + # this reason that, depending on whether the element being constructed has + # non-string a child element, we need to assign a `text` vs `tail` attribute + # to that element or the last non-string child respectively. if len(element): last_child = element[-1] last_child.tail = f"{last_child.tail or ''}{c}" @@ -249,6 +249,14 @@ def _generate_vdom_children( ) +def _component_to_vdom(component: ComponentType) -> VdomDict | str | None: + """Convert a component to a VDOM dictionary""" + result = component.render() + if hasattr(result, "render"): + result = _component_to_vdom(cast(ComponentType, result)) + return cast(VdomDict, result) + + def del_html_head_body_transform(vdom: VdomDict) -> VdomDict: """Transform intended for use with `html_to_vdom`. diff --git a/tests/test_utils.py b/tests/test_utils.py index b071fdc9f..012029216 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,7 @@ import pytest import reactpy -from reactpy import html +from reactpy import component, html from reactpy.utils import ( HTMLParseError, del_html_head_body_transform, @@ -193,6 +193,16 @@ def test_del_html_body_transform(): SOME_OBJECT = object() +@component +def example_parent(): + return html.div({"id": "sample", "style": {"padding": "15px"}}, example_child()) + + +@component +def example_child(): + return html.h1("Sample Application") + + @pytest.mark.parametrize( "vdom_in, html_out", [ @@ -254,10 +264,8 @@ def test_del_html_body_transform(): '
', ), ( - html.div( - {"dataSomething": 1, "dataSomethingElse": 2, "dataisnotdashed": 3} - ), - '
', + html.div(example_parent()), + '

Sample Application

', ), ], ) From c7122d50ee3f802a245822a8926a0c24a348d3a6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:22:34 -0800 Subject: [PATCH 2/6] minor refactoring --- src/reactpy/utils.py | 40 ++++++++++++++++++++-------------------- tests/test_utils.py | 5 +++++ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/reactpy/utils.py b/src/reactpy/utils.py index 46743c657..c93e95669 100644 --- a/src/reactpy/utils.py +++ b/src/reactpy/utils.py @@ -9,7 +9,7 @@ from lxml.html import fromstring, tostring from reactpy.core.types import ComponentType, VdomDict -from reactpy.core.vdom import vdom +from reactpy.core.vdom import vdom as make_vdom _RefValue = TypeVar("_RefValue") _ModelTransform = Callable[[VdomDict], Any] @@ -144,7 +144,7 @@ def _etree_to_vdom( children = _generate_vdom_children(node, transforms) # Convert the lxml node to a VDOM dict - el = vdom(node.tag, dict(node.items()), *children) + el = make_vdom(node.tag, dict(node.items()), *children) # Perform any necessary mutations on the VDOM attributes to meet VDOM spec _mutate_vdom(el) @@ -178,25 +178,25 @@ def _add_vdom_to_etree(parent: etree._Element, node: VdomDict | dict[str, Any]) c = _component_to_vdom(cast(ComponentType, c)) if isinstance(c, dict): _add_vdom_to_etree(element, c) + + # LXML handles string children by storing them under `text` and `tail` + # attributes of Element objects. The `text` attribute, if present, effectively + # becomes that element's first child. Then the `tail` attribute, if present, + # becomes a sibling that follows that element. For example, consider the + # following HTML: + + #

helloworld

+ + # In this code sample, "hello" is the `text` attribute of the `` element + # and "world" is the `tail` attribute of that same `` element. It's for + # this reason that, depending on whether the element being constructed has + # non-string a child element, we need to assign a `text` vs `tail` attribute + # to that element or the last non-string child respectively. + elif len(element): + last_child = element[-1] + last_child.tail = f"{last_child.tail or ''}{c}" else: - # LXML handles string children by storing them under `text` and `tail` - # attributes of Element objects. The `text` attribute, if present, effectively - # becomes that element's first child. Then the `tail` attribute, if present, - # becomes a sibling that follows that element. For example, consider the - # following HTML: - - #

helloworld

- - # In this code sample, "hello" is the `text` attribute of the `` element - # and "world" is the `tail` attribute of that same `` element. It's for - # this reason that, depending on whether the element being constructed has - # non-string a child element, we need to assign a `text` vs `tail` attribute - # to that element or the last non-string child respectively. - if len(element): - last_child = element[-1] - last_child.tail = f"{last_child.tail or ''}{c}" - else: - element.text = f"{element.text or ''}{c}" + element.text = f"{element.text or ''}{c}" def _mutate_vdom(vdom: VdomDict) -> None: diff --git a/tests/test_utils.py b/tests/test_utils.py index 012029216..ef67766e5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -195,6 +195,11 @@ def test_del_html_body_transform(): @component def example_parent(): + return example_middle() + + +@component +def example_middle(): return html.div({"id": "sample", "style": {"padding": "15px"}}, example_child()) From a27a00f2ec5d3081a051e95301a72199943407a6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:24:30 -0800 Subject: [PATCH 3/6] Reduce LOC changes --- src/reactpy/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/reactpy/utils.py b/src/reactpy/utils.py index c93e95669..471784fdb 100644 --- a/src/reactpy/utils.py +++ b/src/reactpy/utils.py @@ -156,24 +156,24 @@ def _etree_to_vdom( return el -def _add_vdom_to_etree(parent: etree._Element, node: VdomDict | dict[str, Any]) -> None: +def _add_vdom_to_etree(parent: etree._Element, vdom: VdomDict | dict[str, Any]) -> None: try: - tag = node["tagName"] + tag = vdom["tagName"] except KeyError as e: - msg = f"Expected a VDOM dict, not {type(node)}" + msg = f"Expected a VDOM dict, not {type(vdom)}" raise TypeError(msg) from e else: - node = cast(VdomDict, node) + vdom = cast(VdomDict, vdom) if tag: element = etree.SubElement(parent, tag) element.attrib.update( - _vdom_attr_to_html_str(k, v) for k, v in node.get("attributes", {}).items() + _vdom_attr_to_html_str(k, v) for k, v in vdom.get("attributes", {}).items() ) else: element = parent - for c in node.get("children", []): + for c in vdom.get("children", []): if hasattr(c, "render"): c = _component_to_vdom(cast(ComponentType, c)) if isinstance(c, dict): From 55ae546a5ce90f630d19083f731c62dbc7f2886a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:25:35 -0800 Subject: [PATCH 4/6] More accurate types on _component_to_vdom --- src/reactpy/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactpy/utils.py b/src/reactpy/utils.py index 471784fdb..1c68274d0 100644 --- a/src/reactpy/utils.py +++ b/src/reactpy/utils.py @@ -254,7 +254,7 @@ def _component_to_vdom(component: ComponentType) -> VdomDict | str | None: result = component.render() if hasattr(result, "render"): result = _component_to_vdom(cast(ComponentType, result)) - return cast(VdomDict, result) + return cast(VdomDict | str | None, result) def del_html_head_body_transform(vdom: VdomDict) -> VdomDict: From 20848ab51970648f1c9d45766b04c90f22699375 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:31:01 -0800 Subject: [PATCH 5/6] Fix python 3.9 support --- src/reactpy/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reactpy/utils.py b/src/reactpy/utils.py index 1c68274d0..77df473fb 100644 --- a/src/reactpy/utils.py +++ b/src/reactpy/utils.py @@ -3,7 +3,7 @@ import re from collections.abc import Iterable from itertools import chain -from typing import Any, Callable, Generic, TypeVar, cast +from typing import Any, Callable, Generic, TypeVar, Union, cast from lxml import etree from lxml.html import fromstring, tostring @@ -254,7 +254,7 @@ def _component_to_vdom(component: ComponentType) -> VdomDict | str | None: result = component.render() if hasattr(result, "render"): result = _component_to_vdom(cast(ComponentType, result)) - return cast(VdomDict | str | None, result) + return cast(Union[VdomDict, str, None], result) def del_html_head_body_transform(vdom: VdomDict) -> VdomDict: From bdfc816c23aa3659b6c32fcebf8708bdf2408e3e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:31:15 -0800 Subject: [PATCH 6/6] Add changelog --- docs/source/about/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index a9ddbe854..178fbba19 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -21,6 +21,7 @@ Unreleased - :pull:`1239` - Script elements no longer support behaving like effects. They now strictly behave like plain HTML script elements. - :pull:`1255` - The ``reactpy.html`` module has been modified to allow for auto-creation of any HTML nodes. For example, you can create a ```` element by calling ``html.data_table()``. - :pull:`1256` - Change ``set_state`` comparison method to check equality with ``==`` more consistently. +- :pull:`1257` - Add support for rendering ``@component`` children within ``vdom_to_html``. **Removed**