Przeglądaj źródła

Always emit a full dict() when hydrating (#843)

Masen Furer 2 lat temu
rodzic
commit
3c4fb256a2

+ 22 - 4
pynecone/middleware/hydrate_middleware.py

@@ -13,6 +13,12 @@ if TYPE_CHECKING:
     from pynecone.app import App
     from pynecone.app import App
 
 
 
 
+IS_HYDRATED = "is_hydrated"
+
+
+State.add_var(IS_HYDRATED, type_=bool, default_value=False)
+
+
 class HydrateMiddleware(Middleware):
 class HydrateMiddleware(Middleware):
     """Middleware to handle initial app hydration."""
     """Middleware to handle initial app hydration."""
 
 
@@ -38,19 +44,31 @@ class HydrateMiddleware(Middleware):
             else:
             else:
                 load_event = None
                 load_event = None
 
 
+            updates = []
+
+            # first get the initial state
+            delta = format.format_state({state.get_name(): state.dict()})
+            if delta:
+                updates.append(StateUpdate(delta=delta))
+
+            # then apply changes from on_load event handlers on top of that
             if load_event:
             if load_event:
                 if not isinstance(load_event, List):
                 if not isinstance(load_event, List):
                     load_event = [load_event]
                     load_event = [load_event]
-                updates = []
                 for single_event in load_event:
                 for single_event in load_event:
                     updates.append(
                     updates.append(
                         await self.execute_load_event(
                         await self.execute_load_event(
                             state, single_event, event.token, event.payload
                             state, single_event, event.token, event.payload
                         )
                         )
                     )
                     )
-                return updates
-            delta = format.format_state({state.get_name(): state.dict()})
-            return StateUpdate(delta=delta) if delta else None
+            # extra message telling the client state that hydration is complete
+            updates.append(
+                StateUpdate(
+                    delta=format.format_state({state.get_name(): {IS_HYDRATED: True}})
+                )
+            )
+
+            return updates
 
 
     async def execute_load_event(
     async def execute_load_event(
         self, state: State, load_event: EventHandler, token: str, payload: Dict
         self, state: State, load_event: EventHandler, token: str, payload: Dict

+ 62 - 9
tests/middleware/test_hydrate_middleware.py

@@ -1,15 +1,29 @@
-from typing import List
+from typing import Any, Dict, List
 
 
 import pytest
 import pytest
 
 
 from pynecone.app import App
 from pynecone.app import App
-from pynecone.middleware.hydrate_middleware import HydrateMiddleware
+from pynecone.middleware.hydrate_middleware import IS_HYDRATED, HydrateMiddleware
 from pynecone.state import State
 from pynecone.state import State
 
 
 
 
+def exp_is_hydrated(state: State) -> Dict[str, Any]:
+    """Expected IS_HYDRATED delta that would be emitted by HydrateMiddleware.
+
+    Args:
+        state: the State that is hydrated
+
+    Returns:
+        dict similar to that returned by `State.get_delta` with IS_HYDRATED: True
+    """
+    return {state.get_name(): {IS_HYDRATED: True}}
+
+
 class TestState(State):
 class TestState(State):
     """A test state with no return in handler."""
     """A test state with no return in handler."""
 
 
+    __test__ = False
+
     num: int = 0
     num: int = 0
 
 
     def test_handler(self):
     def test_handler(self):
@@ -20,6 +34,8 @@ class TestState(State):
 class TestState2(State):
 class TestState2(State):
     """A test state with return in handler."""
     """A test state with return in handler."""
 
 
+    __test__ = False
+
     num: int = 0
     num: int = 0
     name: str
     name: str
 
 
@@ -40,6 +56,8 @@ class TestState2(State):
 class TestState3(State):
 class TestState3(State):
     """A test state with async handler."""
     """A test state with async handler."""
 
 
+    __test__ = False
+
     num: int = 0
     num: int = 0
 
 
     async def test_handler(self):
     async def test_handler(self):
@@ -47,6 +65,16 @@ class TestState3(State):
         self.num += 1
         self.num += 1
 
 
 
 
+@pytest.fixture
+def hydrate_middleware() -> HydrateMiddleware:
+    """Fixture creates an instance of HydrateMiddleware per test case.
+
+    Returns:
+        instance of HydrateMiddleware
+    """
+    return HydrateMiddleware()
+
+
 @pytest.mark.asyncio
 @pytest.mark.asyncio
 @pytest.mark.parametrize(
 @pytest.mark.parametrize(
     "state, expected, event_fixture",
     "state, expected, event_fixture",
@@ -56,30 +84,34 @@ class TestState3(State):
         (TestState3, {"test_state3": {"num": 1}}, "event3"),
         (TestState3, {"test_state3": {"num": 1}}, "event3"),
     ],
     ],
 )
 )
-async def test_preprocess(state, request, event_fixture, expected):
+async def test_preprocess(state, hydrate_middleware, request, event_fixture, expected):
     """Test that a state hydrate event is processed correctly.
     """Test that a state hydrate event is processed correctly.
 
 
     Args:
     Args:
         state: state to process event
         state: state to process event
+        hydrate_middleware: instance of HydrateMiddleware
         request: pytest fixture request
         request: pytest fixture request
         event_fixture: The event fixture(an Event)
         event_fixture: The event fixture(an Event)
         expected: expected delta
         expected: expected delta
     """
     """
     app = App(state=state, load_events={"index": state.test_handler})
     app = App(state=state, load_events={"index": state.test_handler})
 
 
-    hydrate_middleware = HydrateMiddleware()
     result = await hydrate_middleware.preprocess(
     result = await hydrate_middleware.preprocess(
         app=app, event=request.getfixturevalue(event_fixture), state=state()
         app=app, event=request.getfixturevalue(event_fixture), state=state()
     )
     )
     assert isinstance(result, List)
     assert isinstance(result, List)
-    assert result[0].delta == expected
+    assert len(result) == 3
+    assert result[0].delta == {state().get_name(): state().dict()}
+    assert result[1].delta == expected
+    assert result[2].delta == exp_is_hydrated(state())
 
 
 
 
 @pytest.mark.asyncio
 @pytest.mark.asyncio
-async def test_preprocess_multiple_load_events(event1):
+async def test_preprocess_multiple_load_events(hydrate_middleware, event1):
     """Test that a state hydrate event for multiple on-load events is processed correctly.
     """Test that a state hydrate event for multiple on-load events is processed correctly.
 
 
     Args:
     Args:
+        hydrate_middleware: instance of HydrateMiddleware
         event1: an Event.
         event1: an Event.
     """
     """
     app = App(
     app = App(
@@ -87,10 +119,31 @@ async def test_preprocess_multiple_load_events(event1):
         load_events={"index": [TestState.test_handler, TestState.test_handler]},
         load_events={"index": [TestState.test_handler, TestState.test_handler]},
     )
     )
 
 
-    hydrate_middleware = HydrateMiddleware()
     result = await hydrate_middleware.preprocess(
     result = await hydrate_middleware.preprocess(
         app=app, event=event1, state=TestState()
         app=app, event=event1, state=TestState()
     )
     )
     assert isinstance(result, List)
     assert isinstance(result, List)
-    assert result[0].delta == {"test_state": {"num": 1}}
-    assert result[1].delta == {"test_state": {"num": 2}}
+    assert len(result) == 4
+    assert result[0].delta == {"test_state": TestState().dict()}
+    assert result[1].delta == {"test_state": {"num": 1}}
+    assert result[2].delta == {"test_state": {"num": 2}}
+    assert result[3].delta == exp_is_hydrated(TestState())
+
+
+@pytest.mark.asyncio
+async def test_preprocess_no_events(hydrate_middleware, event1):
+    """Test that app without on_load is processed correctly.
+
+    Args:
+        hydrate_middleware: instance of HydrateMiddleware
+        event1: an Event.
+    """
+    result = await hydrate_middleware.preprocess(
+        app=App(state=TestState),
+        event=event1,
+        state=TestState(),
+    )
+    assert isinstance(result, List)
+    assert len(result) == 2
+    assert result[0].delta == {"test_state": TestState().dict()}
+    assert result[1].delta == exp_is_hydrated(TestState())

+ 2 - 0
tests/test_state.py

@@ -6,6 +6,7 @@ from plotly.graph_objects import Figure
 from pynecone.base import Base
 from pynecone.base import Base
 from pynecone.constants import RouteVar
 from pynecone.constants import RouteVar
 from pynecone.event import Event
 from pynecone.event import Event
+from pynecone.middleware.hydrate_middleware import IS_HYDRATED
 from pynecone.state import State
 from pynecone.state import State
 from pynecone.utils import format
 from pynecone.utils import format
 from pynecone.var import BaseVar, ComputedVar
 from pynecone.var import BaseVar, ComputedVar
@@ -191,6 +192,7 @@ def test_class_vars(test_state):
     """
     """
     cls = type(test_state)
     cls = type(test_state)
     assert set(cls.vars.keys()) == {
     assert set(cls.vars.keys()) == {
+        IS_HYDRATED,  # added by hydrate_middleware to all State
         "num1",
         "num1",
         "num2",
         "num2",
         "key",
         "key",