From dc4aafcddd8f4b639dd4dd8aaae1b71c18c7ff29 Mon Sep 17 00:00:00 2001 From: Masahiro Wada Date: Fri, 29 Apr 2022 03:02:24 +0000 Subject: [PATCH] feat: Add enqueue support for rendergin --- alfort/__init__.py | 4 +- alfort/app.py | 26 +++++++----- alfort/vdom.py | 9 ++-- tests/test_app.py | 100 ++++++++++++++++++++++++++++++++++----------- 4 files changed, 98 insertions(+), 41 deletions(-) diff --git a/alfort/__init__.py b/alfort/__init__.py index 63a11b9..dd1cf4a 100644 --- a/alfort/__init__.py +++ b/alfort/__init__.py @@ -5,6 +5,6 @@ except PackageNotFoundError: __version__: str = "unknown" -from .app import Alfort, Dispatch, Effect, Init, Mount, Update, View +from .app import Alfort, Dispatch, Effect, Init, Update, View -__all__ = ["Alfort", "Dispatch", "Effect", "View", "Update", "Init", "Mount"] +__all__ = ["Alfort", "Dispatch", "Effect", "View", "Update", "Init"] diff --git a/alfort/app.py b/alfort/app.py index 8904ae1..076b8f1 100644 --- a/alfort/app.py +++ b/alfort/app.py @@ -23,10 +23,10 @@ Dispatch: TypeAlias = Callable[[M], None] Effect: TypeAlias = Callable[[Dispatch[M]], None] -View: TypeAlias = Callable[[S], VDom | None] -Update: TypeAlias = Callable[[M, S], tuple[S, list[Effect[M]]]] Init: TypeAlias = Callable[[], tuple[S, list[Effect[M]]]] -Mount: TypeAlias = Callable[[N], None] +View: TypeAlias = Callable[[S], VDom] +Update: TypeAlias = Callable[[M, S], tuple[S, list[Effect[M]]]] +Enqueue: TypeAlias = Callable[[Callable[[], None]], None] @dataclass(slots=True, frozen=True) @@ -185,20 +185,26 @@ def _main( init: Init[S, M], view: View[S], update: Update[M, S], - mount: Mount[N], + root_node: N, + enqueue: Enqueue = lambda render: render(), ) -> None: state, effects = init() - root = None + root = NodeDomElement(tag="__root__", props={}, children=[], node=root_node) + + def rooted_view(state: S) -> VDom: + return VDomElement("__root__", {}, [view(state)]) + + def render() -> None: + nonlocal state + nonlocal root + (root, _) = cls.patch(dispatch, root, rooted_view(state)) def dispatch(msg: M) -> None: nonlocal state nonlocal root (state, effects) = update(msg, state) + enqueue(render) cls._run_effects(dispatch, effects) - (root, _) = cls.patch(dispatch, root, view(state)) + enqueue(render) cls._run_effects(dispatch, effects) - (root, _) = cls.patch(dispatch, None, view(state)) - - if root is not None and root.node is not None: - mount(root.node) diff --git a/alfort/vdom.py b/alfort/vdom.py index 63e8c0a..8c12b87 100644 --- a/alfort/vdom.py +++ b/alfort/vdom.py @@ -3,11 +3,13 @@ T = TypeVar("T") +Props: TypeAlias = MutableMapping[str, Any] + @dataclass(slots=True, frozen=True) class PatchProps: remove_keys: list[str] - add_props: "Props" + add_props: Props @dataclass(slots=True, frozen=True) @@ -37,7 +39,7 @@ def apply(self, patch: Patch) -> None: @dataclass(slots=True, frozen=True) class Element(Generic[T]): tag: str - props: "Props" + props: Props children: list[T] @@ -49,9 +51,6 @@ class VDomElement(Element["VDom"]): VDom = VDomElement | str -Props: TypeAlias = MutableMapping[str, Any] - - def el( tag: str, props: Props | None = None, diff --git a/tests/test_app.py b/tests/test_app.py index f1cf34d..12ad00d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,10 +1,10 @@ from dataclasses import dataclass -from typing import Any +from typing import Any, Callable, Generic, TypeVar import pytest from alfort import Alfort, Dispatch, Effect, Init, Update, View -from alfort.app import Mount, NodeDom, NodeDomElement, NodeDomText +from alfort.app import Enqueue, NodeDom, NodeDomElement, NodeDomText from alfort.vdom import ( Node, Patch, @@ -17,6 +17,8 @@ el, ) +T = TypeVar("T", bound=Node) + def to_vnode(node_dom: NodeDom) -> VDom: match node_dom: @@ -26,6 +28,20 @@ def to_vnode(node_dom: NodeDom) -> VDom: return el(tag, props, [to_vnode(child) for child in children]) +class RootNode(Node, Generic[T]): + child: T | None + + def __init__(self) -> None: + self.child = None + + def apply(self, patch: Patch) -> None: + match patch: + case PatchInsertChild(child): + self.child = child + case _: + raise ValueError(f"Invalid patch: {patch}") + + class MockNode(Node): patches: list[Patch] @@ -61,12 +77,7 @@ def main( update: Update[Any, dict[str, Any]], ) -> None: - cls._main( - init=init, - view=view, - update=update, - mount=lambda node: None, - ) + cls._main(init=init, view=view, update=update, root_node=cls.mock_target) @pytest.mark.parametrize( @@ -194,7 +205,7 @@ def apply(self, patch: Patch) -> None: raise ValueError(f"Invalid patch: {patch}") -class AlfortText(Alfort[dict[str, Any], Msg, TextNode]): +class AlfortText(Alfort[dict[str, Any], Msg, TextNode | RootNode[TextNode]]): @classmethod def create_text( cls, @@ -208,10 +219,10 @@ def create_element( cls, tag: str, props: Props, - children: list[TextNode], + children: list[TextNode | RootNode[TextNode]], dispatch: Dispatch[Msg], - ) -> TextNode: - return TextNode("", dispatch) + ) -> RootNode[TextNode]: + return RootNode() @classmethod def main( @@ -219,13 +230,16 @@ def main( init: Init[dict[str, Any], Msg], view: View[dict[str, Any]], update: Update[Msg, dict[str, Any]], - mount: Mount[TextNode], + root_node: RootNode[TextNode], + enqueue: Enqueue = lambda render: render(), ) -> None: - cls._main(init=init, view=view, update=update, mount=mount) + cls._main( + init=init, view=view, update=update, root_node=root_node, enqueue=enqueue + ) def test_update_state() -> None: - root = TextNode("", lambda _: None) + root = RootNode[TextNode]() def view(state: dict[str, int]) -> VDom: return str(state["count"]) @@ -242,14 +256,52 @@ def update( case CountDown(value): return ({"count": state["count"] - value}, []) - def mount(target: TextNode) -> None: - nonlocal root - root = target + AlfortText.main(init=init, view=view, update=update, root_node=root) + + assert root.child is not None + assert root.child.text == "3" + root.child.dispatch(CountUp()) + assert root.child.text == "4" + root.child.dispatch(CountDown()) + assert root.child.text == "3" + + +def test_enqueue() -> None: + root = RootNode[TextNode]() + + view_values: list[str | None] = [] + + def capture(_: Dispatch[Msg]) -> None: + nonlocal view_values + if root.child is None: + view_values.append(None) + else: + view_values.append(root.child.text) + + rendering_queue: list[Callable[[], None]] = [] + + def enqueue(f: Callable[[], None]) -> None: + rendering_queue.append(f) + + def render() -> None: + for f in rendering_queue: + f() + + def view(state: dict[str, int]) -> VDom: + return str(state["count"]) + + def init() -> tuple[dict[str, int], list[Effect[Msg]]]: + return ({"count": 5}, [capture, lambda d: enqueue(lambda: capture(d))]) + + def update( + msg: Msg, state: dict[str, int] + ) -> tuple[dict[str, int], list[Effect[Msg]]]: + return (state, []) - AlfortText.main(init=init, view=view, update=update, mount=mount) + AlfortText.main( + init=init, view=view, update=update, root_node=root, enqueue=enqueue + ) - assert root.text == view({"count": 3}) - root.dispatch(CountUp()) - assert root.text == view({"count": 4}) - root.dispatch(CountDown()) - assert root.text == view({"count": 3}) + assert view_values == [None] + render() + assert view_values == [None, "5"]