Allow conditional props (#359)

This commit is contained in:
Elijah Ahianyo 2023-02-02 08:22:44 +00:00 committed by GitHub
parent ea28f336da
commit 00479362df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 189 additions and 18 deletions

View File

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

View File

@ -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.propcond import PropCond
from pynecone.var import Var
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)

View File

@ -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
pynecone/propcond.py Normal file
View File

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

View File

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

View File

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

View File

@ -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
tests/conftest.py Normal file
View File

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

View File

@ -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") app.add_page(index_page, route=route)
assert set(app.pages.keys()) == {"test/nested"} 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
tests/test_propcond.py Normal file
View File

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

View File

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