Forráskód Böngészése

Make PCList pickleable (#500)

Elijah Ahianyo 2 éve
szülő
commit
69a9c95d73
6 módosított fájl, 158 hozzáadás és 135 törlés
  1. 13 1
      poetry.lock
  2. 13 15
      pynecone/state.py
  3. 1 1
      pynecone/var.py
  4. 1 0
      pyproject.toml
  5. 118 117
      tests/test_app.py
  6. 12 1
      tests/test_var.py

+ 13 - 1
poetry.lock

@@ -138,6 +138,18 @@ files = [
 colorama = {version = "*", markers = "platform_system == \"Windows\""}
 colorama = {version = "*", markers = "platform_system == \"Windows\""}
 importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 
 
+[[package]]
+name = "cloudpickle"
+version = "2.2.1"
+description = "Extended pickling support for Python objects"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "cloudpickle-2.2.1-py3-none-any.whl", hash = "sha256:61f594d1f4c295fa5cd9014ceb3a1fc4a70b0de1164b94fbc2d854ccba056f9f"},
+    {file = "cloudpickle-2.2.1.tar.gz", hash = "sha256:d89684b8de9e34a2a43b3460fbca07d09d6e25ce858df4d5a44240403b6178f5"},
+]
+
 [[package]]
 [[package]]
 name = "colorama"
 name = "colorama"
 version = "0.4.6"
 version = "0.4.6"
@@ -1198,4 +1210,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 [metadata]
 lock-version = "2.0"
 lock-version = "2.0"
 python-versions = "^3.7"
 python-versions = "^3.7"
-content-hash = "0a0cbe4cdf4f07a69b36c76988a3ae25f7d3856a6ab40ace5ee0682c357723d8"
+content-hash = "5be82a91acbdb6df10d3c020b569c583a319905362e94a09c57a55892e1e98dd"

+ 13 - 15
pynecone/state.py

@@ -3,17 +3,17 @@ from __future__ import annotations
 
 
 import asyncio
 import asyncio
 import functools
 import functools
-import pickle
 import traceback
 import traceback
 from abc import ABC
 from abc import ABC
 from typing import Any, Callable, ClassVar, Dict, List, Optional, Sequence, Set, Type
 from typing import Any, Callable, ClassVar, Dict, List, Optional, Sequence, Set, Type
 
 
+import cloudpickle
 from redis import Redis
 from redis import Redis
 
 
 from pynecone import constants, utils
 from pynecone import constants, utils
 from pynecone.base import Base
 from pynecone.base import Base
 from pynecone.event import Event, EventHandler, window_alert
 from pynecone.event import Event, EventHandler, window_alert
-from pynecone.var import BaseVar, ComputedVar, Var
+from pynecone.var import BaseVar, ComputedVar, PCList, Var
 
 
 Delta = Dict[str, Any]
 Delta = Dict[str, Any]
 
 
@@ -678,7 +678,7 @@ class StateManager(Base):
             if redis_state is None:
             if redis_state is None:
                 self.set_state(token, self.state())
                 self.set_state(token, self.state())
                 return self.get_state(token)
                 return self.get_state(token)
-            return pickle.loads(redis_state)
+            return cloudpickle.loads(redis_state)
 
 
         if token not in self.states:
         if token not in self.states:
             self.states[token] = self.state()
             self.states[token] = self.state()
@@ -693,7 +693,7 @@ class StateManager(Base):
         """
         """
         if self.redis is None:
         if self.redis is None:
             return
             return
-        self.redis.set(token, pickle.dumps(state), ex=self.token_expiration)
+        self.redis.set(token, cloudpickle.dumps(state), ex=self.token_expiration)
 
 
 
 
 def _convert_mutable_datatypes(
 def _convert_mutable_datatypes(
@@ -712,17 +712,15 @@ def _convert_mutable_datatypes(
     Returns:
     Returns:
         The converted field_value
         The converted field_value
     """
     """
-    # TODO: The PCList class needs to be pickleable to work with Redis.
-    # We will uncomment this code once this is fixed.
-    # if isinstance(field_value, list):
-    #     for index in range(len(field_value)):
-    #         field_value[index] = _convert_mutable_datatypes(
-    #             field_value[index], reassign_field, field_name
-    #         )
-
-    #     field_value = PCList(
-    #         field_value, reassign_field=reassign_field, field_name=field_name
-    #     )
+    if isinstance(field_value, list):
+        for index in range(len(field_value)):
+            field_value[index] = _convert_mutable_datatypes(
+                field_value[index], reassign_field, field_name
+            )
+
+        field_value = PCList(
+            field_value, reassign_field=reassign_field, field_name=field_name
+        )
 
 
     if isinstance(field_value, dict):
     if isinstance(field_value, dict):
         for key, value in field_value.items():
         for key, value in field_value.items():

+ 1 - 1
pynecone/var.py

@@ -821,7 +821,7 @@ class PCList(list):
             kargs: The kwargs passed.
             kargs: The kwargs passed.
         """
         """
         super().extend(*args, **kargs)
         super().extend(*args, **kargs)
-        self._reassign_field()
+        self._reassign_field() if hasattr(self, "_reassign_field") else None
 
 
     def pop(self, *args, **kargs):
     def pop(self, *args, **kargs):
         """Remove an element.
         """Remove an element.

+ 1 - 0
pyproject.toml

@@ -37,6 +37,7 @@ httpx = "^0.23.1"
 python-socketio = "^5.7.2"
 python-socketio = "^5.7.2"
 psutil = "^5.9.4"
 psutil = "^5.9.4"
 websockets = "^10.4"
 websockets = "^10.4"
+cloudpickle = "^2.2.1"
 
 
 [tool.poetry.dev-dependencies]
 [tool.poetry.dev-dependencies]
 pytest = "^7.1.2"
 pytest = "^7.1.2"

+ 118 - 117
tests/test_app.py

@@ -1,10 +1,11 @@
 import os.path
 import os.path
-from typing import Type
+from typing import List, Tuple, Type
 
 
 import pytest
 import pytest
 
 
 from pynecone.app import App, DefaultState
 from pynecone.app import App, DefaultState
 from pynecone.components import Box
 from pynecone.components import Box
+from pynecone.event import Event
 from pynecone.middleware import HydrateMiddleware
 from pynecone.middleware import HydrateMiddleware
 from pynecone.state import State
 from pynecone.state import State
 from pynecone.style import Style
 from pynecone.style import Style
@@ -226,119 +227,119 @@ def list_mutation_state():
     return TestState()
     return TestState()
 
 
 
 
-# @pytest.mark.asyncio
-# @pytest.mark.parametrize(
-#     "event_tuples",
-#     [
-#         pytest.param(
-#             [
-#                 (
-#                     "test_state.make_friend",
-#                     {"test_state": {"plain_friends": ["Tommy", "another-fd"]}},
-#                 ),
-#                 (
-#                     "test_state.change_first_friend",
-#                     {"test_state": {"plain_friends": ["Jenny", "another-fd"]}},
-#                 ),
-#             ],
-#             id="append then __setitem__",
-#         ),
-#         pytest.param(
-#             [
-#                 (
-#                     "test_state.unfriend_first_friend",
-#                     {"test_state": {"plain_friends": []}},
-#                 ),
-#                 (
-#                     "test_state.make_friend",
-#                     {"test_state": {"plain_friends": ["another-fd"]}},
-#                 ),
-#             ],
-#             id="delitem then append",
-#         ),
-#         pytest.param(
-#             [
-#                 (
-#                     "test_state.make_friends_with_colleagues",
-#                     {"test_state": {"plain_friends": ["Tommy", "Peter", "Jimmy"]}},
-#                 ),
-#                 (
-#                     "test_state.remove_tommy",
-#                     {"test_state": {"plain_friends": ["Peter", "Jimmy"]}},
-#                 ),
-#                 (
-#                     "test_state.remove_last_friend",
-#                     {"test_state": {"plain_friends": ["Peter"]}},
-#                 ),
-#                 (
-#                     "test_state.unfriend_all_friends",
-#                     {"test_state": {"plain_friends": []}},
-#                 ),
-#             ],
-#             id="extend, remove, pop, clear",
-#         ),
-#         pytest.param(
-#             [
-#                 (
-#                     "test_state.add_jimmy_to_second_group",
-#                     {
-#                         "test_state": {
-#                             "friends_in_nested_list": [["Tommy"], ["Jenny", "Jimmy"]]
-#                         }
-#                     },
-#                 ),
-#                 (
-#                     "test_state.remove_first_person_from_first_group",
-#                     {
-#                         "test_state": {
-#                             "friends_in_nested_list": [[], ["Jenny", "Jimmy"]]
-#                         }
-#                     },
-#                 ),
-#                 (
-#                     "test_state.remove_first_group",
-#                     {"test_state": {"friends_in_nested_list": [["Jenny", "Jimmy"]]}},
-#                 ),
-#             ],
-#             id="nested list",
-#         ),
-#         pytest.param(
-#             [
-#                 (
-#                     "test_state.add_jimmy_to_tommy_friends",
-#                     {"test_state": {"friends_in_dict": {"Tommy": ["Jenny", "Jimmy"]}}},
-#                 ),
-#                 (
-#                     "test_state.remove_jenny_from_tommy",
-#                     {"test_state": {"friends_in_dict": {"Tommy": ["Jimmy"]}}},
-#                 ),
-#                 (
-#                     "test_state.tommy_has_no_fds",
-#                     {"test_state": {"friends_in_dict": {"Tommy": []}}},
-#                 ),
-#             ],
-#             id="list in dict",
-#         ),
-#     ],
-# )
-# async def test_list_mutation_detection__plain_list(
-#     event_tuples: List[Tuple[str, List[str]]], list_mutation_state: State
-# ):
-#     """Test list mutation detection
-#     when reassignment is not explicitly included in the logic.
-
-#     Args:
-#         event_tuples: From parametrization.
-#         list_mutation_state: A state with list mutation features.
-#     """
-#     for event_name, expected_delta in event_tuples:
-#         result = await list_mutation_state.process(
-#             Event(
-#                 token="fake-token",
-#                 name=event_name,
-#                 router_data={"pathname": "/", "query": {}},
-#                 payload={},
-#             )
-#         )
-
-#         assert result.delta == expected_delta
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+    "event_tuples",
+    [
+        pytest.param(
+            [
+                (
+                    "test_state.make_friend",
+                    {"test_state": {"plain_friends": ["Tommy", "another-fd"]}},
+                ),
+                (
+                    "test_state.change_first_friend",
+                    {"test_state": {"plain_friends": ["Jenny", "another-fd"]}},
+                ),
+            ],
+            id="append then __setitem__",
+        ),
+        pytest.param(
+            [
+                (
+                    "test_state.unfriend_first_friend",
+                    {"test_state": {"plain_friends": []}},
+                ),
+                (
+                    "test_state.make_friend",
+                    {"test_state": {"plain_friends": ["another-fd"]}},
+                ),
+            ],
+            id="delitem then append",
+        ),
+        pytest.param(
+            [
+                (
+                    "test_state.make_friends_with_colleagues",
+                    {"test_state": {"plain_friends": ["Tommy", "Peter", "Jimmy"]}},
+                ),
+                (
+                    "test_state.remove_tommy",
+                    {"test_state": {"plain_friends": ["Peter", "Jimmy"]}},
+                ),
+                (
+                    "test_state.remove_last_friend",
+                    {"test_state": {"plain_friends": ["Peter"]}},
+                ),
+                (
+                    "test_state.unfriend_all_friends",
+                    {"test_state": {"plain_friends": []}},
+                ),
+            ],
+            id="extend, remove, pop, clear",
+        ),
+        pytest.param(
+            [
+                (
+                    "test_state.add_jimmy_to_second_group",
+                    {
+                        "test_state": {
+                            "friends_in_nested_list": [["Tommy"], ["Jenny", "Jimmy"]]
+                        }
+                    },
+                ),
+                (
+                    "test_state.remove_first_person_from_first_group",
+                    {
+                        "test_state": {
+                            "friends_in_nested_list": [[], ["Jenny", "Jimmy"]]
+                        }
+                    },
+                ),
+                (
+                    "test_state.remove_first_group",
+                    {"test_state": {"friends_in_nested_list": [["Jenny", "Jimmy"]]}},
+                ),
+            ],
+            id="nested list",
+        ),
+        pytest.param(
+            [
+                (
+                    "test_state.add_jimmy_to_tommy_friends",
+                    {"test_state": {"friends_in_dict": {"Tommy": ["Jenny", "Jimmy"]}}},
+                ),
+                (
+                    "test_state.remove_jenny_from_tommy",
+                    {"test_state": {"friends_in_dict": {"Tommy": ["Jimmy"]}}},
+                ),
+                (
+                    "test_state.tommy_has_no_fds",
+                    {"test_state": {"friends_in_dict": {"Tommy": []}}},
+                ),
+            ],
+            id="list in dict",
+        ),
+    ],
+)
+async def test_list_mutation_detection__plain_list(
+    event_tuples: List[Tuple[str, List[str]]], list_mutation_state: State
+):
+    """Test list mutation detection
+    when reassignment is not explicitly included in the logic.
+
+    Args:
+        event_tuples: From parametrization.
+        list_mutation_state: A state with list mutation features.
+    """
+    for event_name, expected_delta in event_tuples:
+        result = await list_mutation_state.process(
+            Event(
+                token="fake-token",
+                name=event_name,
+                router_data={"pathname": "/", "query": {}},
+                payload={},
+            )
+        )
+
+        assert result.delta == expected_delta

+ 12 - 1
tests/test_var.py

@@ -1,9 +1,10 @@
 from typing import Dict, List
 from typing import Dict, List
 
 
+import cloudpickle
 import pytest
 import pytest
 
 
 from pynecone.base import Base
 from pynecone.base import Base
-from pynecone.var import BaseVar, Var
+from pynecone.var import BaseVar, PCList, Var
 
 
 test_vars = [
 test_vars = [
     BaseVar(name="prop1", type_=int),
     BaseVar(name="prop1", type_=int),
@@ -207,3 +208,13 @@ def test_dict_indexing():
     # Check correct indexing.
     # Check correct indexing.
     assert str(dct["a"]) == '{dct["a"]}'
     assert str(dct["a"]) == '{dct["a"]}'
     assert str(dct["asdf"]) == '{dct["asdf"]}'
     assert str(dct["asdf"]) == '{dct["asdf"]}'
+
+
+def test_pickleable_pc_list():
+    """Test that PCList is pickleable."""
+    pc_list = PCList(
+        original_list=[1, 2, 3], reassign_field=lambda x: x, field_name="random"
+    )
+
+    pickled_list = cloudpickle.dumps(pc_list)
+    assert cloudpickle.loads(pickled_list) == pc_list