From bb1cf4322e8c6b2a3c72632eea75ed6eeff20951 Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Wed, 17 May 2023 23:11:41 +0000 Subject: [PATCH] Foreach support for other data structures(dict, set, tuple) (#1029) --- .../jinja/web/pages/utils.js.jinja2 | 2 +- pynecone/base.py | 2 +- pynecone/components/layout/foreach.py | 21 ++- pynecone/utils/format.py | 2 +- pynecone/utils/types.py | 2 +- pynecone/vars.py | 2 +- tests/components/layout/test_foreach.py | 177 ++++++++++++++++++ tests/test_state.py | 5 + 8 files changed, 203 insertions(+), 10 deletions(-) create mode 100644 tests/components/layout/test_foreach.py diff --git a/pynecone/.templates/jinja/web/pages/utils.js.jinja2 b/pynecone/.templates/jinja/web/pages/utils.js.jinja2 index d06d29e20..731cac49d 100644 --- a/pynecone/.templates/jinja/web/pages/utils.js.jinja2 +++ b/pynecone/.templates/jinja/web/pages/utils.js.jinja2 @@ -62,7 +62,7 @@ {# Args: #} {# component: component dictionary #} {% macro render_iterable_tag(component) %} -{ {{- component.iterable_state }}.map(({{ component.arg_name }}, {{ component.arg_index }}) => ( +{ {%- if component.iterable_type == 'dict' -%}Object.entries({{- component.iterable_state }}){%- else -%}{{- component.iterable_state }}{%- endif -%}.map(({{ component.arg_name }}, {{ component.arg_index }}) => ( {% for child in component.children %} {{ render(child) }} {% endfor %} diff --git a/pynecone/base.py b/pynecone/base.py index 034483bc3..750cd6c4f 100644 --- a/pynecone/base.py +++ b/pynecone/base.py @@ -32,7 +32,7 @@ class Base(pydantic.BaseModel): Returns: The object as a json string. """ - return self.__config__.json_dumps(self.dict()) + return self.__config__.json_dumps(self.dict(), default=list) def set(self: PcType, **kwargs) -> PcType: """Set multiple fields and return the object. diff --git a/pynecone/components/layout/foreach.py b/pynecone/components/layout/foreach.py index 9865e34dc..763680761 100644 --- a/pynecone/components/layout/foreach.py +++ b/pynecone/components/layout/foreach.py @@ -1,7 +1,7 @@ """Create a list of components from an iterable.""" from __future__ import annotations -from typing import Any, Callable, List +from typing import Any, Callable, Dict, List, Set, Tuple, Union from pynecone.components.component import Component from pynecone.components.layout.fragment import Fragment @@ -13,13 +13,15 @@ class Foreach(Component): """A component that takes in an iterable and a render function and renders a list of components.""" # The iterable to create components from. - iterable: Var[List] + iterable: Var[Union[List, Dict, Tuple, Set]] # A function from the render args to the component. render_fn: Callable = Fragment.create @classmethod - def create(cls, iterable: Var[List], render_fn: Callable, **props) -> Foreach: + def create( + cls, iterable: Var[Union[List, Dict, Tuple, Set]], render_fn: Callable, **props + ) -> Foreach: """Create a foreach component. Args: @@ -34,7 +36,11 @@ class Foreach(Component): TypeError: If the iterable is of type Any. """ try: - type_ = iterable.type_.__args__[0] + type_ = ( + iterable.type_ + if iterable.type_.mro()[0] == dict + else iterable.type_.__args__[0] + ) except Exception: type_ = Any iterable = Var.create(iterable) # type: ignore @@ -61,7 +67,11 @@ class Foreach(Component): """ tag = self._render() try: - type_ = self.iterable.type_.__args__[0] + type_ = ( + self.iterable.type_ + if self.iterable.type_.mro()[0] == dict + else self.iterable.type_.__args__[0] + ) except Exception: type_ = Any arg = BaseVar( @@ -84,4 +94,5 @@ class Foreach(Component): iterable_state=tag.iterable.full_name, arg_name=arg.name, arg_index=index_arg, + iterable_type=tag.iterable.type_.mro()[0].__name__, ) diff --git a/pynecone/utils/format.py b/pynecone/utils/format.py index 7064c1522..a48be7203 100644 --- a/pynecone/utils/format.py +++ b/pynecone/utils/format.py @@ -447,4 +447,4 @@ def json_dumps(obj: Any) -> str: Returns: A string """ - return json.dumps(obj, ensure_ascii=False) + return json.dumps(obj, ensure_ascii=False, default=list) diff --git a/pynecone/utils/types.py b/pynecone/utils/types.py index 888d85122..4833ed611 100644 --- a/pynecone/utils/types.py +++ b/pynecone/utils/types.py @@ -12,7 +12,7 @@ from pynecone.base import Base GenericType = Union[Type, _GenericAlias] # Valid state var types. -PrimitiveType = Union[int, float, bool, str, list, dict, tuple] +PrimitiveType = Union[int, float, bool, str, list, dict, set, tuple] StateVar = Union[PrimitiveType, Base, None] diff --git a/pynecone/vars.py b/pynecone/vars.py index b617c3831..c40ab9b45 100644 --- a/pynecone/vars.py +++ b/pynecone/vars.py @@ -205,7 +205,7 @@ class Var(ABC): ): if self.type_ == Any: raise TypeError( - f"Could not index into var of type Any. (If you are trying to index into a state var, add a type annotation to the var.)" + f"Could not index into var of type Any. (If you are trying to index into a state var, add the correct type annotation to the var.)" ) raise TypeError( f"Var {self.name} of type {self.type_} does not support indexing." diff --git a/tests/components/layout/test_foreach.py b/tests/components/layout/test_foreach.py new file mode 100644 index 000000000..487a0fb32 --- /dev/null +++ b/tests/components/layout/test_foreach.py @@ -0,0 +1,177 @@ +from typing import Dict, List, Set, Tuple + +import pytest + +from pynecone.components import box, foreach, text +from pynecone.components.layout import Foreach +from pynecone.state import State + + +class ForEachState(State): + """The test state.""" + + color_a: List[str] = ["red", "yellow"] + color_b: List[Dict[str, str]] = [ + { + "name": "red", + }, + {"name": "yellow"}, + ] + color_c: List[Dict[str, List[str]]] = [{"shades": ["light-red"]}] + color_d: Dict[str, str] = {"category": "primary", "name": "red"} + color_e: Dict[str, List[str]] = { + "red": ["orange", "yellow"], + "yellow": ["orange", "green"], + } + color_f: Dict[str, Dict[str, List[Dict[str, str]]]] = { + "primary": {"red": [{"shade": "dark"}]} + } + color_g: Tuple[str, str] = ( + "red", + "yellow", + ) + color_h: Set[str] = {"red", "green"} + + +def display_a(color): + return box(text(color)) + + +def display_b(color): + return box(text(color["name"])) + + +def display_c(color): + return box(text(color["shades"][0])) + + +def display_d(color): + return box(text(color[0]), text(color[1])) + + +def display_e(color): + # color is a key-value pair list similar to `dict.items()` + return box(text(color[0]), text(color[1][0])) + + +def display_f(color): + return box(text(color[0]), text(color[1]["red"][0]["shade"])) + + +def show_item(item): + return text(item[1][0]["shade"]) + + +def display_f1(color): + return box(text(foreach(color[1], show_item))) + + +def display_g(color): + return box(text(color)) + + +def display_h(color): + return box(text(color)) + + +@pytest.mark.parametrize( + "state_var, render_fn, render_dict", + [ + ( + ForEachState.color_a, + display_a, + { + "iterable_state": "for_each_state.color_a", + "arg_index": "i", + "iterable_type": "list", + }, + ), + ( + ForEachState.color_b, + display_b, + { + "iterable_state": "for_each_state.color_b", + "arg_index": "i", + "iterable_type": "list", + }, + ), + ( + ForEachState.color_c, + display_c, + { + "iterable_state": "for_each_state.color_c", + "arg_index": "i", + "iterable_type": "list", + }, + ), + ( + ForEachState.color_d, + display_d, + { + "iterable_state": "for_each_state.color_d", + "arg_index": "i", + "iterable_type": "dict", + }, + ), + ( + ForEachState.color_e, + display_e, + { + "iterable_state": "for_each_state.color_e", + "arg_index": "i", + "iterable_type": "dict", + }, + ), + ( + ForEachState.color_f, + display_f, + { + "iterable_state": "for_each_state.color_f", + "arg_index": "i", + "iterable_type": "dict", + }, + ), + ( + ForEachState.color_f, + display_f1, + { + "iterable_state": "for_each_state.color_f", + "arg_index": "i", + "iterable_type": "dict", + }, + ), + ( + ForEachState.color_g, + display_g, + { + "iterable_state": "for_each_state.color_g", + "arg_index": "i", + "iterable_type": "tuple", + }, + ), + ( + ForEachState.color_h, + display_h, + { + "iterable_state": "for_each_state.color_h", + "arg_index": "i", + "iterable_type": "set", + }, + ), + ], +) +def test_foreach_render(state_var, render_fn, render_dict): + """Test that the foreach component renders without error. + + Args: + state_var: the state var. + render_fn: The render callable + render_dict: return dict on calling `component.render` + """ + component = Foreach.create(state_var, render_fn) + + rend = component.render() + + assert rend["iterable_state"] == render_dict["iterable_state"] + assert rend["arg_index"] == render_dict["arg_index"] + assert rend["iterable_type"] == render_dict["iterable_type"] diff --git a/tests/test_state.py b/tests/test_state.py index 44d9bddc1..ae0447cf4 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -28,6 +28,7 @@ class TestState(State): num1: int num2: float = 3.14 key: str + map_key: str = "a" array: List[float] = [1, 2, 3.14] mapping: Dict[str, List[int]] = {"a": [1, 2, 3], "b": [4, 5, 6]} obj: Object = Object() @@ -190,6 +191,7 @@ def test_class_vars(test_state): "num1", "num2", "key", + "map_key", "array", "mapping", "obj", @@ -281,6 +283,9 @@ def test_class_indexing_with_vars(): prop = TestState.mapping["a"][TestState.num1] assert str(prop) == '{test_state.mapping["a"].at(test_state.num1)}' + prop = TestState.mapping[TestState.map_key] + assert str(prop) == "{test_state.mapping[test_state.map_key]}" + def test_class_attributes(): """Test that we can get class attributes."""