Throw error for unannotated computed vars (#941)

This commit is contained in:
Elijah Ahianyo 2023-05-06 19:26:49 +00:00 committed by GitHub
parent d32996c91f
commit 9ea1a64d22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 150 additions and 17 deletions

View File

@ -273,26 +273,32 @@ class Var(ABC):
The var attribute.
Raises:
Exception: If the attribute is not found.
AttributeError: If the var is wrongly annotated or can't find attribute.
TypeError: If an annotation to the var isn't provided.
"""
try:
return super().__getattribute__(name)
except Exception as e:
# Check if the attribute is one of the class fields.
if (
not name.startswith("_")
and hasattr(self.type_, "__fields__")
and name in self.type_.__fields__
):
type_ = self.type_.__fields__[name].outer_type_
if isinstance(type_, ModelField):
type_ = type_.type_
return BaseVar(
name=f"{self.name}.{name}",
type_=type_,
state=self.state,
)
raise e
if not name.startswith("_"):
if self.type_ == Any:
raise TypeError(
f"You must provide an annotation for the state var `{self.full_name}`. Annotation cannot be `{self.type_}`"
) from None
if hasattr(self.type_, "__fields__") and name in self.type_.__fields__:
type_ = self.type_.__fields__[name].outer_type_
if isinstance(type_, ModelField):
type_ = type_.type_
return BaseVar(
name=f"{self.name}.{name}",
type_=type_,
state=self.state,
)
raise AttributeError(
f"The State var `{self.full_name}` has no attribute '{name}' or may have been annotated "
f"wrongly.\n"
f"original message: {e.args[0]}"
) from e
def operation(
self,
@ -792,7 +798,7 @@ class BaseVar(Var, Base):
return setter
class ComputedVar(property, Var):
class ComputedVar(Var, property):
"""A field with computed getters."""
@property

View File

@ -1,10 +1,12 @@
import typing
from typing import Dict, List
import cloudpickle
import pytest
from pynecone.base import Base
from pynecone.var import BaseVar, ImportVar, PCDict, PCList, Var
from pynecone.state import State
from pynecone.var import BaseVar, ComputedVar, ImportVar, PCDict, PCList, Var
test_vars = [
BaseVar(name="prop1", type_=int),
@ -26,6 +28,69 @@ def TestObj():
return TestObj
@pytest.fixture
def ParentState(TestObj):
class ParentState(State):
foo: int
bar: int
@ComputedVar
def var_without_annotation(self):
return TestObj
return ParentState
@pytest.fixture
def ChildState(ParentState, TestObj):
class ChildState(ParentState):
@ComputedVar
def var_without_annotation(self):
return TestObj
return ChildState
@pytest.fixture
def GrandChildState(ChildState, TestObj):
class GrandChildState(ChildState):
@ComputedVar
def var_without_annotation(self):
return TestObj
return GrandChildState
@pytest.fixture
def StateWithAnyVar(TestObj):
class StateWithAnyVar(State):
@ComputedVar
def var_without_annotation(self) -> typing.Any:
return TestObj
return StateWithAnyVar
@pytest.fixture
def StateWithCorrectVarAnnotation():
class StateWithCorrectVarAnnotation(State):
@ComputedVar
def var_with_annotation(self) -> str:
return "Correct annotation"
return StateWithCorrectVarAnnotation
@pytest.fixture
def StateWithWrongVarAnnotation(TestObj):
class StateWithWrongVarAnnotation(State):
@ComputedVar
def var_with_annotation(self) -> str:
return TestObj
return StateWithWrongVarAnnotation
@pytest.mark.parametrize(
"prop,expected",
zip(
@ -229,6 +294,68 @@ def test_dict_indexing():
assert str(dct["asdf"]) == '{dct["asdf"]}'
@pytest.mark.parametrize(
"fixture,full_name",
[
("ParentState", "parent_state.var_without_annotation"),
("ChildState", "parent_state.child_state.var_without_annotation"),
(
"GrandChildState",
"parent_state.child_state.grand_child_state.var_without_annotation",
),
("StateWithAnyVar", "state_with_any_var.var_without_annotation"),
],
)
def test_computed_var_without_annotation_error(request, fixture, full_name):
"""Test that a type error is thrown when an attribute of a computed var is
accessed without annotating the computed var.
Args:
request: Fixture Request.
fixture: The state fixture.
full_name: The full name of the state var.
"""
with pytest.raises(TypeError) as err:
state = request.getfixturevalue(fixture)
state.var_without_annotation.foo
assert (
err.value.args[0]
== f"You must provide an annotation for the state var `{full_name}`. Annotation cannot be `typing.Any`"
)
@pytest.mark.parametrize(
"fixture,full_name",
[
(
"StateWithCorrectVarAnnotation",
"state_with_correct_var_annotation.var_with_annotation",
),
(
"StateWithWrongVarAnnotation",
"state_with_wrong_var_annotation.var_with_annotation",
),
],
)
def test_computed_var_with_annotation_error(request, fixture, full_name):
"""Test that an Attribute error is thrown when a non-existent attribute of an annotated computed var is
accessed or when the wrong annotation is provided to a computed var.
Args:
request: Fixture Request.
fixture: The state fixture.
full_name: The full name of the state var.
"""
with pytest.raises(AttributeError) as err:
state = request.getfixturevalue(fixture)
state.var_with_annotation.foo
assert (
err.value.args[0]
== f"The State var `{full_name}` has no attribute 'foo' or may have been annotated wrongly.\n"
f"original message: 'ComputedVar' object has no attribute 'foo'"
)
def test_pickleable_pc_list():
"""Test that PCList is pickleable."""
pc_list = PCList(