Repository: python/mypy

Issue:
Crash when using decorated function as type annotation in import cycle
Discovered from a user report at Dropbox, the following test case crashes:
```
[case testCheckDecoratedFuncAsAnnotWithImportCycle]
import a
[file a.py]
from typing import TypeVar
import b

T = TypeVar('T')
def idf(x: T) -> T: return x

@idf
def Session() -> None: pass

[file b.py]
MYPY = False
if MYPY:
    from a import Session

def f(self, session: Session) -> None:  # E: Invalid type "a.Session"
    pass
[builtins fixtures/bool.pyi]
```

with a traceback ending with:
```
  File "/home/msullivan/src/mypy/mypy/semanal.py", line 442, in _visit_func_def
    defn.type = analyzer.visit_callable_type(defn.type, nested=False)
  File "/home/msullivan/src/mypy/mypy/typeanal.py", line 421, in visit_callable_type
    ret = t.copy_modified(arg_types=self.anal_array(t.arg_types, nested=nested),
  File "/home/msullivan/src/mypy/mypy/typeanal.py", line 639, in anal_array
    res.append(self.anal_type(t, nested))
  File "/home/msullivan/src/mypy/mypy/typeanal.py", line 646, in anal_type
    return t.accept(self)
  File "/home/msullivan/src/mypy/mypy/types.py", line 242, in accept
    return visitor.visit_unbound_type(self)
  File "/home/msullivan/src/mypy/mypy/typeanal.py", line 182, in visit_unbound_type
    typ = self.visit_unbound_type_nonoptional(t)
  File "/home/msullivan/src/mypy/mypy/typeanal.py", line 218, in visit_unbound_type_nonoptional
    tvar_def = self.tvar_scope.get_binding(sym)
  File "/home/msullivan/src/mypy/mypy/tvar_scope.py", line 78, in get_binding
```


Discussion:
Looks like an easy fix

Resolving patch:
diff --git a/mypy/semanal_pass1.py b/mypy/semanal_pass1.py
--- a/mypy/semanal_pass1.py
+++ b/mypy/semanal_pass1.py
@@ -147,14 +147,20 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
             for lval in s.lvalues:
                 self.analyze_lvalue(lval, explicit_type=s.type is not None)
 
-    def visit_func_def(self, func: FuncDef) -> None:
+    def visit_func_def(self, func: FuncDef, decorated: bool = False) -> None:
+        """Process a func def.
+
+        decorated is true if we are processing a func def in a
+        Decorator that needs a _fullname and to have its body analyzed but
+        does not need to be added to the symbol table.
+        """
         sem = self.sem
         if sem.type is not None:
             # Don't process methods during pass 1.
             return
         func.is_conditional = sem.block_depth[-1] > 0
         func._fullname = sem.qualified_name(func.name())
-        at_module = sem.is_module_scope()
+        at_module = sem.is_module_scope() and not decorated
         if (at_module and func.name() == '__getattr__' and
                 self.sem.cur_mod_node.is_package_init_file() and self.sem.cur_mod_node.is_stub):
             if isinstance(func.type, CallableType):
@@ -311,6 +317,7 @@ def visit_decorator(self, d: Decorator) -> None:
             return
         d.var._fullname = self.sem.qualified_name(d.var.name())
         self.add_symbol(d.var.name(), SymbolTableNode(self.kind_by_scope(), d), d)
+        self.visit_func_def(d.func, decorated=True)
 
     def visit_if_stmt(self, s: IfStmt) -> None:
         infer_reachability_of_if_statement(s, self.sem.options)


Test patch:
diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test
--- a/test-data/unit/check-modules.test
+++ b/test-data/unit/check-modules.test
@@ -733,6 +733,27 @@ import m.a
 [file m/a.py]
 [out]
 
+[case testCheckDecoratedFuncAsAnnotWithImportCycle]
+import a
+[file a.py]
+from typing import TypeVar
+import b
+
+T = TypeVar('T')
+def idf(x: T) -> T: return x
+
+@idf
+def Session() -> None: pass
+
+[file b.py]
+MYPY = False
+if MYPY:
+    from a import Session
+
+def f(self, session: Session) -> None:  # E: Invalid type "a.Session"
+    pass
+[builtins fixtures/bool.pyi]
+
 
 -- Checks dealing with submodules and different kinds of imports
 -- -------------------------------------------------------------
diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test
--- a/test-data/unit/check-unreachable-code.test
+++ b/test-data/unit/check-unreachable-code.test
@@ -416,6 +416,25 @@ x = 1
 [builtins fixtures/ops.pyi]
 [out]
 
+[case testSysPlatformInFunctionImport3]
+from typing import Callable
+import sys
+
+def idf(x: Callable[[], None]) -> Callable[[], None]: return x
+
+@idf
+def foo() -> None:
+    if sys.platform == 'fictional':
+        import b as a
+    else:
+        import a
+    a.x
+[file a.py]
+x = 1
+[builtins fixtures/ops.pyi]
+[out]
+
+
 [case testSysPlatformInMethodImport2]
 import sys
 class A: