diff --git a/poetry.lock b/poetry.lock index 901ae569a..7471e7e47 100644 --- a/poetry.lock +++ b/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" diff --git a/pynecone/state.py b/pynecone/state.py index 75db05045..c9629365e 100644 --- a/pynecone/state.py +++ b/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 - # ) + 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 - # ) + 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(): diff --git a/pynecone/var.py b/pynecone/var.py index 754e77908..27ba9e991 100644 --- a/pynecone/var.py +++ b/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. diff --git a/pyproject.toml b/pyproject.toml index 56a82f091..1a52f3ae1 100644 --- a/pyproject.toml +++ b/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" diff --git a/tests/test_app.py b/tests/test_app.py index 3c50e35cc..169cead84 100644 --- a/tests/test_app.py +++ b/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. +@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={}, -# ) -# ) + 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 + assert result.delta == expected_delta diff --git a/tests/test_var.py b/tests/test_var.py index e00726713..925907c48 100644 --- a/tests/test_var.py +++ b/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