Make PCList pickleable (#500)

This commit is contained in:
Elijah Ahianyo 2023-02-10 20:25:58 +00:00 committed by GitHub
parent 8d9c75824c
commit 69a9c95d73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 155 additions and 132 deletions

14
poetry.lock generated
View File

@ -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"

View File

@ -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. if isinstance(field_value, list):
# We will uncomment this code once this is fixed. for index in range(len(field_value)):
# if isinstance(field_value, list): field_value[index] = _convert_mutable_datatypes(
# for index in range(len(field_value)): field_value[index], reassign_field, field_name
# field_value[index] = _convert_mutable_datatypes( )
# field_value[index], reassign_field, field_name
# )
# field_value = PCList( field_value = PCList(
# field_value, reassign_field=reassign_field, field_name=field_name 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():

View File

@ -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.

View File

@ -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"

View File

@ -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.asyncio
# @pytest.mark.parametrize( @pytest.mark.parametrize(
# "event_tuples", "event_tuples",
# [ [
# pytest.param( pytest.param(
# [ [
# ( (
# "test_state.make_friend", "test_state.make_friend",
# {"test_state": {"plain_friends": ["Tommy", "another-fd"]}}, {"test_state": {"plain_friends": ["Tommy", "another-fd"]}},
# ), ),
# ( (
# "test_state.change_first_friend", "test_state.change_first_friend",
# {"test_state": {"plain_friends": ["Jenny", "another-fd"]}}, {"test_state": {"plain_friends": ["Jenny", "another-fd"]}},
# ), ),
# ], ],
# id="append then __setitem__", id="append then __setitem__",
# ), ),
# pytest.param( pytest.param(
# [ [
# ( (
# "test_state.unfriend_first_friend", "test_state.unfriend_first_friend",
# {"test_state": {"plain_friends": []}}, {"test_state": {"plain_friends": []}},
# ), ),
# ( (
# "test_state.make_friend", "test_state.make_friend",
# {"test_state": {"plain_friends": ["another-fd"]}}, {"test_state": {"plain_friends": ["another-fd"]}},
# ), ),
# ], ],
# id="delitem then append", id="delitem then append",
# ), ),
# pytest.param( pytest.param(
# [ [
# ( (
# "test_state.make_friends_with_colleagues", "test_state.make_friends_with_colleagues",
# {"test_state": {"plain_friends": ["Tommy", "Peter", "Jimmy"]}}, {"test_state": {"plain_friends": ["Tommy", "Peter", "Jimmy"]}},
# ), ),
# ( (
# "test_state.remove_tommy", "test_state.remove_tommy",
# {"test_state": {"plain_friends": ["Peter", "Jimmy"]}}, {"test_state": {"plain_friends": ["Peter", "Jimmy"]}},
# ), ),
# ( (
# "test_state.remove_last_friend", "test_state.remove_last_friend",
# {"test_state": {"plain_friends": ["Peter"]}}, {"test_state": {"plain_friends": ["Peter"]}},
# ), ),
# ( (
# "test_state.unfriend_all_friends", "test_state.unfriend_all_friends",
# {"test_state": {"plain_friends": []}}, {"test_state": {"plain_friends": []}},
# ), ),
# ], ],
# id="extend, remove, pop, clear", id="extend, remove, pop, clear",
# ), ),
# pytest.param( pytest.param(
# [ [
# ( (
# "test_state.add_jimmy_to_second_group", "test_state.add_jimmy_to_second_group",
# { {
# "test_state": { "test_state": {
# "friends_in_nested_list": [["Tommy"], ["Jenny", "Jimmy"]] "friends_in_nested_list": [["Tommy"], ["Jenny", "Jimmy"]]
# } }
# }, },
# ), ),
# ( (
# "test_state.remove_first_person_from_first_group", "test_state.remove_first_person_from_first_group",
# { {
# "test_state": { "test_state": {
# "friends_in_nested_list": [[], ["Jenny", "Jimmy"]] "friends_in_nested_list": [[], ["Jenny", "Jimmy"]]
# } }
# }, },
# ), ),
# ( (
# "test_state.remove_first_group", "test_state.remove_first_group",
# {"test_state": {"friends_in_nested_list": [["Jenny", "Jimmy"]]}}, {"test_state": {"friends_in_nested_list": [["Jenny", "Jimmy"]]}},
# ), ),
# ], ],
# id="nested list", id="nested list",
# ), ),
# pytest.param( pytest.param(
# [ [
# ( (
# "test_state.add_jimmy_to_tommy_friends", "test_state.add_jimmy_to_tommy_friends",
# {"test_state": {"friends_in_dict": {"Tommy": ["Jenny", "Jimmy"]}}}, {"test_state": {"friends_in_dict": {"Tommy": ["Jenny", "Jimmy"]}}},
# ), ),
# ( (
# "test_state.remove_jenny_from_tommy", "test_state.remove_jenny_from_tommy",
# {"test_state": {"friends_in_dict": {"Tommy": ["Jimmy"]}}}, {"test_state": {"friends_in_dict": {"Tommy": ["Jimmy"]}}},
# ), ),
# ( (
# "test_state.tommy_has_no_fds", "test_state.tommy_has_no_fds",
# {"test_state": {"friends_in_dict": {"Tommy": []}}}, {"test_state": {"friends_in_dict": {"Tommy": []}}},
# ), ),
# ], ],
# id="list in dict", id="list in dict",
# ), ),
# ], ],
# ) )
# async def test_list_mutation_detection__plain_list( async def test_list_mutation_detection__plain_list(
# event_tuples: List[Tuple[str, List[str]]], list_mutation_state: State event_tuples: List[Tuple[str, List[str]]], list_mutation_state: State
# ): ):
# """Test list mutation detection """Test list mutation detection
# when reassignment is not explicitly included in the logic. when reassignment is not explicitly included in the logic.
# Args: Args:
# event_tuples: From parametrization. event_tuples: From parametrization.
# list_mutation_state: A state with list mutation features. list_mutation_state: A state with list mutation features.
# """ """
# for event_name, expected_delta in event_tuples: for event_name, expected_delta in event_tuples:
# result = await list_mutation_state.process( result = await list_mutation_state.process(
# Event( Event(
# token="fake-token", token="fake-token",
# name=event_name, name=event_name,
# router_data={"pathname": "/", "query": {}}, router_data={"pathname": "/", "query": {}},
# payload={}, payload={},
# ) )
# ) )
# assert result.delta == expected_delta assert result.delta == expected_delta

View File

@ -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