Repository: python/mypy
Pull request: Fix type of forward reference to a decorated class method
The decorator case wasn't properly handled before when accessing an attribute
through a type object -- the type of the forward reference defaulted to `Any`.

The fix doesn't work at module top level since module top levels can't
be deferred, but the reference would generally fail at runtime anyway.

Fixes #4485.

Problem statement:
Forward reference to decorated class method has type Any
In the program below, the type inferred for `cls.g` in `A.f` is `Any`, even though it should be `def (x: builtins.str)`:


```py
from typing import TypeVar, Callable

T = TypeVar('T')
def dec() -> Callable[[T], T]: pass

class A:
    @classmethod
    def f(cls) -> None:
        reveal_type(cls.g)  # Any  <-- unexpected

    @classmethod
    @dec()
    def g(cls, x: str) -> None:
        pass

    @classmethod
    def h(cls) -> None:
        reveal_type(cls.g)  # def (x: builtins.str)  <-- ok
```

This works correctly when the decorated method is accessed using an instance type (such as `self`).

In fine-grained incremental mode the types can be more precise, which can result in additional errors in fine-grained incremental mode.

Hints from issue discussion:

Commit message: Fix type of forward reference to a decorated class method

The case wasn't properly handled before when accessing through a type
object -- the type of the forward reference defaulted to `Any`.

The fix doesn't work at module top level since module top levels can't
be deferred, but the reference would generally fail at runtime anyway.

Fixes #4485.

Patch to review:
diff --git a/mypy/checkmember.py b/mypy/checkmember.py
index af2d142335e4..875546154b9b 100644
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -467,8 +467,12 @@ def analyze_class_attribute_access(itype: Instance,
         return builtin_type('types.ModuleType')
 
     if is_decorated:
-        # TODO: Return type of decorated function. This is quick hack to work around #998.
-        return AnyType(TypeOfAny.special_form)
+        assert isinstance(node.node, Decorator)
+        if node.node.type:
+            return node.node.type
+        else:
+            not_ready_callback(name, context)
+            return AnyType(TypeOfAny.special_form)
     else:
         return function_type(cast(FuncBase, node.node), builtin_type('builtins.function'))
 
diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test
index c9bb0fdb05c9..9cf006548088 100644
--- a/test-data/unit/check-inference.test
+++ b/test-data/unit/check-inference.test
@@ -1785,6 +1785,29 @@ y = 0
 tmp/m.py:2: error: "int" not callable
 main:3: error: "int" not callable
 
+[case testForwardReferenceToDecoratedClassMethod]
+from typing import TypeVar, Callable
+
+T = TypeVar('T')
+def dec() -> Callable[[T], T]: pass
+
+A.g  # E: Cannot determine type of 'g'
+
+class A:
+    @classmethod
+    def f(cls) -> None:
+        reveal_type(cls.g)  # E: Revealed type is 'def (x: builtins.str)'
+
+    @classmethod
+    @dec()
+    def g(cls, x: str) -> None:
+        pass
+
+    @classmethod
+    def h(cls) -> None:
+        reveal_type(cls.g)  # E: Revealed type is 'def (x: builtins.str)'
+[builtins fixtures/classmethod.pyi]
+
 
 -- Tests for special cases of unification
 -- --------------------------------------


Code review comments:
diff_hunk: @@ -467,8 +467,12 @@ def analyze_class_attribute_access(itype: Instance,
         return builtin_type('types.ModuleType')
 
     if is_decorated:
-        # TODO: Return type of decorated function. This is quick hack to work around #998.
-        return AnyType(TypeOfAny.special_form)
+        assert isinstance(node.node, Decorator)
+        if node.node.type:
+            return node.node.type
+        else:
+            not_ready_callback(name, context)
+            return AnyType(TypeOfAny.special_form)
path: mypy/checkmember.py
text: @user1:
I thought that we normally return `TypeOfAny.from_error` in all cases where the may be an error.