Browse Source

Allow conditional props (#359)

Elijah Ahianyo 2 years ago
parent
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 ComputedVar as var
 from .state import State
 from .state import State
 from .style import toggle_color_mode
 from .style import toggle_color_mode
+from .var import Var

+ 18 - 2
pynecone/components/__init__.py

@@ -1,8 +1,7 @@
 """Import all the components."""
 """Import all the components."""
 
 
 from pynecone import utils
 from pynecone import utils
-from pynecone.event import EventSpec
-from pynecone.var import Var
+from pynecone.propcond import PropCond
 
 
 from .component import Component
 from .component import Component
 from .datadisplay import *
 from .datadisplay import *
@@ -25,6 +24,7 @@ locals().update(
     }
     }
 )
 )
 
 
+
 # Add responsive styles shortcuts.
 # Add responsive styles shortcuts.
 def mobile_only(*children, **props):
 def mobile_only(*children, **props):
     """Create a component that is only visible on mobile.
     """Create a component that is only visible on mobile.
@@ -89,3 +89,19 @@ def mobile_and_tablet(*children, **props):
         The component.
         The component.
     """
     """
     return Box.create(*children, **props, display=["block", "block", "block", "none"])
     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 import utils
 from pynecone.base import Base
 from pynecone.base import Base
+from pynecone.propcond import PropCond
 from pynecone.event import EventChain
 from pynecone.event import EventChain
 from pynecone.var import Var
 from pynecone.var import Var
 
 
@@ -47,7 +48,7 @@ class Tag(Base):
 
 
     @staticmethod
     @staticmethod
     def format_prop(
     def format_prop(
-        prop: Union[Var, EventChain, ComponentStyle, str],
+        prop: Union[Var, EventChain, ComponentStyle, PropCond, str],
     ) -> Union[int, float, str]:
     ) -> Union[int, float, str]:
         """Format a prop.
         """Format a prop.
 
 
@@ -71,6 +72,10 @@ class Tag(Base):
             events = ",".join([utils.format_event(event) for event in prop.events])
             events = ",".join([utils.format_event(event) for event in prop.events])
             prop = f"({local_args}) => Event([{events}])"
             prop = f"({local_args}) => Event([{events}])"
 
 
+        # Handle conditional props.
+        elif isinstance(prop, PropCond):
+            return str(prop)
+
         # Handle other types.
         # Handle other types.
         elif isinstance(prop, str):
         elif isinstance(prop, str):
             if utils.is_wrapped(prop, "{"):
             if utils.is_wrapped(prop, "{"):
@@ -85,7 +90,9 @@ class Tag(Base):
             if isinstance(prop, dict):
             if isinstance(prop, dict):
                 # Convert any var keys to strings.
                 # Convert any var keys to strings.
                 prop = {
                 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()
                     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."""
 """General utility functions."""
+
 from __future__ import annotations
 from __future__ import annotations
 
 
 import contextlib
 import contextlib
@@ -17,6 +18,7 @@ from collections import defaultdict
 from pathlib import Path
 from pathlib import Path
 from subprocess import DEVNULL, PIPE, STDOUT
 from subprocess import DEVNULL, PIPE, STDOUT
 from types import ModuleType
 from types import ModuleType
+from typing import _GenericAlias  # type: ignore
 from typing import (
 from typing import (
     TYPE_CHECKING,
     TYPE_CHECKING,
     Any,
     Any,
@@ -983,7 +985,11 @@ def format_route(route: str) -> str:
 
 
 
 
 def format_cond(
 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:
 ) -> str:
     """Format a conditional expression.
     """Format a conditional expression.
 
 
@@ -992,11 +998,22 @@ def format_cond(
         true_value: The value to return if the cond is true.
         true_value: The value to return if the cond is true.
         false_value: The value to return if the cond is false.
         false_value: The value to return if the cond is false.
         is_nested: Whether the cond is nested.
         is_nested: Whether the cond is nested.
+        is_prop: Whether the cond is a prop
 
 
     Returns:
     Returns:
         The formatted conditional expression.
         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:
     if not is_nested:
         expr = wrap(expr, "{")
         expr = wrap(expr, "{")
     return 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.
     """Test the compile_imports function.
 
 
     Args:
     Args:
         import_dict: The import dictionary.
         import_dict: The import dictionary.
         output: The expected output.
         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(
 @pytest.mark.parametrize(

+ 19 - 3
tests/components/test_tag.py

@@ -1,3 +1,4 @@
+import platform
 from typing import Dict
 from typing import Dict
 
 
 import pytest
 import pytest
@@ -6,6 +7,7 @@ from pynecone.components import Box
 from pynecone.components.tags import CondTag, IterTag, Tag
 from pynecone.components.tags import CondTag, IterTag, Tag
 from pynecone.event import EventChain, EventHandler, EventSpec
 from pynecone.event import EventChain, EventHandler, EventSpec
 from pynecone.var import BaseVar, Var
 from pynecone.var import BaseVar, Var
+from pynecone.propcond import PropCond
 
 
 
 
 def mock_event(arg):
 def mock_event(arg):
@@ -40,6 +42,14 @@ def mock_event(arg):
             ),
             ),
             '{(e) => Event([E("mock_event", {arg:e.target.value})])}',
             '{(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):
 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"'),
         ({"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.
     """Test that the formatted props are correct.
 
 
     Args:
     Args:
         props: The props to test.
         props: The props to test.
         formatted: The expected formatted props.
         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(
 @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.
     """Test that the formatted tag is correct.
 
 
     Args:
     Args:
         tag: The tag to test.
         tag: The tag to test.
         expected: The expected formatted tag.
         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
     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
 from typing import List, Tuple, Type
 
 
 import pytest
 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"}
     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.
     """Test adding a page to an app.
 
 
     Args:
     Args:
         app: The app to test.
         app: The app to test.
         index_page: The index page.
         index_page: The index page.
+        windows_platform: Whether the system is windows.
     """
     """
+    route = "\\test" if windows_platform else "/test"
     assert app.pages == {}
     assert app.pages == {}
-    app.add_page(index_page, route="/test")
+    app.add_page(index_page, route=route)
     assert set(app.pages.keys()) == {"test"}
     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.
     """Test adding a page to an app.
 
 
     Args:
     Args:
         app: The app to test.
         app: The app to test.
         index_page: The index page.
         index_page: The index page.
+        windows_platform: Whether the system is windows.
     """
     """
+    route = "\\test\\nested" if windows_platform else "/test/nested"
     assert app.pages == {}
     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]):
 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"),
         ("  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.
     """Test indenting a string.
 
 
     Args:
     Args:
         text: The text to indent.
         text: The text to indent.
         indent_level: The number of spaces to indent by.
         indent_level: The number of spaces to indent by.
         expected: The expected output string.
         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(
 @pytest.mark.parametrize(