Repository: python/mypy

Issue:
Unfortunate clash of union, generic and multiple inheritance
Behold:

``` python
from typing import Generic, TypeVar, Union

T = TypeVar('T')
S = TypeVar('S')

class A(Generic[T]): pass

class I(Generic[T]): pass

class F(A[str], I[str]): pass

class Loop:
    def run(self, future: Union[A[S], I[S]]) -> S: pass

loop = Loop()
f = F()
loop.run(f)  # Error (see below)
```

This gives the following error:

```
__tmp__.py:17: error: Argument 1 to "run" of "Loop" has incompatible type "F"; expected "Union[A[None], I[None]]"
```

I spent way too much time boiling it down to this. It must involve multiple inheritance from two generic classes and an instance whose type matches either variant of the union; if I reduce the example further (e.g. make A or I non-generic) the error goes away.

I don't understand why this is (and it's probably a duplicate) but it's holding up creating proper stubs for asyncio that incorporate Awaitable. (In the original, A is Awaitable, I is Iterable, F is Future, Loop is AbstractEventLoop.)



Discussion:
I ran into this too, because `typing.AwaitableGenerator` is apparently affected by the bug. I commented on https://github.com/python/typeshed/pull/903 with some close-to-real affected code.

Here's what I reduced my example to (slightly smaller than Guido's; T is Generator, U is Awaitable, and V is AwaitableGenerator):
```
from typing import TypeVar, Union, Generic

_T = TypeVar('_T')

class T(Generic[_T]): pass
class U(Generic[_T]): pass
class V(T[_T], U[_T]): pass

def wait_for(fut: Union[T[_T], U[_T]]) -> None: ...

wait_for(V[bool]())
```
This is probably due to how constraints are inferred for unions. Here there are two ways we can infer constraints between `V[bool]` and `Union[T[_T], U[_T]]`, since `V` multiply inherits from `T` and `U`. In this case the constraints would be the same either way, so mypy should be able to infer them without problems. There actually is a TODO comment in `mypy.constraints.any_constraints` that discusses this problem, so I suspect that this is a known issue. I'll have a look at it.

Resolving patch:
diff --git a/mypy/constraints.py b/mypy/constraints.py
--- a/mypy/constraints.py
+++ b/mypy/constraints.py
@@ -12,6 +12,7 @@
 from mypy.maptype import map_instance_to_supertype
 from mypy import nodes
 import mypy.subtypes
+from mypy.sametypes import is_same_type
 from mypy.erasetype import erase_typevars
 
 
@@ -203,15 +204,33 @@ def any_constraints(options: List[Optional[List[Constraint]]], eager: bool) -> L
         valid_options = [option for option in options if option is not None]
     if len(valid_options) == 1:
         return valid_options[0]
-    # Otherwise, there are either no valid options or multiple valid options.
-    # Give up and deduce nothing.
+    elif (len(valid_options) > 1 and
+          all(is_same_constraints(valid_options[0], c)
+              for c in valid_options[1:])):
+        # Multiple sets of constraints that are all the same. Just pick any one of them.
+        # TODO: More generally, if a given (variable, direction) pair appears in
+        #       every option, combine the bounds with meet/join.
+        return valid_options[0]
+
+    # Otherwise, there are either no valid options or multiple, inconsistent valid
+    # options. Give up and deduce nothing.
     return []
 
-    # TODO: In the latter case, it could happen that every valid
-    # option requires the same constraint on the same variable. Then
-    # we could include that that constraint in the result. Or more
-    # generally, if a given (variable, direction) pair appears in
-    # every option, combine the bounds with meet/join.
+
+def is_same_constraints(x: List[Constraint], y: List[Constraint]) -> bool:
+    for c1 in x:
+        if not any(is_same_constraint(c1, c2) for c2 in y):
+            return False
+    for c1 in y:
+        if not any(is_same_constraint(c1, c2) for c2 in x):
+            return False
+    return True
+
+
+def is_same_constraint(c1: Constraint, c2: Constraint) -> bool:
+    return (c1.type_var == c2.type_var
+            and c1.op == c2.op
+            and is_same_type(c1.target, c2.target))
 
 
 def simplify_away_incomplete_types(types: List[Type]) -> List[Type]:


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
@@ -756,6 +756,34 @@ b = k1
 b = k2
 [builtins fixtures/list.pyi]
 
+[case testAmbiguousUnionContextAndMultipleInheritance]
+from typing import TypeVar, Union, Generic
+
+_T = TypeVar('_T')
+
+class T(Generic[_T]): pass
+class U(Generic[_T]): pass
+class V(T[_T], U[_T]): pass
+
+def wait_for(fut: Union[T[_T], U[_T]]) -> _T: ...
+
+reveal_type(wait_for(V[str]()))  # E: Revealed type is 'builtins.str*'
+
+[case testAmbiguousUnionContextAndMultipleInheritance2]
+from typing import TypeVar, Union, Generic
+
+_T = TypeVar('_T')
+_S = TypeVar('_S')
+
+class T(Generic[_T, _S]): pass
+class U(Generic[_T, _S]): pass
+class V(T[_T, _S], U[_T, _S]): pass
+
+def wait_for(fut: Union[T[_T, _S], U[_T, _S]]) -> T[_T, _S]: ...
+
+reveal_type(wait_for(V[int, str]()))  \
+    # E: Revealed type is '__main__.T[builtins.int*, builtins.str*]'
+
 
 -- Literal expressions
 -- -------------------