Răsfoiți Sursa

Make PCList pickleable (#500)

Elijah Ahianyo 2 ani în urmă
părinte
comite
69a9c95d73
6 a modificat fișierele cu 158 adăugiri și 135 ștergeri
  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\""}
 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]]
 name = "colorama"
 version = "0.4.6"
@@ -1198,4 +1210,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 lock-version = "2.0"
 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 functools
-import pickle
 import traceback
 from abc import ABC
 from typing import Any, Callable, ClassVar, Dict, List, Optional, Sequence, Set, Type
 
+import cloudpickle
 from redis import Redis
 
 from pynecone import constants, utils
 from pynecone.base import Base
 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]
 
@@ -678,7 +678,7 @@ class StateManager(Base):
             if redis_state is None:
                 self.set_state(token, self.state())
                 return self.get_state(token)
-            return pickle.loads(redis_state)
+            return cloudpickle.loads(redis_state)
 
         if token not in self.states:
             self.states[token] = self.state()
@@ -693,7 +693,7 @@ class StateManager(Base):
         """
         if self.redis is None:
             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(
@@ -712,17 +712,15 @@ def _convert_mutable_datatypes(
     Returns:
         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):
         for key, value in field_value.items():

+ 1 - 1
pynecone/var.py

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

+ 1 - 0
pyproject.toml

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

+ 118 - 117
tests/test_app.py

@@ -1,10 +1,11 @@
 import os.path
-from typing import Type
+from typing import List, Tuple, Type
 
 import pytest
 
 from pynecone.app import App, DefaultState
 from pynecone.components import Box
+from pynecone.event import Event
 from pynecone.middleware import HydrateMiddleware
 from pynecone.state import State
 from pynecone.style import Style
@@ -226,119 +227,119 @@ def list_mutation_state():
     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
 
+import cloudpickle
 import pytest
 
 from pynecone.base import Base
-from pynecone.var import BaseVar, Var
+from pynecone.var import BaseVar, PCList, Var
 
 test_vars = [
     BaseVar(name="prop1", type_=int),
@@ -207,3 +208,13 @@ def test_dict_indexing():
     # Check correct indexing.
     assert str(dct["a"]) == '{dct["a"]}'
     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