Improve prop error messages (#84)

* Add better error messages for component props
This commit is contained in:
Nikhil Rao 2022-12-13 11:31:57 -08:00 committed by GitHub
parent d39bcc7d38
commit 9ecadcc646
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 176 additions and 18 deletions

View File

@ -5,7 +5,7 @@
<img width="600" src="docs/images/logo_white.svg#gh-dark-mode-only" alt="Pynecone Logo"> <img width="600" src="docs/images/logo_white.svg#gh-dark-mode-only" alt="Pynecone Logo">
</h1> </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) [![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) ![tests](https://github.com/pynecone-io/pynecone/actions/workflows/build.yml/badge.svg)

View File

@ -71,6 +71,9 @@ class Component(Base, ABC):
Args: Args:
*args: Args to initialize the component. *args: Args to initialize the component.
**kwargs: Kwargs 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. # Get the component fields, triggers, and props.
fields = self.get_fields() fields = self.get_fields()
@ -97,13 +100,25 @@ class Component(Base, ABC):
# Check whether the key is a component prop. # Check whether the key is a component prop.
if utils._issubclass(field_type, Var): if utils._issubclass(field_type, Var):
# Convert any constants into vars and make sure the types match. try:
kwargs[key] = Var.create(value) # Try to create a var from the value.
passed_type = kwargs[key].type_ kwargs[key] = Var.create(value)
expected_type = fields[key].outer_type_.__args__[0]
assert utils._issubclass( # Check that the var type is not None.
passed_type, expected_type if kwargs[key] is None:
), f"Invalid var passed for {key}, expected {expected_type}, got {passed_type}." 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. # Check if the key is an event trigger.
if key in triggers: if key in triggers:
@ -241,7 +256,7 @@ class Component(Base, ABC):
Returns: Returns:
The unique fields. The unique fields.
""" """
return set(cls.get_fields()) - set(Component.get_fields()) - {"library", "tag"} return set(cls.get_fields()) - set(Component.get_fields())
@classmethod @classmethod
def create(cls, *children, **props) -> Component: def create(cls, *children, **props) -> Component:
@ -262,6 +277,7 @@ class Component(Base, ABC):
# Validate all the children. # Validate all the children.
for child in children: for child in children:
# Make sure the child is a valid type.
if not utils._isinstance(child, ComponentChild): if not utils._isinstance(child, ComponentChild):
raise TypeError( raise TypeError(
"Children of Pynecone components must be other components, " "Children of Pynecone components must be other components, "

View File

@ -20,9 +20,21 @@ def version():
@cli.command() @cli.command()
def init(): 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() 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. # Only create the app directory if it doesn't exist.
if not os.path.exists(constants.CONFIG_FILE): if not os.path.exists(constants.CONFIG_FILE):
# Create a configuration file. # Create a configuration file.
@ -63,7 +75,17 @@ def run(
env: The environment to run the app in. env: The environment to run the app in.
frontend: Whether to run the frontend. frontend: Whether to run the frontend.
backend: Whether to run the backend. 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") utils.console.rule("[bold]Starting Pynecone App")
app = utils.get_app() app = utils.get_app()

View File

@ -321,6 +321,15 @@ def install_dependencies():
subprocess.call([get_bun_path(), "install"], cwd=constants.WEB_DIR, stdout=PIPE) 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): def export_app(app):
"""Zip up the app for deployment. """Zip up the app for deployment.

View File

@ -1,10 +1,20 @@
from typing import Type from typing import List, Set, Type
import pytest import pytest
from pynecone.components.component import Component, ImportDict 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.style import Style
from pynecone.var import Var
@pytest.fixture
def TestState():
class TestState(State):
num: int
return TestState
@pytest.fixture @pytest.fixture
@ -16,6 +26,13 @@ def component1() -> Type[Component]:
""" """
class TestComponent1(Component): class TestComponent1(Component):
# A test string prop.
text: Var[str]
# A test number prop.
number: Var[int]
def _get_imports(self) -> ImportDict: def _get_imports(self) -> ImportDict:
return {"react": {"Component"}} return {"react": {"Component"}}
@ -34,6 +51,19 @@ def component2() -> Type[Component]:
""" """
class TestComponent2(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: def _get_imports(self) -> ImportDict:
return {"react-redux": {"connect"}} return {"react-redux": {"connect"}}
@ -71,7 +101,7 @@ def on_click2() -> EventHandler:
return EventHandler(fn=on_click2) 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. """Test that style attributes are set in the dict.
Args: Args:
@ -82,7 +112,7 @@ def test_set_style_attrs(component1: Type[Component]):
assert component.style["textAlign"] == "center" assert component.style["textAlign"] == "center"
def test_create_component(component1: Type[Component]): def test_create_component(component1):
"""Test that the component is created correctly. """Test that the component is created correctly.
Args: Args:
@ -96,7 +126,7 @@ def test_create_component(component1: Type[Component]):
assert c.style == {"color": "white", "textAlign": "center"} 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. """Test adding a style to a component.
Args: Args:
@ -113,7 +143,7 @@ def test_add_style(component1: Type[Component], component2: Type[Component]):
assert c2.style["color"] == "black" 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. """Test getting the imports of a component.
Args: Args:
@ -126,7 +156,7 @@ def test_get_imports(component1: Type[Component], component2: Type[Component]):
assert c2.get_imports() == {"react-redux": {"connect"}, "react": {"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. """Test getting the custom code of a component.
Args: Args:
@ -152,3 +182,84 @@ def test_get_custom_code(component1: Type[Component], component2: Type[Component
"console.log('component1')", "console.log('component1')",
"console.log('component2')", "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