Throw error for unannotated computed vars (#941)
This commit is contained in:
parent
d32996c91f
commit
9ea1a64d22
@ -273,26 +273,32 @@ class Var(ABC):
|
|||||||
The var attribute.
|
The var attribute.
|
||||||
|
|
||||||
Raises:
|
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:
|
try:
|
||||||
return super().__getattribute__(name)
|
return super().__getattribute__(name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Check if the attribute is one of the class fields.
|
# Check if the attribute is one of the class fields.
|
||||||
if (
|
if not name.startswith("_"):
|
||||||
not name.startswith("_")
|
if self.type_ == Any:
|
||||||
and hasattr(self.type_, "__fields__")
|
raise TypeError(
|
||||||
and name in self.type_.__fields__
|
f"You must provide an annotation for the state var `{self.full_name}`. Annotation cannot be `{self.type_}`"
|
||||||
):
|
) from None
|
||||||
type_ = self.type_.__fields__[name].outer_type_
|
if hasattr(self.type_, "__fields__") and name in self.type_.__fields__:
|
||||||
if isinstance(type_, ModelField):
|
type_ = self.type_.__fields__[name].outer_type_
|
||||||
type_ = type_.type_
|
if isinstance(type_, ModelField):
|
||||||
return BaseVar(
|
type_ = type_.type_
|
||||||
name=f"{self.name}.{name}",
|
return BaseVar(
|
||||||
type_=type_,
|
name=f"{self.name}.{name}",
|
||||||
state=self.state,
|
type_=type_,
|
||||||
)
|
state=self.state,
|
||||||
raise e
|
)
|
||||||
|
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(
|
def operation(
|
||||||
self,
|
self,
|
||||||
@ -792,7 +798,7 @@ class BaseVar(Var, Base):
|
|||||||
return setter
|
return setter
|
||||||
|
|
||||||
|
|
||||||
class ComputedVar(property, Var):
|
class ComputedVar(Var, property):
|
||||||
"""A field with computed getters."""
|
"""A field with computed getters."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
|
import typing
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
import cloudpickle
|
import cloudpickle
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pynecone.base import Base
|
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 = [
|
test_vars = [
|
||||||
BaseVar(name="prop1", type_=int),
|
BaseVar(name="prop1", type_=int),
|
||||||
@ -26,6 +28,69 @@ def TestObj():
|
|||||||
return 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(
|
@pytest.mark.parametrize(
|
||||||
"prop,expected",
|
"prop,expected",
|
||||||
zip(
|
zip(
|
||||||
@ -229,6 +294,68 @@ def test_dict_indexing():
|
|||||||
assert str(dct["asdf"]) == '{dct["asdf"]}'
|
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():
|
def test_pickleable_pc_list():
|
||||||
"""Test that PCList is pickleable."""
|
"""Test that PCList is pickleable."""
|
||||||
pc_list = PCList(
|
pc_list = PCList(
|
||||||
|
Loading…
Reference in New Issue
Block a user