Repository: python/mypy

Issue:
Master (0.530-dev) crash on tuple unpacking
Repro:
```py3
from typing import Tuple, TypeVar

_T1 = TypeVar('_T1')

def make_tuple(elem: _T1) -> Tuple[_T1]:
    return (elem,)

def main() -> None:
    ((a, b),) = make_tuple((1, 2))
```

produces:
```
Traceback (most recent call last):
  File "/Users/ambv/.venvs/mypy/bin/mypy", line 11, in <module>
    load_entry_point('mypy', 'console_scripts', 'mypy')()
  File "mypy/__main__.py", line 7, in console_entry
    main(None)
  File "mypy/main.py", line 50, in main
    res = type_check_only(sources, bin_dir, options)
  File "mypy/main.py", line 103, in type_check_only
    options=options)
  File "mypy/build.py", line 198, in build
    graph = dispatch(sources, manager)
  File "mypy/build.py", line 1841, in dispatch
    process_graph(graph, manager)
  File "mypy/build.py", line 2091, in process_graph
    process_stale_scc(graph, scc, manager)
  File "mypy/build.py", line 2194, in process_stale_scc
    graph[id].type_check_first_pass()
  File "mypy/build.py", line 1753, in type_check_first_pass
    self.type_checker.check_first_pass()
  File "mypy/checker.py", line 194, in check_first_pass
    self.accept(d)
  File "mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "mypy/nodes.py", line 519, in accept
    return visitor.visit_func_def(self)
  File "mypy/checker.py", line 530, in visit_func_def
    self.check_func_item(defn, name=defn.name())
  File "mypy/checker.py", line 590, in check_func_item
    self.check_func_def(defn, typ, name)
  File "mypy/checker.py", line 750, in check_func_def
    self.accept(item.body)
  File "mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "mypy/nodes.py", line 773, in accept
    return visitor.visit_block(self)
  File "mypy/checker.py", line 1304, in visit_block
    self.accept(s)
  File "mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "mypy/nodes.py", line 817, in accept
    return visitor.visit_assignment_stmt(self)
  File "mypy/checker.py", line 1311, in visit_assignment_stmt
    self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax)
  File "mypy/checker.py", line 1339, in check_assignment
    infer_lvalue_type)
  File "mypy/checker.py", line 1589, in check_assignment_to_multiple_lvalues
    self.check_multi_assignment(lvalues, rvalue, context, infer_lvalue_type)
  File "mypy/checker.py", line 1630, in check_multi_assignment
    context, undefined_rvalue, infer_lvalue_type)
  File "mypy/checker.py", line 1650, in check_multi_assignment_from_tuple
    reinferred_rvalue_type = self.expr_checker.accept(rvalue, lvalue_type)
  File "mypy/checkexpr.py", line 2372, in accept
    typ = node.accept(self)
  File "mypy/nodes.py", line 1256, in accept
    return visitor.visit_call_expr(self)
  File "mypy/checkexpr.py", line 266, in visit_call_expr
    ret_type = self.check_call_expr_with_callee_type(callee_type, e, fullname, object_type)
  File "mypy/checkexpr.py", line 503, in check_call_expr_with_callee_type
    object_type=object_type)[0]
  File "mypy/checkexpr.py", line 563, in check_call
    callee, context)
  File "mypy/checkexpr.py", line 752, in infer_function_type_arguments_using_context
    erased_ctx = replace_meta_vars(ctx, ErasedType())
  File "mypy/erasetype.py", line 94, in replace_meta_vars
    return t.accept(TypeVarEraser(lambda id: id.is_meta_var(), target_type))
  File "mypy/types.py", line 982, in accept
    return visitor.visit_tuple_type(self)
  File "mypy/types.py", line 1554, in visit_tuple_type
    return TupleType(self.translate_types(t.items),
  File "mypy/types.py", line 1574, in translate_types
    return [t.accept(self) for t in types]
  File "mypy/types.py", line 1574, in <listcomp>
    return [t.accept(self) for t in types]
  File "mypy/types.py", line 982, in accept
    return visitor.visit_tuple_type(self)
  File "mypy/types.py", line 1554, in visit_tuple_type
    return TupleType(self.translate_types(t.items),
  File "mypy/types.py", line 1574, in translate_types
    return [t.accept(self) for t in types]
  File "mypy/types.py", line 1574, in <listcomp>
    return [t.accept(self) for t in types]
AttributeError: 'NoneType' object has no attribute 'accept'
/tmp/test.py:13: : note: use --pdb to drop into pdb
```


Discussion:
This was introduced sometime since Aug 11; we've been using a master revision of mypy from that date up until now without this error, and saw this error when attempting to update to eec674584316 today.
Confirmed as a regression on master.
I modified the example to make it way shorter.
Can the OP bisect this to pinpoint to a precise commit/PR?
I don't know where you get the Aug 11 date from. The same crash happens with v0.4.6, released Nov last year. So I don't think this counts as a regression and that makes the fix less urgent.
I'm seeing the same thing in attempting to bisect. I think this changed in @ambv 's simplification of the repro case. IOW I believe the root cause is an old bug in mypy, and a typeshed change since Aug 11 triggered the bug on our actual code using `asyncio.gather`.
The original longer repro case:

```
import asyncio
from typing import Tuple

async def get_location(arg: str) -> Tuple[int, int]:
    return 1, 2

async def main() -> None:
    (
        (a_x, a_y),
    ) = await asyncio.gather(
        get_location('start'),
    )
    print(a_x, a_y)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
```

bisects to #3967. Before that it typechecks clean.

Working on bisecting it within typeshed.
Not surprisingly, the recent regression bisects to https://github.com/python/typeshed/pull/1550
Thanks for confirming my hunch that the simplified repro was worth a separate message rather than editing the OP. :-)

So it seems that the actual bug is old and the improved `gather()` stub is correct. What do you suggest we do now? I'd like to see it fixed, of course, but even with the simplified repro that may not be so easy. Is it worth cherry-picking into the release branch for 0.530? Maybe the fix will be simple and obviously correct, but for a bug that hid this long I kind of doubt it, and I'm out of energy tonight to double down on this.

Maybe @JukkaL or @ilevkivskyi have a fresh insight when they get up.
I noticed that this does not happen for list types, which led me to believe it is part of the special tuple checking code. Stepping through the code, we end up [here](https://github.com/python/mypy/blob/master/mypy/checker.py#L1686). Essentially, we usually give the rvalue type to definitions, which is the correct thing to do. However, when checking nested lvalue tuple types, we [check recursively](https://github.com/python/mypy/blob/master/mypy/checker.py#L1771), so the new definition types don't have an rvalue to inherit, causing their type to be `None`, and things breaking.
Yes, this is related to https://github.com/python/mypy/issues/3966, I actually already have a fix on my last strict optional branch. I will add a test and make a PR now.
Hm, although the crashes are fixed on my branch, the revealed types of ``a`` and ``b`` in the example are ``Any``. I think we can give a more precise type here.

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -1769,7 +1769,10 @@ def check_lvalue(self, lvalue: Lvalue) -> Tuple[Optional[Type],
             lvalue_type = self.expr_checker.analyze_ref_expr(lvalue, lvalue=True)
             self.store_type(lvalue, lvalue_type)
         elif isinstance(lvalue, TupleExpr) or isinstance(lvalue, ListExpr):
-            types = [self.check_lvalue(sub_expr)[0] for sub_expr in lvalue.items]
+            types = [self.check_lvalue(sub_expr)[0] or
+                     # This type will be used as a context for further inference of rvalue,
+                     # we put Uninhabited if there is no information available from lvalue.
+                     UninhabitedType() for sub_expr in lvalue.items]
             lvalue_type = TupleType(types, self.named_type('builtins.tuple'))
         else:
             lvalue_type = self.expr_checker.accept(lvalue)
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -769,7 +769,7 @@ def infer_function_type_arguments_using_context(
         # Only substitute non-Uninhabited and non-erased types.
         new_args = []  # type: List[Optional[Type]]
         for arg in args:
-            if isinstance(arg, UninhabitedType) or has_erased_component(arg):
+            if has_uninhabited_component(arg) or has_erased_component(arg):
                 new_args.append(None)
             else:
                 new_args.append(arg)
@@ -2768,6 +2768,19 @@ def visit_erased_type(self, t: ErasedType) -> bool:
         return True
 
 
+def has_uninhabited_component(t: Optional[Type]) -> bool:
+    return t is not None and t.accept(HasUninhabitedComponentsQuery())
+
+
+class HasUninhabitedComponentsQuery(types.TypeQuery[bool]):
+    """Visitor for querying whether a type has an UninhabitedType component."""
+    def __init__(self) -> None:
+        super().__init__(any)
+
+    def visit_uninhabited_type(self, t: UninhabitedType) -> bool:
+        return True
+
+
 def overload_arg_similarity(actual: Type, formal: Type) -> int:
     """Return if caller argument (actual) is compatible with overloaded signature arg (formal).
 


Test patch:
diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test
--- a/test-data/unit/check-inference.test
+++ b/test-data/unit/check-inference.test
@@ -1927,3 +1927,18 @@ x = None
 (x, x) = f('')
 reveal_type(x) # E: Revealed type is 'builtins.str'
 [out]
+
+[case testInferenceNestedTuplesFromGenericIterable]
+from typing import Tuple, TypeVar
+
+T = TypeVar('T')
+
+def make_tuple(elem: T) -> Tuple[T]:
+    return (elem,)
+
+def main() -> None:
+    ((a, b),) = make_tuple((1, 2))
+    reveal_type(a) # E: Revealed type is 'builtins.int'
+    reveal_type(b) # E: Revealed type is 'builtins.int'
+[builtins fixtures/tuple.pyi]
+[out]
diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test
--- a/test-data/unit/pythoneval.test
+++ b/test-data/unit/pythoneval.test
@@ -1402,3 +1402,18 @@ o: object = p
 it2: Iterable[int] = p
 [out]
 _testCanConvertTypedDictToAnySuperclassOfMapping.py:11: error: Incompatible types in assignment (expression has type "Point", variable has type "Iterable[int]")
+
+[case testAsyncioGatherPreciseType]
+import asyncio
+from typing import Tuple
+
+async def get_location(arg: str) -> Tuple[str, str]:
+    return arg, arg
+
+async def main() -> None:
+    ((a_x, a_y),) = await asyncio.gather(get_location('start'))
+    reveal_type(a_x)
+    reveal_type(a_y)
+[out]
+_testAsyncioGatherPreciseType.py:9: error: Revealed type is 'builtins.str'
+_testAsyncioGatherPreciseType.py:10: error: Revealed type is 'builtins.str'