Repository: python/mypy

Issue:
Support updating tuple subclasses in fine-grained incremental
Here is a test that fails, producing the *terrible* error message of `Argument 1 to "f" has incompatible type "N"; expected "N"`

```
[case testNamedTupleUpdate]
import b
[file a.py]
from typing import NamedTuple
N = NamedTuple('N', [('x', int)])
x = N(1)
[file a.py.2]
from typing import NamedTuple
N = NamedTuple('N', [('x', str)])
x = N('hi')
[file b.py]
import a
def f(x: a.N) -> None:
    pass
f(a.x)
[out]
==
```

I had been hoping that this would be a simple matter of mucking about with the `tuple_type` before merging `TypeInfo`s, but that turns out not to work: the `tuple_type` stored in the `TypeInfo` is not actually the same `TupleType` that is used for objects of that type.

One possible solution is to add a layer of indirection in order to support updating type aliases (#4394) and then reuse that for TupleTypes as well.


Discussion:
Oh, wait, this is actually a lot easier than that. It doesn't need to be resolved in merge: it suffices to make changes to `tuple_type` count as "major" changes to a type.

Resolving patch:
diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py
--- a/mypy/server/astdiff.py
+++ b/mypy/server/astdiff.py
@@ -306,13 +306,13 @@ def snapshot_definition(node: Optional[SymbolNode],
         #   type_vars
         #   bases
         #   _promote
-        #   tuple_type
         #   typeddict_type
         attrs = (node.is_abstract,
                  node.is_enum,
                  node.fallback_to_any,
                  node.is_named_tuple,
                  node.is_newtype,
+                 snapshot_optional_type(node.tuple_type),
                  [base.fullname() for base in node.mro])
         prefix = node.fullname()
         symbol_table = snapshot_symbol_table(prefix, node.names)
diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py
--- a/mypy/server/astmerge.py
+++ b/mypy/server/astmerge.py
@@ -259,7 +259,6 @@ def process_type_info(self, info: TypeInfo) -> None:
         # - declared_metaclass
         # - metaclass_type
         # - _promote
-        # - tuple_type
         # - typeddict_type
         # - replaced
         replace_nodes_in_symbol_table(info.names, self.replacements)
@@ -267,6 +266,8 @@ def process_type_info(self, info: TypeInfo) -> None:
             info.mro[i] = self.fixup(info.mro[i])
         for i, base in enumerate(info.bases):
             self.fixup_type(info.bases[i])
+        if info.tuple_type:
+            self.fixup_type(info.tuple_type)
 
     def replace_statements(self, nodes: List[Statement]) -> List[Statement]:
         result = []


Test patch:
diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test
--- a/test-data/unit/diff.test
+++ b/test-data/unit/diff.test
@@ -274,6 +274,7 @@ N = NamedTuple('N', [('x', int), ('y', int)])
 M = NamedTuple('M', [('x', int), ('y', str)])
 [out]
 __main__.A
+__main__.N
 __main__.N.__init__
 __main__.N._asdict
 __main__.N._make
diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -1563,3 +1563,39 @@ class C:
 ==
 main:5: error: Too few arguments for "__init__" of "C"
 main:6: error: Too few arguments for "D"
+
+[case testNamedTupleUpdate]
+import b
+[file a.py]
+from typing import NamedTuple
+N = NamedTuple('N', [('x', int)])
+x = N(1)
+[file a.py.2]
+from typing import NamedTuple
+N = NamedTuple('N', [('x', str)])
+x = N('hi')
+[file b.py]
+import a
+def f(x: a.N) -> None:
+    pass
+f(a.x)
+[out]
+==
+
+[case testNamedTupleUpdate2]
+import b
+[file a.py]
+from typing import NamedTuple
+N = NamedTuple('N', [('x', int)])
+x = N(1)
+[file a.py.2]
+from typing import NamedTuple
+N = NamedTuple('N', [('y', int)])
+x = N(2)
+[file b.py]
+import a
+def f(x: a.N) -> None:
+    pass
+f(a.x)
+[out]
+==