Browse Source

Allow conditional props (#359)

Elijah Ahianyo 2 năm trước cách đây
mục cha
commit
00479362df

+ 1 - 0
pynecone/__init__.py

@@ -16,3 +16,4 @@ from .model import Model, session
 from .state import ComputedVar as var
 from .state import State
 from .style import toggle_color_mode
+from .var import Var

+ 18 - 2
pynecone/components/__init__.py

@@ -1,8 +1,7 @@
 """Import all the components."""
 
 from pynecone import utils
-from pynecone.event import EventSpec
-from pynecone.var import Var
+from pynecone.propcond import PropCond
 
 from .component import Component
 from .datadisplay import *
@@ -25,6 +24,7 @@ locals().update(
     }
 )
 
+
 # Add responsive styles shortcuts.
 def mobile_only(*children, **props):
     """Create a component that is only visible on mobile.
@@ -89,3 +89,19 @@ def mobile_and_tablet(*children, **props):
         The component.
     """
     return Box.create(*children, **props, display=["block", "block", "block", "none"])
+
+
+def cond(cond_var, c1, c2=None):
+    """Create a conditional component or Prop.
+
+    Args:
+        cond_var: The cond to determine which component to render.
+        c1: The component or prop to render if the cond_var is true.
+        c2: The component or prop to render if the cond_var is false.
+
+    Returns:
+        The conditional component.
+    """
+    if isinstance(c1, Component) and isinstance(c2, Component):
+        return Cond.create(cond_var, c1, c2)
+    return PropCond.create(cond_var, c1, c2)

+ 9 - 2
pynecone/components/tags/tag.py

@@ -12,6 +12,7 @@ from plotly.io import to_json
 
 from pynecone import utils
 from pynecone.base import Base
+from pynecone.propcond import PropCond
 from pynecone.event import EventChain
 from pynecone.var import Var
 
@@ -47,7 +48,7 @@ class Tag(Base):
 
     @staticmethod
     def format_prop(
-        prop: Union[Var, EventChain, ComponentStyle, str],
+        prop: Union[Var, EventChain, ComponentStyle, PropCond, str],
     ) -> Union[int, float, str]:
         """Format a prop.
 
@@ -71,6 +72,10 @@ class Tag(Base):
             events = ",".join([utils.format_event(event) for event in prop.events])
             prop = f"({local_args}) => Event([{events}])"
 
+        # Handle conditional props.
+        elif isinstance(prop, PropCond):
+            return str(prop)
+
         # Handle other types.
         elif isinstance(prop, str):
             if utils.is_wrapped(prop, "{"):
@@ -85,7 +90,9 @@ class Tag(Base):
             if isinstance(prop, dict):
                 # Convert any var keys to strings.
                 prop = {
-                    key: str(val) if isinstance(val, Var) else val
+                    key: str(val)
+                    if isinstance(val, Var) or isinstance(val, PropCond)
+                    else val
                     for key, val in prop.items()
                 }
 

+ 51 - 0
pynecone/propcond.py

@@ -0,0 +1,51 @@
+"""Create a Prop Condition."""
+from typing import Any
+
+from pynecone import utils
+from pynecone.base import Base
+from pynecone.var import Var
+
+
+class PropCond(Base):
+    """A conditional prop."""
+
+    # The condition to determine which prop to render.
+    cond: Var[Any]
+
+    # The prop to render if the condition is true.
+    prop1: Any
+
+    # The prop to render if the condition is false.
+    prop2: Any
+
+    @classmethod
+    def create(cls, cond: Var, prop1: Any, prop2: Any = None):
+        """Create a conditional Prop.
+
+        Args:
+            cond: The cond to determine which prop to render.
+            prop1: The prop value to render if the cond is true.
+            prop2: The prop value to render if the cond is false.
+
+        Returns:
+            The conditional Prop.
+        """
+        return cls(
+            cond=cond,
+            prop1=prop1,
+            prop2=prop2,
+        )
+
+    def __str__(self) -> str:
+        """Render the prop as a React string.
+
+        Returns:
+            The React code to render the prop.
+        """
+        assert self.cond is not None, "The condition must be set."
+        return utils.format_cond(
+            cond=self.cond.full_name,
+            true_value=self.prop1,
+            false_value=self.prop2,
+            is_prop=True,
+        )

+ 19 - 2
pynecone/utils.py

@@ -1,4 +1,5 @@
 """General utility functions."""
+
 from __future__ import annotations
 
 import contextlib
@@ -17,6 +18,7 @@ from collections import defaultdict
 from pathlib import Path
 from subprocess import DEVNULL, PIPE, STDOUT
 from types import ModuleType
+from typing import _GenericAlias  # type: ignore
 from typing import (
     TYPE_CHECKING,
     Any,
@@ -983,7 +985,11 @@ def format_route(route: str) -> str:
 
 
 def format_cond(
-    cond: str, true_value: str, false_value: str = '""', is_nested: bool = False
+    cond: str,
+    true_value: str,
+    false_value: str = '""',
+    is_nested: bool = False,
+    is_prop=False,
 ) -> str:
     """Format a conditional expression.
 
@@ -992,11 +998,22 @@ def format_cond(
         true_value: The value to return if the cond is true.
         false_value: The value to return if the cond is false.
         is_nested: Whether the cond is nested.
+        is_prop: Whether the cond is a prop
 
     Returns:
         The formatted conditional expression.
     """
-    expr = f"{cond} ? {true_value} : {false_value}"
+    if is_prop:
+        if isinstance(true_value, str):
+            true_value = wrap(true_value, "'")
+        if isinstance(false_value, str):
+            false_value = wrap(false_value, "'")
+        expr = f"{cond} ? {true_value} : {false_value}".replace("{", "").replace(
+            "}", ""
+        )
+    else:
+        expr = f"{cond} ? {true_value} : {false_value}"
+
     if not is_nested:
         expr = wrap(expr, "{")
     return expr

+ 7 - 2
tests/compiler/test_compiler.py

@@ -41,14 +41,19 @@ def test_compile_import_statement(lib: str, fields: Set[str], output: str):
         ),
     ],
 )
-def test_compile_imports(import_dict: utils.ImportDict, output: str):
+def test_compile_imports(
+    import_dict: utils.ImportDict, output: str, windows_platform: bool
+):
     """Test the compile_imports function.
 
     Args:
         import_dict: The import dictionary.
         output: The expected output.
+        windows_platform: whether system is windows.
     """
-    assert utils.compile_imports(import_dict) == output
+    assert utils.compile_imports(import_dict) == (
+        output.replace("\n", "\r\n") if windows_platform else output
+    )
 
 
 @pytest.mark.parametrize(

+ 19 - 3
tests/components/test_tag.py

@@ -1,3 +1,4 @@
+import platform
 from typing import Dict
 
 import pytest
@@ -6,6 +7,7 @@ from pynecone.components import Box
 from pynecone.components.tags import CondTag, IterTag, Tag
 from pynecone.event import EventChain, EventHandler, EventSpec
 from pynecone.var import BaseVar, Var
+from pynecone.propcond import PropCond
 
 
 def mock_event(arg):
@@ -40,6 +42,14 @@ def mock_event(arg):
             ),
             '{(e) => Event([E("mock_event", {arg:e.target.value})])}',
         ),
+        (
+            PropCond.create(
+                cond=BaseVar(name="random_var", type_=str),
+                prop1="true_value",
+                prop2="false_value",
+            ),
+            "{random_var ? 'true_value' : 'false_value'}",
+        ),
     ],
 )
 def test_format_value(prop: Var, formatted: str):
@@ -61,14 +71,17 @@ def test_format_value(prop: Var, formatted: str):
         ({"key": True, "key2": "value2"}, 'key={true}\nkey2="value2"'),
     ],
 )
-def test_format_props(props: Dict[str, Var], formatted: str):
+def test_format_props(props: Dict[str, Var], formatted: str, windows_platform: bool):
     """Test that the formatted props are correct.
 
     Args:
         props: The props to test.
         formatted: The expected formatted props.
+        windows_platform: Whether the system is windows.
     """
-    assert Tag(props=props).format_props() == formatted
+    assert Tag(props=props).format_props() == (
+        formatted.replace("\n", "\r\n") if windows_platform else formatted
+    )
 
 
 @pytest.mark.parametrize(
@@ -123,13 +136,16 @@ def test_add_props():
         ),
     ],
 )
-def test_format_tag(tag: Tag, expected: str):
+def test_format_tag(tag: Tag, expected: str, windows_platform: bool):
     """Test that the formatted tag is correct.
 
     Args:
         tag: The tag to test.
         expected: The expected formatted tag.
+        windows_platform: Whether the system is windows.
     """
+
+    expected = expected.replace("\n", "\r\n") if windows_platform else expected
     assert str(tag) == expected
 
 

+ 15 - 0
tests/conftest.py

@@ -0,0 +1,15 @@
+"""Test fixtures."""
+import platform
+from typing import Generator
+
+import pytest
+
+
+@pytest.fixture(scope="function")
+def windows_platform() -> Generator:
+    """Check if system is windows.
+
+    Yields:
+        whether system is windows.
+    """
+    yield platform.system() == "Windows"

+ 10 - 5
tests/test_app.py

@@ -1,3 +1,4 @@
+import os.path
 from typing import List, Tuple, Type
 
 import pytest
@@ -88,28 +89,32 @@ def test_add_page_default_route(app: App, index_page, about_page):
     assert set(app.pages.keys()) == {"index", "about"}
 
 
-def test_add_page_set_route(app: App, index_page):
+def test_add_page_set_route(app: App, index_page, windows_platform: bool):
     """Test adding a page to an app.
 
     Args:
         app: The app to test.
         index_page: The index page.
+        windows_platform: Whether the system is windows.
     """
+    route = "\\test" if windows_platform else "/test"
     assert app.pages == {}
-    app.add_page(index_page, route="/test")
+    app.add_page(index_page, route=route)
     assert set(app.pages.keys()) == {"test"}
 
 
-def test_add_page_set_route_nested(app: App, index_page):
+def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool):
     """Test adding a page to an app.
 
     Args:
         app: The app to test.
         index_page: The index page.
+        windows_platform: Whether the system is windows.
     """
+    route = "\\test\\nested" if windows_platform else "/test/nested"
     assert app.pages == {}
-    app.add_page(index_page, route="/test/nested")
-    assert set(app.pages.keys()) == {"test/nested"}
+    app.add_page(index_page, route=route)
+    assert set(app.pages.keys()) == {route.strip(os.path.sep)}
 
 
 def test_initialize_with_state(TestState: Type[State]):

+ 35 - 0
tests/test_propcond.py

@@ -0,0 +1,35 @@
+from typing import Any
+
+import pytest
+
+from pynecone.propcond import PropCond
+from pynecone.var import BaseVar, Var
+from pynecone.utils import wrap
+
+
+@pytest.mark.parametrize(
+    "prop1,prop2",
+    [
+        (1, 3),
+        (1, "text"),
+        ("text1", "text2"),
+    ],
+)
+def test_validate_propcond(prop1: Any, prop2: Any):
+    """Test the creation of conditional props
+
+    Args:
+        prop1: truth condition value
+        prop2: false condition value
+
+    """
+    prop_cond = PropCond.create(
+        cond=BaseVar(name="cond_state.value", type_=str), prop1=prop1, prop2=prop2
+    )
+
+    expected_prop1 = wrap(prop1, "'") if isinstance(prop1, str) else prop1
+    expected_prop2 = wrap(prop2, "'") if isinstance(prop2, str) else prop2
+
+    assert str(prop_cond) == (
+        "{cond_state.value ? " f"{expected_prop1} : " f"{expected_prop2}" "}"
+    )

+ 5 - 2
tests/test_utils.py

@@ -145,15 +145,18 @@ def test_wrap(text: str, open: str, expected: str, check_first: bool, num: int):
         ("  hello\n  world", 2, "    hello\n    world\n"),
     ],
 )
-def test_indent(text: str, indent_level: int, expected: str):
+def test_indent(text: str, indent_level: int, expected: str, windows_platform: bool):
     """Test indenting a string.
 
     Args:
         text: The text to indent.
         indent_level: The number of spaces to indent by.
         expected: The expected output string.
+        windows_platform: Whether the system is windows.
     """
-    assert utils.indent(text, indent_level) == expected
+    assert utils.indent(text, indent_level) == (
+        expected.replace("\n", "\r\n") if windows_platform else expected
+    )
 
 
 @pytest.mark.parametrize(