Improve prop error messages (#84)
* Add better error messages for component props
This commit is contained in:
parent
d39bcc7d38
commit
9ecadcc646
@ -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.**
|
||||||
|
|
||||||
[](https://badge.fury.io/py/pynecone-io)
|
[](https://badge.fury.io/py/pynecone-io)
|
||||||

|

|
||||||
|
@ -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, "
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user