Repository: python/mypy

Issue:
AssertionError: Cannot find X for Y.X (incremental with warm cache)
This is similar to #2451 but the cause is different. I haven't boiled it down yet but the clue seems to be there's a NamedTuple definition inside a [**EDITs from here:**] for-loop inside a method. And another report where there's a namedtuple inside a method.


Discussion:
OK, simple repro: run `mypy -i main.py ntcrash.py` twice (without touching any files in between) where:
- ntcrash.py:
```python
from typing import NamedTuple

class C:
    def f(self) -> None:
        A = NamedTuple('A', [('x', int), ('y', int)])
```
- main.py:
```python
import ntcrash
```
The problem is in the code that @kirbyfan64 fixed -- it's still broken in this case due to the peculiarities of how namedtuple classes are bootstrapped in the mypy code.
Is this the 5th item in #2090 - "handle locally-defined namedtuple"? 
Yes, that's right. I'm not sure how to resolve it. Maybe the "qualified name" should be similar to what we use at runtime? E.g. with this code
```python
class C:
    def f(self):
        class D: pass
        return D()
d = C().f()
print(d.__class__.__qualname__)
```
the output is `C.f.<locals>.D`. Combined with the module name that might work? But we'd have to introduce a namespace for the method.

Thinking aloud, another possibility would be to give the inner class a synthetic qualified name starting with a digit, e.g. `C.0001_f_D` (plus module prefix).
(The key requirement here being simply that whatever the qualified name is can be found by the lookup method in fixup.py. We may have to modify both the qualified name and the lookup function to deal with the special case.)
I'm going crazy. I can only make my repro "work" (i.e. cause the AssertionError) if I make main.py import a non-existent symbol from ntcrash, e.g.`from ntcrash import nope`.
It's unrelated to the content. You should `touch main.py` before running mypy. 
That doesn't work for me. I delete the cache before starting. I really do have import something non-existing from ntcrash into main.

Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -1678,14 +1678,18 @@ def check_namedtuple(self, node: Expression, var_name: str = None) -> Optional[T
         if not ok:
             # Error. Construct dummy return value.
             return self.build_namedtuple_typeinfo('namedtuple', [], [])
+        name = cast(StrExpr, call.args[0]).value
+        if name != var_name or self.is_func_scope():
+            # Give it a unique name derived from the line number.
+            name += '@' + str(call.line)
+        info = self.build_namedtuple_typeinfo(name, items, types)
+        # Store it as a global just in case it would remain anonymous.
+        # (Or in the nearest class if there is one.)
+        stnode = SymbolTableNode(GDEF, info, self.cur_mod_id)
+        if self.type:
+            self.type.names[name] = stnode
         else:
-            name = cast(StrExpr, call.args[0]).value
-            if name != var_name:
-                # Give it a unique name derived from the line number.
-                name += '@' + str(call.line)
-            info = self.build_namedtuple_typeinfo(name, items, types)
-            # Store it as a global just in case it would remain anonymous.
-            self.globals[name] = SymbolTableNode(GDEF, info, self.cur_mod_id)
+            self.globals[name] = stnode
         call.analyzed = NamedTupleExpr(info)
         call.analyzed.set_line(call.line, call.column)
         return info
@@ -1897,14 +1901,18 @@ def check_typeddict(self, node: Expression, var_name: str = None) -> Optional[Ty
         if not ok:
             # Error. Construct dummy return value.
             return self.build_typeddict_typeinfo('TypedDict', [], [])
+        name = cast(StrExpr, call.args[0]).value
+        if name != var_name or self.is_func_scope():
+            # Give it a unique name derived from the line number.
+            name += '@' + str(call.line)
+        info = self.build_typeddict_typeinfo(name, items, types)
+        # Store it as a global just in case it would remain anonymous.
+        # (Or in the nearest class if there is one.)
+        stnode = SymbolTableNode(GDEF, info, self.cur_mod_id)
+        if self.type:
+            self.type.names[name] = stnode
         else:
-            name = cast(StrExpr, call.args[0]).value
-            if name != var_name:
-                # Give it a unique name derived from the line number.
-                name += '@' + str(call.line)
-            info = self.build_typeddict_typeinfo(name, items, types)
-            # Store it as a global just in case it would remain anonymous.
-            self.globals[name] = SymbolTableNode(GDEF, info, self.cur_mod_id)
+            self.globals[name] = stnode
         call.analyzed = TypedDictExpr(info)
         call.analyzed.set_line(call.line, call.column)
         return info


Test patch:
diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test
--- a/test-data/unit/check-incremental.test
+++ b/test-data/unit/check-incremental.test
@@ -1640,3 +1640,129 @@ follow_imports = skip
 main:3: error: Revealed type is 'builtins.int'
 [out2]
 main:3: error: Revealed type is 'Any'
+
+[case testIncrementalNamedTupleInMethod]
+from ntcrash import nope
+[file ntcrash.py]
+from typing import NamedTuple
+class C:
+    def f(self) -> None:
+        A = NamedTuple('A', [('x', int), ('y', int)])
+[out1]
+main:1: error: Module 'ntcrash' has no attribute 'nope'
+[out2]
+main:1: error: Module 'ntcrash' has no attribute 'nope'
+
+[case testIncrementalNamedTupleInMethod2]
+from ntcrash import nope
+[file ntcrash.py]
+from typing import NamedTuple
+class C:
+    class D:
+        def f(self) -> None:
+            A = NamedTuple('A', [('x', int), ('y', int)])
+[out1]
+main:1: error: Module 'ntcrash' has no attribute 'nope'
+[out2]
+main:1: error: Module 'ntcrash' has no attribute 'nope'
+
+[case testIncrementalNamedTupleInMethod3]
+from ntcrash import nope
+[file ntcrash.py]
+from typing import NamedTuple
+class C:
+    def a(self):
+        class D:
+            def f(self) -> None:
+                A = NamedTuple('A', [('x', int), ('y', int)])
+[out1]
+main:1: error: Module 'ntcrash' has no attribute 'nope'
+[out2]
+main:1: error: Module 'ntcrash' has no attribute 'nope'
+
+[case testIncrementalNamedTupleInMethod4]
+from ntcrash import C
+reveal_type(C().a)
+reveal_type(C().b)
+reveal_type(C().c)
+[file ntcrash.py]
+from typing import NamedTuple
+class C:
+    def __init__(self) -> None:
+        A = NamedTuple('A', [('x', int)])
+        self.a = A(0)
+        self.b = A(0)  # type: A
+        self.c = A
+[out1]
+main:2: error: Revealed type is 'Tuple[builtins.int, fallback=ntcrash.C.A@4]'
+main:3: error: Revealed type is 'Tuple[builtins.int, fallback=ntcrash.C.A@4]'
+main:4: error: Revealed type is 'def (x: builtins.int) -> Tuple[builtins.int, fallback=ntcrash.C.A@4]'
+[out2]
+main:2: error: Revealed type is 'Tuple[builtins.int, fallback=ntcrash.C.A@4]'
+main:3: error: Revealed type is 'Tuple[builtins.int, fallback=ntcrash.C.A@4]'
+main:4: error: Revealed type is 'def (x: builtins.int) -> Tuple[builtins.int, fallback=ntcrash.C.A@4]'
+
+[case testIncrementalTypedDictInMethod]
+from tdcrash import nope
+[file tdcrash.py]
+from mypy_extensions import TypedDict
+class C:
+    def f(self) -> None:
+        A = TypedDict('A', {'x': int, 'y': int})
+[builtins fixtures/dict.pyi]
+[out1]
+main:1: error: Module 'tdcrash' has no attribute 'nope'
+[out2]
+main:1: error: Module 'tdcrash' has no attribute 'nope'
+
+[case testIncrementalTypedDictInMethod2]
+from tdcrash import nope
+[file tdcrash.py]
+from mypy_extensions import TypedDict
+class C:
+    class D:
+        def f(self) -> None:
+            A = TypedDict('A', {'x': int, 'y': int})
+[builtins fixtures/dict.pyi]
+[out1]
+main:1: error: Module 'tdcrash' has no attribute 'nope'
+[out2]
+main:1: error: Module 'tdcrash' has no attribute 'nope'
+
+[case testIncrementalTypedDictInMethod3]
+from tdcrash import nope
+[file tdcrash.py]
+from mypy_extensions import TypedDict
+class C:
+    def a(self):
+        class D:
+            def f(self) -> None:
+                A = TypedDict('A', {'x': int, 'y': int})
+[builtins fixtures/dict.pyi]
+[out1]
+main:1: error: Module 'tdcrash' has no attribute 'nope'
+[out2]
+main:1: error: Module 'tdcrash' has no attribute 'nope'
+
+[case testIncrementalTypedDictInMethod4]
+from ntcrash import C
+reveal_type(C().a)
+reveal_type(C().b)
+reveal_type(C().c)
+[file ntcrash.py]
+from mypy_extensions import TypedDict
+class C:
+    def __init__(self) -> None:
+        A = TypedDict('A', {'x': int})
+        self.a = A(x=0)
+        self.b = A(x=0)  # type: A
+        self.c = A
+[builtins fixtures/dict.pyi]
+[out1]
+main:2: error: Revealed type is 'TypedDict(x=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])'
+main:3: error: Revealed type is 'TypedDict(x=builtins.int, _fallback=ntcrash.C.A@4)'
+main:4: error: Revealed type is 'def () -> ntcrash.C.A@4'
+[out2]
+main:2: error: Revealed type is 'TypedDict(x=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])'
+main:3: error: Revealed type is 'TypedDict(x=builtins.int, _fallback=ntcrash.C.A@4)'
+main:4: error: Revealed type is 'def () -> ntcrash.C.A@4'
diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test
--- a/test-data/unit/check-namedtuple.test
+++ b/test-data/unit/check-namedtuple.test
@@ -411,3 +411,19 @@ reveal_type(B._make(['']))  # E: Revealed type is 'Tuple[builtins.str, fallback=
 b = B._make([''])  # type: B
 
 [builtins fixtures/list.pyi]
+
+[case testNamedTupleInClassNamespace]
+# https://github.com/python/mypy/pull/2553#issuecomment-266474341
+from typing import NamedTuple
+class C:
+    def f(self):
+        A = NamedTuple('A', [('x', int)])
+    def g(self):
+        A = NamedTuple('A', [('y', int)])
+C.A  # E: "C" has no attribute "A"
+
+[case testNamedTupleInFunction]
+from typing import NamedTuple
+def f() -> None:
+    A = NamedTuple('A', [('x', int)])
+A  # E: Name 'A' is not defined
diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test
--- a/test-data/unit/check-typeddict.test
+++ b/test-data/unit/check-typeddict.test
@@ -432,3 +432,22 @@ reveal_type(f(g))  # E: Revealed type is '<uninhabited>'
 
 -- TODO: Implement support for this case.
 --[case testCannotIsInstanceTypedDictType]
+
+-- scoping
+[case testTypedDictInClassNamespace]
+# https://github.com/python/mypy/pull/2553#issuecomment-266474341
+from mypy_extensions import TypedDict
+class C:
+    def f(self):
+        A = TypedDict('A', {'x': int})
+    def g(self):
+        A = TypedDict('A', {'y': int})
+C.A  # E: "C" has no attribute "A"
+[builtins fixtures/dict.pyi]
+
+[case testTypedDictInFunction]
+from mypy_extensions import TypedDict
+def f() -> None:
+    A = TypedDict('A', {'x': int})
+A  # E: Name 'A' is not defined
+[builtins fixtures/dict.pyi]