Repository: python/mypy

Issue:
"AssertionError: Cannot find component" for local namedtuple
<!--
  Use this form only if mypy reports an "INTERNAL ERROR" and/or gives a traceback.
  Please include the traceback and all other messages below (use `mypy --show-traceback`).
-->

**Crash Report**

mypy crashes when inheriting from a locally defined namedtuple, when all of the following is true:
- Its fields are not a literal
- Another file generates an `attr-defined` error
- Cache is enabled (without `--no-incremental`)

Seems to be related to https://github.com/python/mypy/issues/10913, but produces a traceback similar to the closed https://github.com/python/mypy/issues/7281.

**Traceback**

```
Traceback (most recent call last):
  File "/home/.../env/bin/mypy", line 8, in <module>
    sys.exit(console_entry())
  File "/home/.../env/lib/python3.7/site-packages/mypy/__main__.py", line 11, in console_entry
    main(None, sys.stdout, sys.stderr)
  File "mypy/main.py", line 87, in main
  File "mypy/main.py", line 165, in run_build
  File "mypy/build.py", line 179, in build
  File "mypy/build.py", line 254, in _build
  File "mypy/build.py", line 2697, in dispatch
  File "mypy/build.py", line 3014, in process_graph
  File "mypy/build.py", line 3092, in process_fresh_modules
  File "mypy/build.py", line 1991, in fix_cross_refs
  File "mypy/fixup.py", line 26, in fixup_module
  File "mypy/fixup.py", line 90, in visit_symbol_table
  File "mypy/fixup.py", line 49, in visit_type_info
  File "mypy/types.py", line 846, in accept
  File "mypy/fixup.py", line 154, in visit_instance
  File "mypy/fixup.py", line 269, in lookup_qualified_typeinfo
  File "mypy/fixup.py", line 297, in lookup_qualified
  File "mypy/fixup.py", line 306, in lookup_qualified_stnode
  File "mypy/lookup.py", line 47, in lookup_fully_qualified
AssertionError: Cannot find component 'TupleType@4' for 'a.TupleType@4'
```

**To Reproduce**

Use this script to reproduce:
```bash
#!/bin/sh

mkdir -p mypy_crash

cat >mypy_crash/a.py <<EOF
from collections import namedtuple

def crash(fields):
    TupleType = namedtuple("TupleType", fields)
    class InheritFromTuple(TupleType):
        pass
EOF

cat >mypy_crash/b.py <<EOF
from a import A
EOF

rm -rf .mypy_cache

echo "First run, should be ok"
mypy mypy_crash/
echo

echo "Second run, with cache disabled, should be ok"
mypy --no-incremental mypy_crash/
echo

echo "Third run, with cache enabled, this should crash"
mypy mypy_crash/
```

mypy does not crash if any of the following are removed:
- File `b.py`
- Import error in file `b.py`, e.g. by replacing `from a import A` by `from a import crash` (or any other valid name)
- Definition of `class InheritFromTuple` in `a.py`
- Argument `fields` in definition of namedtuple, e.g. by `TupleType = namedtuple("TupleType", 'a b c')`

**Your Environment**

<!-- Include as many relevant details about the environment you experienced the bug in -->

- Mypy version used: 0.910
- Mypy command-line flags: (none, see script)
- Mypy configuration options from `mypy.ini` (and other config files): (none, can reproduce without mypy.ini)
- Python version used: 3.7.9
- Operating system and version: Ubuntu 18.04.6 LTS (bionic)

<!--
You can freely edit this text, please remove all the lines
you believe are unnecessary.
-->



Discussion:

Resolving patch:
diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py
--- a/mypy/semanal_namedtuple.py
+++ b/mypy/semanal_namedtuple.py
@@ -186,11 +186,16 @@ def check_namedtuple(self,
             # Error. Construct dummy return value.
             if var_name:
                 name = var_name
+                if is_func_scope:
+                    name += '@' + str(call.line)
             else:
-                name = 'namedtuple@' + str(call.line)
+                name = var_name = 'namedtuple@' + str(call.line)
             info = self.build_namedtuple_typeinfo(name, [], [], {}, node.line)
-            self.store_namedtuple_info(info, name, call, is_typed)
-            return name, info
+            self.store_namedtuple_info(info, var_name, call, is_typed)
+            if name != var_name or is_func_scope:
+                # NOTE: we skip local namespaces since they are not serialized.
+                self.api.add_symbol_skip_local(name, info)
+            return var_name, info
         if not ok:
             # This is a valid named tuple but some types are not ready.
             return typename, None


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
@@ -5711,3 +5711,32 @@ class C:
 [builtins fixtures/dict.pyi]
 [out2]
 tmp/a.py:2: error: "object" has no attribute "xyz"
+
+[case testIncrementalInvalidNamedTupleInUnannotatedFunction]
+import a
+
+[file a.py]
+import b
+
+[file a.py.2]
+import b # f
+
+[file b.py]
+from typing import NamedTuple
+
+def toplevel(fields):
+    TupleType = NamedTuple("TupleType", fields)
+    class InheritFromTuple(TupleType):
+        pass
+    NT2 = NamedTuple("bad", [('x', int)])
+    nt2: NT2 = NT2(x=1)
+
+class C:
+    def method(self, fields):
+        TupleType = NamedTuple("TupleType", fields)
+        class InheritFromTuple(TupleType):
+            pass
+        NT2 = NamedTuple("bad", [('x', int)])
+        nt2: NT2 = NT2(x=1)
+
+[builtins fixtures/tuple.pyi]
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
@@ -1120,3 +1120,17 @@ def bar1(c: C1) -> None:
         reveal_type(c)  # N: Revealed type is "Tuple[builtins.int, fallback=__main__.C1]"
 [builtins fixtures/tuple.pyi]
 [typing fixtures/typing-namedtuple.pyi]
+
+[case testInvalidNamedTupleWithinFunction]
+from collections import namedtuple
+
+def f(fields) -> None:
+    TupleType = namedtuple("TupleType", fields) \
+        # E: List or tuple literal expected as the second argument to "namedtuple()"
+    class InheritFromTuple(TupleType):
+        pass
+    t: TupleType
+    it: InheritFromTuple
+    NT2 = namedtuple("bad", "x") # E: First argument to namedtuple() should be "NT2", not "bad"
+    nt2: NT2 = NT2(x=1)
+[builtins fixtures/tuple.pyi]