Browse Source

Throw error for unannotated computed vars (#941)

Elijah Ahianyo 2 năm trước cách đây
mục cha
commit
9ea1a64d22
2 tập tin đã thay đổi với 150 bổ sung17 xóa
  1. 22 16
      pynecone/var.py
  2. 128 1
      tests/test_var.py

+ 22 - 16
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

+ 128 - 1
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(