From 9ecadcc6463ecdfa11ab0215ccc7f2874144dd69 Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Tue, 13 Dec 2022 11:31:57 -0800 Subject: [PATCH] Improve prop error messages (#84) * Add better error messages for component props --- README.md | 2 +- pynecone/components/component.py | 32 ++++++-- pynecone/pc.py | 26 +++++- pynecone/utils.py | 9 +++ tests/components/test_component.py | 125 +++++++++++++++++++++++++++-- 5 files changed, 176 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index deb16959f..787c2d6a9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Pynecone Logo -**Build performant, customizable web apps in minutes just using Python.** +**Build performant, customizable web apps in pure Python.** [![PyPI version](https://badge.fury.io/py/pynecone-io.svg)](https://badge.fury.io/py/pynecone-io) ![tests](https://github.com/pynecone-io/pynecone/actions/workflows/build.yml/badge.svg) diff --git a/pynecone/components/component.py b/pynecone/components/component.py index 839fa254e..82476efa6 100644 --- a/pynecone/components/component.py +++ b/pynecone/components/component.py @@ -71,6 +71,9 @@ class Component(Base, ABC): Args: *args: Args to initialize the component. **kwargs: Kwargs to initialize the component. + + Raises: + TypeError: If an invalid prop is passed. """ # Get the component fields, triggers, and props. fields = self.get_fields() @@ -97,13 +100,25 @@ class Component(Base, ABC): # Check whether the key is a component prop. if utils._issubclass(field_type, Var): - # Convert any constants into vars and make sure the types match. - kwargs[key] = Var.create(value) - passed_type = kwargs[key].type_ - expected_type = fields[key].outer_type_.__args__[0] - assert utils._issubclass( - passed_type, expected_type - ), f"Invalid var passed for {key}, expected {expected_type}, got {passed_type}." + try: + # Try to create a var from the value. + kwargs[key] = Var.create(value) + + # Check that the var type is not None. + if kwargs[key] is None: + raise TypeError + + # Get the passed type and the var type. + passed_type = kwargs[key].type_ + expected_type = fields[key].outer_type_.__args__[0] + except TypeError: + # If it is not a valid var, check the base types. + passed_type = type(value) + expected_type = fields[key].outer_type_ + if not utils._issubclass(passed_type, expected_type): + raise TypeError( + f"Invalid var passed for prop {key}, expected type {expected_type}, got value {value} of type {passed_type}." + ) # Check if the key is an event trigger. if key in triggers: @@ -241,7 +256,7 @@ class Component(Base, ABC): Returns: The unique fields. """ - return set(cls.get_fields()) - set(Component.get_fields()) - {"library", "tag"} + return set(cls.get_fields()) - set(Component.get_fields()) @classmethod def create(cls, *children, **props) -> Component: @@ -262,6 +277,7 @@ class Component(Base, ABC): # Validate all the children. for child in children: + # Make sure the child is a valid type. if not utils._isinstance(child, ComponentChild): raise TypeError( "Children of Pynecone components must be other components, " diff --git a/pynecone/pc.py b/pynecone/pc.py index 28b46f2b6..6e3652e8c 100644 --- a/pynecone/pc.py +++ b/pynecone/pc.py @@ -20,9 +20,21 @@ def version(): @cli.command() def init(): - """Initialize a new Pynecone app.""" + """Initialize a new Pynecone app. + + Raises: + Exit: If the app directory is invalid. + """ app_name = utils.get_default_app_name() - with utils.console.status(f"[bold]Initializing {app_name}") as status: + + # Make sure they don't name the app "pynecone". + if app_name == constants.MODULE_NAME: + utils.console.print( + f"[red]The app directory cannot be named [bold]{constants.MODULE_NAME}." + ) + raise typer.Exit() + + with utils.console.status(f"[bold]Initializing {app_name}"): # Only create the app directory if it doesn't exist. if not os.path.exists(constants.CONFIG_FILE): # Create a configuration file. @@ -63,7 +75,17 @@ def run( env: The environment to run the app in. frontend: Whether to run the frontend. backend: Whether to run the backend. + + Raises: + Exit: If the app is not initialized. """ + # Check that the app is initialized. + if not utils.is_initialized(): + utils.console.print( + f"[red]The app is not initialized. Run [bold]pc init[/bold] first." + ) + raise typer.Exit() + utils.console.rule("[bold]Starting Pynecone App") app = utils.get_app() diff --git a/pynecone/utils.py b/pynecone/utils.py index 1d818839a..33f7be24c 100644 --- a/pynecone/utils.py +++ b/pynecone/utils.py @@ -321,6 +321,15 @@ def install_dependencies(): subprocess.call([get_bun_path(), "install"], cwd=constants.WEB_DIR, stdout=PIPE) +def is_initialized() -> bool: + """Check whether the app is initialized. + + Returns: + Whether the app is initialized in the current directory. + """ + return os.path.exists(constants.CONFIG_FILE) and os.path.exists(constants.WEB_DIR) + + def export_app(app): """Zip up the app for deployment. diff --git a/tests/components/test_component.py b/tests/components/test_component.py index 86d5e580f..3a6c6f27b 100644 --- a/tests/components/test_component.py +++ b/tests/components/test_component.py @@ -1,10 +1,20 @@ -from typing import Type +from typing import List, Set, Type import pytest from pynecone.components.component import Component, ImportDict -from pynecone.event import EventHandler +from pynecone.event import EVENT_TRIGGERS, EventHandler +from pynecone.state import State from pynecone.style import Style +from pynecone.var import Var + + +@pytest.fixture +def TestState(): + class TestState(State): + num: int + + return TestState @pytest.fixture @@ -16,6 +26,13 @@ def component1() -> Type[Component]: """ class TestComponent1(Component): + + # A test string prop. + text: Var[str] + + # A test number prop. + number: Var[int] + def _get_imports(self) -> ImportDict: return {"react": {"Component"}} @@ -34,6 +51,19 @@ def component2() -> Type[Component]: """ class TestComponent2(Component): + + # A test list prop. + arr: Var[List[str]] + + @classmethod + def get_controlled_triggers(cls) -> Set[str]: + """Test controlled triggers. + + Returns: + Test controlled triggers. + """ + return {"on_open", "on_close"} + def _get_imports(self) -> ImportDict: return {"react-redux": {"connect"}} @@ -71,7 +101,7 @@ def on_click2() -> EventHandler: return EventHandler(fn=on_click2) -def test_set_style_attrs(component1: Type[Component]): +def test_set_style_attrs(component1): """Test that style attributes are set in the dict. Args: @@ -82,7 +112,7 @@ def test_set_style_attrs(component1: Type[Component]): assert component.style["textAlign"] == "center" -def test_create_component(component1: Type[Component]): +def test_create_component(component1): """Test that the component is created correctly. Args: @@ -96,7 +126,7 @@ def test_create_component(component1: Type[Component]): assert c.style == {"color": "white", "textAlign": "center"} -def test_add_style(component1: Type[Component], component2: Type[Component]): +def test_add_style(component1, component2): """Test adding a style to a component. Args: @@ -113,7 +143,7 @@ def test_add_style(component1: Type[Component], component2: Type[Component]): assert c2.style["color"] == "black" -def test_get_imports(component1: Type[Component], component2: Type[Component]): +def test_get_imports(component1, component2): """Test getting the imports of a component. Args: @@ -126,7 +156,7 @@ def test_get_imports(component1: Type[Component], component2: Type[Component]): assert c2.get_imports() == {"react-redux": {"connect"}, "react": {"Component"}} -def test_get_custom_code(component1: Type[Component], component2: Type[Component]): +def test_get_custom_code(component1, component2): """Test getting the custom code of a component. Args: @@ -152,3 +182,84 @@ def test_get_custom_code(component1: Type[Component], component2: Type[Component "console.log('component1')", "console.log('component2')", } + + +def test_get_props(component1, component2): + """Test that the props are set correctly. + + Args: + component1: A test component. + component2: A test component. + """ + assert component1.get_props() == {"text", "number"} + assert component2.get_props() == {"arr"} + + +@pytest.mark.parametrize( + "text,number", + [ + ("", 0), + ("test", 1), + ("hi", -13), + ], +) +def test_valid_props(component1, text: str, number: int): + """Test that we can construct a component with valid props. + + Args: + component1: A test component. + text: A test string. + number: A test number. + """ + c = component1.create(text=text, number=number) + assert c.text == text + assert c.number == number + + +@pytest.mark.parametrize( + "text,number", [("", "bad_string"), (13, 1), (None, 1), ("test", [1, 2, 3])] +) +def test_invalid_prop_type(component1, text: str, number: int): + """Test that an invalid prop type raises an error. + + Args: + component1: A test component. + text: A test string. + number: A test number. + """ + # Check that + with pytest.raises(TypeError): + component1.create(text=text, number=number) + + +def test_var_props(component1, TestState): + """Test that we can set a Var prop. + + Args: + component1: A test component. + TestState: A test state. + """ + c1 = component1.create(text="hello", number=TestState.num) + assert c1.number == TestState.num + + +def test_get_controlled_triggers(component1, component2): + """Test that we can get the controlled triggers of a component. + + Args: + component1: A test component. + component2: A test component. + """ + assert component1.get_controlled_triggers() == set() + assert component2.get_controlled_triggers() == {"on_open", "on_close"} + + +def test_get_triggers(component1, component2): + """Test that we can get the triggers of a component. + + Args: + component1: A test component. + component2: A test component. + """ + assert component1.get_triggers() == EVENT_TRIGGERS + assert component2.get_triggers() == {"on_open", "on_close"} | EVENT_TRIGGERS