diff --git a/pynecone/var.py b/pynecone/var.py index c6b487437..1cb7f8d84 100644 --- a/pynecone/var.py +++ b/pynecone/var.py @@ -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 diff --git a/tests/test_var.py b/tests/test_var.py index 4d2609dfc..0fddba458 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -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(