From f0346506d75369a4472fe3b1d5f6f734317fc000 Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Wed, 26 Apr 2023 19:32:51 -0700 Subject: [PATCH] Allow fstrings as component children (#890) --- pynecone/app.py | 1 + pynecone/components/base/bare.py | 6 +++++- pynecone/components/component.py | 19 +++++++++++++++++++ tests/components/base/__init__.py | 0 tests/components/base/test_bare.py | 25 +++++++++++++++++++++++++ tests/components/test_component.py | 26 +++++++++++++++++++++++++- 6 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 tests/components/base/__init__.py create mode 100644 tests/components/base/test_bare.py diff --git a/pynecone/app.py b/pynecone/app.py index b4c387b18..c0b3fe369 100644 --- a/pynecone/app.py +++ b/pynecone/app.py @@ -263,6 +263,7 @@ class App(Base): # Generate the component if it is a callable. try: component = component if isinstance(component, Component) else component() + component.set_state(self.state) except TypeError as e: message = str(e) if "BaseVar" in message or "ComputedVar" in message: diff --git a/pynecone/components/base/bare.py b/pynecone/components/base/bare.py index abf5ffe88..ce26c78cf 100644 --- a/pynecone/components/base/bare.py +++ b/pynecone/components/base/bare.py @@ -27,4 +27,8 @@ class Bare(Component): return cls(contents=str(contents)) # type: ignore def _render(self) -> Tag: - return Tagless(contents=str(self.contents)) + contents = str(self.contents) + if self.state is not None: + check = f"{{{self.state.get_name()}" + contents = str(self.contents).replace(check, f"${check}") + return Tagless(contents=contents) diff --git a/pynecone/components/component.py b/pynecone/components/component.py index c153fc0f3..b93923e34 100644 --- a/pynecone/components/component.py +++ b/pynecone/components/component.py @@ -24,6 +24,9 @@ from pynecone.style import Style from pynecone.utils import format, imports, path_ops, types from pynecone.var import BaseVar, Var +if typing.TYPE_CHECKING: + from pynecone.state import State + class Component(Base, ABC): """The base class for all Pynecone components.""" @@ -34,6 +37,9 @@ class Component(Base, ABC): # The style of the component. style: Style = Style() + # The app state the component is connected to. + state: Optional[Type[State]] = None + # A mapping from event triggers to event chains. event_triggers: Dict[str, Union[EventChain, Var]] = {} @@ -393,6 +399,19 @@ class Component(Base, ABC): child.add_style(style) return self + def set_state(self, state: Type[State]): + """Set the state of the component and its children. + + Args: + state: The state to set. + """ + # Set the state of the component. + self.state = state + + # Set the state of the children. + for child in self.children: + child.set_state(state) + def render(self) -> str: """Render the component. diff --git a/tests/components/base/__init__.py b/tests/components/base/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/components/base/test_bare.py b/tests/components/base/test_bare.py new file mode 100644 index 000000000..00e89477e --- /dev/null +++ b/tests/components/base/test_bare.py @@ -0,0 +1,25 @@ +import pytest + +from pynecone.components.base.bare import Bare +from pynecone.state import DefaultState + + +@pytest.mark.parametrize( + "contents,expected", + [ + ("hello", "hello"), + ("{}", "{}"), + ("{default_state.name}", "${default_state.name}"), + ("{state.name}", "{state.name}"), + ], +) +def test_fstrings(contents, expected): + """Test that fstrings are rendered correctly. + + Args: + contents: The contents of the component. + expected: The expected output. + """ + comp = Bare.create(contents) + comp.set_state(DefaultState) + assert str(comp) == expected diff --git a/tests/components/test_component.py b/tests/components/test_component.py index 9ef5a0ac4..1f88a6e99 100644 --- a/tests/components/test_component.py +++ b/tests/components/test_component.py @@ -6,7 +6,7 @@ import pynecone as pc from pynecone.components.component import Component, CustomComponent, custom_component from pynecone.components.layout.box import Box from pynecone.event import EVENT_ARG, EVENT_TRIGGERS, EventHandler -from pynecone.state import State +from pynecone.state import DefaultState, State from pynecone.style import Style from pynecone.utils import imports from pynecone.var import Var @@ -434,3 +434,27 @@ def test_get_hooks_nested2(component3, component4): ).get_hooks() == exp_hooks ) + + +def test_set_state(component1, component2, component3): + """Test that setting the state of a component works. + + Args: + component1: test component. + component2: another component. + component3: component with hooks defined. + """ + c2 = component2.create() + c3 = component3.create() + c1 = component1.create(c2, c3) + + # State should be None by default. + assert c1.state is None + assert c2.state is None + assert c3.state is None + + # Setting the parent state should set the child state. + c1.set_state(DefaultState) + assert c1.state == DefaultState + assert c2.state == DefaultState + assert c3.state == DefaultState