Allow conditional props (#359)
This commit is contained in:
parent
ea28f336da
commit
00479362df
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
51
pynecone/propcond.py
Normal 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,
|
||||||
|
)
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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
15
tests/conftest.py
Normal 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"
|
@ -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
35
tests/test_propcond.py
Normal 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}" "}"
|
||||||
|
)
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user