Create PCDict (#503)

This commit is contained in:
Elijah Ahianyo 2023-02-12 20:52:45 +00:00 committed by GitHub
parent 8c8b2396fc
commit 8de2163d84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 380 additions and 89 deletions

View File

@ -5,7 +5,18 @@ import asyncio
import functools import functools
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,
Union,
)
import cloudpickle import cloudpickle
from redis import Redis from redis import Redis
@ -13,7 +24,7 @@ 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, PCList, Var from pynecone.var import BaseVar, ComputedVar, PCDict, PCList, Var
Delta = Dict[str, Any] Delta = Dict[str, Any]
@ -79,7 +90,7 @@ class State(Base, ABC):
value, self._reassign_field, field.name value, self._reassign_field, field.name
) )
if utils._issubclass(field.type_, List): if utils._issubclass(field.type_, Union[List, Dict]):
setattr(self, field.name, value_in_pc_data) setattr(self, field.name, value_in_pc_data)
self.clean() self.clean()
@ -727,5 +738,7 @@ def _convert_mutable_datatypes(
field_value[key] = _convert_mutable_datatypes( field_value[key] = _convert_mutable_datatypes(
value, reassign_field, field_name value, reassign_field, field_name
) )
field_value = PCDict(
field_value, reassign_field=reassign_field, field_name=field_name
)
return field_value return field_value

View File

@ -773,72 +773,155 @@ class PCList(list):
super().__init__(original_list) super().__init__(original_list)
def append(self, *args, **kargs): def append(self, *args, **kwargs):
"""Append. """Append.
Args: Args:
args: The args passed. args: The args passed.
kargs: The kwargs passed. kwargs: The kwargs passed.
""" """
super().append(*args, **kargs) super().append(*args, **kwargs)
self._reassign_field() self._reassign_field()
def __setitem__(self, *args, **kargs): def __setitem__(self, *args, **kwargs):
"""Set item. """Set item.
Args: Args:
args: The args passed. args: The args passed.
kargs: The kwargs passed. kwargs: The kwargs passed.
""" """
super().__setitem__(*args, **kargs) super().__setitem__(*args, **kwargs)
self._reassign_field() self._reassign_field()
def __delitem__(self, *args, **kargs): def __delitem__(self, *args, **kwargs):
"""Delete item. """Delete item.
Args: Args:
args: The args passed. args: The args passed.
kargs: The kwargs passed. kwargs: The kwargs passed.
""" """
super().__delitem__(*args, **kargs) super().__delitem__(*args, **kwargs)
self._reassign_field() self._reassign_field()
def clear(self, *args, **kargs): def clear(self, *args, **kwargs):
"""Remove all item from the list. """Remove all item from the list.
Args: Args:
args: The args passed. args: The args passed.
kargs: The kwargs passed. kwargs: The kwargs passed.
""" """
super().clear(*args, **kargs) super().clear(*args, **kwargs)
self._reassign_field() self._reassign_field()
def extend(self, *args, **kargs): def extend(self, *args, **kwargs):
"""Add all item of a list to the end of the list. """Add all item of a list to the end of the list.
Args: Args:
args: The args passed. args: The args passed.
kargs: The kwargs passed. kwargs: The kwargs passed.
""" """
super().extend(*args, **kargs) super().extend(*args, **kwargs)
self._reassign_field() if hasattr(self, "_reassign_field") else None self._reassign_field() if hasattr(self, "_reassign_field") else None
def pop(self, *args, **kargs): def pop(self, *args, **kwargs):
"""Remove an element. """Remove an element.
Args: Args:
args: The args passed. args: The args passed.
kargs: The kwargs passed. kwargs: The kwargs passed.
""" """
super().pop(*args, **kargs) super().pop(*args, **kwargs)
self._reassign_field() self._reassign_field()
def remove(self, *args, **kargs): def remove(self, *args, **kwargs):
"""Remove an element. """Remove an element.
Args: Args:
args: The args passed. args: The args passed.
kargs: The kwargs passed. kwargs: The kwargs passed.
""" """
super().remove(*args, **kargs) super().remove(*args, **kwargs)
self._reassign_field()
class PCDict(dict):
"""A custom dict that pynecone can detect its mutation."""
def __init__(
self,
original_dict: Dict,
reassign_field: Callable = lambda _field_name: None,
field_name: str = "",
):
"""Initialize PCDict.
Args:
original_dict: The original dict
reassign_field:
The method in the parent state to reassign the field.
Default to be a no-op function
field_name: the name of field in the parent state
"""
super().__init__(original_dict)
self._reassign_field = lambda: reassign_field(field_name)
def clear(self):
"""Remove all item from the list."""
super().clear()
self._reassign_field()
def setdefault(self, *args, **kwargs):
"""set default.
Args:
args: The args passed.
kwargs: The kwargs passed.
"""
super().setdefault(*args, **kwargs)
self._reassign_field()
def popitem(self):
"""Pop last item."""
super().popitem()
self._reassign_field()
def pop(self, k, d=None):
"""Remove an element.
Args:
k: The args passed.
d: The kwargs passed.
"""
super().pop(k, d)
self._reassign_field()
def update(self, *args, **kwargs):
"""update dict.
Args:
args: The args passed.
kwargs: The kwargs passed.
"""
super().update(*args, **kwargs)
self._reassign_field()
def __setitem__(self, *args, **kwargs):
"""set item.
Args:
args: The args passed.
kwargs: The kwargs passed.
"""
super().__setitem__(*args, **kwargs)
self._reassign_field() if hasattr(self, "_reassign_field") else None
def __delitem__(self, *args, **kwargs):
"""delete item.
Args:
args: The args passed.
kwargs: The kwargs passed.
"""
super().__delitem__(*args, **kwargs)
self._reassign_field() self._reassign_field()

View File

@ -4,6 +4,8 @@ from typing import Generator
import pytest import pytest
from pynecone.state import State
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def windows_platform() -> Generator: def windows_platform() -> Generator:
@ -13,3 +15,122 @@ def windows_platform() -> Generator:
whether system is windows. whether system is windows.
""" """
yield platform.system() == "Windows" yield platform.system() == "Windows"
@pytest.fixture
def list_mutation_state():
"""A fixture to create a state with list mutation features.
Returns:
A state with list mutation features.
"""
class TestState(State):
"""The test state."""
# plain list
plain_friends = ["Tommy"]
def make_friend(self):
self.plain_friends.append("another-fd")
def change_first_friend(self):
self.plain_friends[0] = "Jenny"
def unfriend_all_friends(self):
self.plain_friends.clear()
def unfriend_first_friend(self):
del self.plain_friends[0]
def remove_last_friend(self):
self.plain_friends.pop()
def make_friends_with_colleagues(self):
colleagues = ["Peter", "Jimmy"]
self.plain_friends.extend(colleagues)
def remove_tommy(self):
self.plain_friends.remove("Tommy")
# list in dict
friends_in_dict = {"Tommy": ["Jenny"]}
def remove_jenny_from_tommy(self):
self.friends_in_dict["Tommy"].remove("Jenny")
def add_jimmy_to_tommy_friends(self):
self.friends_in_dict["Tommy"].append("Jimmy")
def tommy_has_no_fds(self):
self.friends_in_dict["Tommy"].clear()
# nested list
friends_in_nested_list = [["Tommy"], ["Jenny"]]
def remove_first_group(self):
self.friends_in_nested_list.pop(0)
def remove_first_person_from_first_group(self):
self.friends_in_nested_list[0].pop(0)
def add_jimmy_to_second_group(self):
self.friends_in_nested_list[1].append("Jimmy")
return TestState()
@pytest.fixture
def dict_mutation_state():
"""A fixture to create a state with dict mutation features.
Returns:
A state with dict mutation features.
"""
class TestState(State):
"""The test state."""
# plain dict
details = {"name": "Tommy"}
def add_age(self):
self.details.update({"age": 20}) # type: ignore
def change_name(self):
self.details["name"] = "Jenny"
def remove_last_detail(self):
self.details.popitem()
def clear_details(self):
self.details.clear()
def remove_name(self):
del self.details["name"]
def pop_out_age(self):
self.details.pop("age")
# dict in list
address = [{"home": "home address"}, {"work": "work address"}]
def remove_home_address(self):
self.address[0].pop("home")
def add_street_to_home_address(self):
self.address[0]["street"] = "street address"
# nested dict
friend_in_nested_dict = {"name": "Nikhil", "friend": {"name": "Alek"}}
def change_friend_name(self):
self.friend_in_nested_dict["friend"]["name"] = "Tommy"
def remove_friend(self):
self.friend_in_nested_dict.pop("friend")
def add_friend_age(self):
self.friend_in_nested_dict["friend"]["age"] = 30
return TestState()

View File

@ -164,69 +164,6 @@ def test_set_and_get_state(TestState: Type[State]):
assert state2.var == 2 # type: ignore assert state2.var == 2 # type: ignore
@pytest.fixture
def list_mutation_state():
"""A fixture to create a state with list mutation features.
Returns:
A state with list mutation features.
"""
class TestState(State):
"""The test state."""
# plain list
plain_friends = ["Tommy"]
def make_friend(self):
self.plain_friends.append("another-fd")
def change_first_friend(self):
self.plain_friends[0] = "Jenny"
def unfriend_all_friends(self):
self.plain_friends.clear()
def unfriend_first_friend(self):
del self.plain_friends[0]
def remove_last_friend(self):
self.plain_friends.pop()
def make_friends_with_colleagues(self):
colleagues = ["Peter", "Jimmy"]
self.plain_friends.extend(colleagues)
def remove_tommy(self):
self.plain_friends.remove("Tommy")
# list in dict
friends_in_dict = {"Tommy": ["Jenny"]}
def remove_jenny_from_tommy(self):
self.friends_in_dict["Tommy"].remove("Jenny")
def add_jimmy_to_tommy_friends(self):
self.friends_in_dict["Tommy"].append("Jimmy")
def tommy_has_no_fds(self):
self.friends_in_dict["Tommy"].clear()
# nested list
friends_in_nested_list = [["Tommy"], ["Jenny"]]
def remove_first_group(self):
self.friends_in_nested_list.pop(0)
def remove_first_person_from_first_group(self):
self.friends_in_nested_list[0].pop(0)
def add_jimmy_to_second_group(self):
self.friends_in_nested_list[1].append("Jimmy")
return TestState()
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"event_tuples", "event_tuples",
@ -343,3 +280,130 @@ async def test_list_mutation_detection__plain_list(
) )
assert result.delta == expected_delta assert result.delta == expected_delta
@pytest.mark.asyncio
@pytest.mark.parametrize(
"event_tuples",
[
pytest.param(
[
(
"test_state.add_age",
{"test_state": {"details": {"name": "Tommy", "age": 20}}},
),
(
"test_state.change_name",
{"test_state": {"details": {"name": "Jenny", "age": 20}}},
),
(
"test_state.remove_last_detail",
{"test_state": {"details": {"name": "Jenny"}}},
),
],
id="update then __setitem__",
),
pytest.param(
[
(
"test_state.clear_details",
{"test_state": {"details": {}}},
),
(
"test_state.add_age",
{"test_state": {"details": {"age": 20}}},
),
],
id="delitem then update",
),
pytest.param(
[
(
"test_state.add_age",
{"test_state": {"details": {"name": "Tommy", "age": 20}}},
),
(
"test_state.remove_name",
{"test_state": {"details": {"age": 20}}},
),
(
"test_state.pop_out_age",
{"test_state": {"details": {}}},
),
],
id="add, remove, pop",
),
pytest.param(
[
(
"test_state.remove_home_address",
{"test_state": {"address": [{}, {"work": "work address"}]}},
),
(
"test_state.add_street_to_home_address",
{
"test_state": {
"address": [
{"street": "street address"},
{"work": "work address"},
]
}
},
),
],
id="dict in list",
),
pytest.param(
[
(
"test_state.change_friend_name",
{
"test_state": {
"friend_in_nested_dict": {
"name": "Nikhil",
"friend": {"name": "Tommy"},
}
}
},
),
(
"test_state.add_friend_age",
{
"test_state": {
"friend_in_nested_dict": {
"name": "Nikhil",
"friend": {"name": "Tommy", "age": 30},
}
}
},
),
(
"test_state.remove_friend",
{"test_state": {"friend_in_nested_dict": {"name": "Nikhil"}}},
),
],
id="nested dict",
),
],
)
async def test_dict_mutation_detection__plain_list(
event_tuples: List[Tuple[str, List[str]]], dict_mutation_state: State
):
"""Test dict mutation detection
when reassignment is not explicitly included in the logic.
Args:
event_tuples: From parametrization.
dict_mutation_state: A state with dict mutation features.
"""
for event_name, expected_delta in event_tuples:
result = await dict_mutation_state.process(
Event(
token="fake-token",
name=event_name,
router_data={"pathname": "/", "query": {}},
payload={},
)
)
assert result.delta == expected_delta

View File

@ -4,7 +4,7 @@ import cloudpickle
import pytest import pytest
from pynecone.base import Base from pynecone.base import Base
from pynecone.var import BaseVar, PCList, Var from pynecone.var import BaseVar, PCDict, PCList, Var
test_vars = [ test_vars = [
BaseVar(name="prop1", type_=int), BaseVar(name="prop1", type_=int),
@ -218,3 +218,13 @@ def test_pickleable_pc_list():
pickled_list = cloudpickle.dumps(pc_list) pickled_list = cloudpickle.dumps(pc_list)
assert cloudpickle.loads(pickled_list) == pc_list assert cloudpickle.loads(pickled_list) == pc_list
def test_pickleable_pc_dict():
"""Test that PCDict is pickleable."""
pc_dict = PCDict(
original_dict={1: 2, 3: 4}, reassign_field=lambda x: x, field_name="random"
)
pickled_dict = cloudpickle.dumps(pc_dict)
assert cloudpickle.loads(pickled_dict) == pc_dict