Browse Source

Improve prop error messages (#84)

* Add better error messages for component props
Nikhil Rao 2 years ago
parent
commit
9ecadcc646
5 changed files with 176 additions and 18 deletions
  1. 1 1
      README.md
  2. 24 8
      pynecone/components/component.py
  3. 24 2
      pynecone/pc.py
  4. 9 0
      pynecone/utils.py
  5. 118 7
      tests/components/test_component.py

+ 1 - 1
README.md

@@ -5,7 +5,7 @@
   <img width="600" src="docs/images/logo_white.svg#gh-dark-mode-only" alt="Pynecone Logo">
 </h1>
 
-**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)

+ 24 - 8
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, "

+ 24 - 2
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()
 

+ 9 - 0
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.
 

+ 118 - 7
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