Repository: python/mypy

Issue:
Mypy incorrectly complains about overloads being incorrect
Minimal repro:

```
from typing import overload, TypeVar, Sequence

class Entity: ...
class Assoc: ...

E = TypeVar("E", bound=Entity)
A = TypeVar("A", bound=Assoc)

class Test:
    @overload
    async def multiget_cached(
        self,
        edgecls: Sequence[E],
    ) -> None:
        ...

    @overload
    async def multiget_cached(
        self,
        edgecls: Sequence[A],
    ) -> None:
        ...

    async def multiget_cached(
        self,
        edgecls: Sequence[E] | Sequence[A],
    ) -> None:                                    
        ...
```

According to Ivan this is likely a bug in the type unifier.


Discussion:
LOL, I just started writing an issue about this as well. It seems to me the root cause is
```python
from typing import Callable, TypeVar, Union, Sequence

TI = TypeVar("TI", bound=int)
TS = TypeVar("TS", bound=str)

f: Callable[[Sequence[TI]], None]
g: Callable[[Union[Sequence[TI], Sequence[TS]]], None]
f = g  # Bogus error: Incompatible types in assignment
```
And IMO a proper fix would be to filter out "trivially unsatisfiable" (because of upper bounds) constraints _and_ then turning `[]` into `None` before comparing constraints in `any_constraints()` (used when handling unions).
Changed topic as it is not specific to overloads.
Another similar bug: https://github.com/python/mypy/issues/13220
Btw although this is a relatively fragile part of the code, it is also a simple PR, I can try it and see what `mypy_primer` will say.
@hauntsaninja That one is a bit different, where we need to select upper bound as a solution if it is "better" that the "best" constraint. IMO this is more risky to cause unpredictable fallout (but FWIW probably also an easy fix by itself).
Oh, I forgot that constraints have `TypeVarId`s not `TypeVarType`, so actually it will be a significant refactoring to pass through the information about upper bounds.

Alternatively, we can just never add unsatisfiable constraints in the leaf case. But this will probably also require `eager=True` for both union cases, which is a big change (and is not very principled).
Yeah, this last hack is not an option. I just tried, it works, but errors are worse, now we get something about `<nothing>` instead of e.g. `Value of type variable "T" of "twice" cannot be "int"`

Resolving patch:
diff --git a/mypy/constraints.py b/mypy/constraints.py
--- a/mypy/constraints.py
+++ b/mypy/constraints.py
@@ -34,6 +34,7 @@
     TypeQuery,
     TypeType,
     TypeVarId,
+    TypeVarLikeType,
     TypeVarTupleType,
     TypeVarType,
     TypeVisitor,
@@ -73,10 +74,11 @@ class Constraint:
     op = 0  # SUBTYPE_OF or SUPERTYPE_OF
     target: Type
 
-    def __init__(self, type_var: TypeVarId, op: int, target: Type) -> None:
-        self.type_var = type_var
+    def __init__(self, type_var: TypeVarLikeType, op: int, target: Type) -> None:
+        self.type_var = type_var.id
         self.op = op
         self.target = target
+        self.origin_type_var = type_var
 
     def __repr__(self) -> str:
         op_str = "<:"
@@ -190,7 +192,7 @@ def _infer_constraints(template: Type, actual: Type, direction: int) -> list[Con
     #     T :> U2", but they are not equivalent to the constraint solver,
     #     which never introduces new Union types (it uses join() instead).
     if isinstance(template, TypeVarType):
-        return [Constraint(template.id, direction, actual)]
+        return [Constraint(template, direction, actual)]
 
     # Now handle the case of either template or actual being a Union.
     # For a Union to be a subtype of another type, every item of the Union
@@ -286,7 +288,7 @@ def merge_with_any(constraint: Constraint) -> Constraint:
     # TODO: if we will support multiple sources Any, use this here instead.
     any_type = AnyType(TypeOfAny.implementation_artifact)
     return Constraint(
-        constraint.type_var,
+        constraint.origin_type_var,
         constraint.op,
         UnionType.make_union([target, any_type], target.line, target.column),
     )
@@ -345,11 +347,37 @@ def any_constraints(options: list[list[Constraint] | None], eager: bool) -> list
                     merged_option = None
                 merged_options.append(merged_option)
             return any_constraints(list(merged_options), eager)
+
+    # If normal logic didn't work, try excluding trivially unsatisfiable constraint (due to
+    # upper bounds) from each option, and comparing them again.
+    filtered_options = [filter_satisfiable(o) for o in options]
+    if filtered_options != options:
+        return any_constraints(filtered_options, eager=eager)
+
     # Otherwise, there are either no valid options or multiple, inconsistent valid
     # options. Give up and deduce nothing.
     return []
 
 
+def filter_satisfiable(option: list[Constraint] | None) -> list[Constraint] | None:
+    """Keep only constraints that can possibly be satisfied.
+
+    Currently, we filter out constraints where target is not a subtype of the upper bound.
+    Since those can be never satisfied. We may add more cases in future if it improves type
+    inference.
+    """
+    if not option:
+        return option
+    satisfiable = []
+    for c in option:
+        # TODO: add similar logic for TypeVar values (also in various other places)?
+        if mypy.subtypes.is_subtype(c.target, c.origin_type_var.upper_bound):
+            satisfiable.append(c)
+    if not satisfiable:
+        return None
+    return satisfiable
+
+
 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):
@@ -560,9 +588,9 @@ def visit_instance(self, template: Instance) -> list[Constraint]:
                                 suffix.arg_kinds[len(prefix.arg_kinds) :],
                                 suffix.arg_names[len(prefix.arg_names) :],
                             )
-                            res.append(Constraint(mapped_arg.id, SUPERTYPE_OF, suffix))
+                            res.append(Constraint(mapped_arg, SUPERTYPE_OF, suffix))
                         elif isinstance(suffix, ParamSpecType):
-                            res.append(Constraint(mapped_arg.id, SUPERTYPE_OF, suffix))
+                            res.append(Constraint(mapped_arg, SUPERTYPE_OF, suffix))
                     elif isinstance(tvar, TypeVarTupleType):
                         raise NotImplementedError
 
@@ -583,7 +611,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]:
                         if isinstance(template_unpack, TypeVarTupleType):
                             res.append(
                                 Constraint(
-                                    template_unpack.id, SUPERTYPE_OF, TypeList(list(mapped_middle))
+                                    template_unpack, SUPERTYPE_OF, TypeList(list(mapped_middle))
                                 )
                             )
                         elif (
@@ -644,9 +672,9 @@ def visit_instance(self, template: Instance) -> list[Constraint]:
                                 suffix.arg_kinds[len(prefix.arg_kinds) :],
                                 suffix.arg_names[len(prefix.arg_names) :],
                             )
-                            res.append(Constraint(template_arg.id, SUPERTYPE_OF, suffix))
+                            res.append(Constraint(template_arg, SUPERTYPE_OF, suffix))
                         elif isinstance(suffix, ParamSpecType):
-                            res.append(Constraint(template_arg.id, SUPERTYPE_OF, suffix))
+                            res.append(Constraint(template_arg, SUPERTYPE_OF, suffix))
                 return res
             if (
                 template.type.is_protocol
@@ -763,7 +791,7 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]:
                     prefix_len = min(prefix_len, max_prefix_len)
                     res.append(
                         Constraint(
-                            param_spec.id,
+                            param_spec,
                             SUBTYPE_OF,
                             cactual.copy_modified(
                                 arg_types=cactual.arg_types[prefix_len:],
@@ -774,7 +802,7 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]:
                         )
                     )
                 else:
-                    res.append(Constraint(param_spec.id, SUBTYPE_OF, cactual_ps))
+                    res.append(Constraint(param_spec, SUBTYPE_OF, cactual_ps))
 
                 # compare prefixes
                 cactual_prefix = cactual.copy_modified(
@@ -805,7 +833,7 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]:
             else:
                 res = [
                     Constraint(
-                        param_spec.id,
+                        param_spec,
                         SUBTYPE_OF,
                         callable_with_ellipsis(any_type, any_type, template.fallback),
                     )
@@ -877,7 +905,7 @@ def visit_tuple_type(self, template: TupleType) -> list[Constraint]:
                         modified_actual = actual.copy_modified(items=list(actual_items))
                     return [
                         Constraint(
-                            type_var=unpacked_type.id, op=self.direction, target=modified_actual
+                            type_var=unpacked_type, op=self.direction, target=modified_actual
                         )
                     ]
 


Test patch:
diff --git a/mypy/test/testconstraints.py b/mypy/test/testconstraints.py
--- a/mypy/test/testconstraints.py
+++ b/mypy/test/testconstraints.py
@@ -19,7 +19,7 @@ def test_basic_type_variable(self) -> None:
         fx = self.fx
         for direction in [SUBTYPE_OF, SUPERTYPE_OF]:
             assert infer_constraints(fx.gt, fx.ga, direction) == [
-                Constraint(type_var=fx.t.id, op=direction, target=fx.a)
+                Constraint(type_var=fx.t, op=direction, target=fx.a)
             ]
 
     @pytest.mark.xfail
@@ -27,13 +27,13 @@ def test_basic_type_var_tuple_subtype(self) -> None:
         fx = self.fx
         assert infer_constraints(
             Instance(fx.gvi, [UnpackType(fx.ts)]), Instance(fx.gvi, [fx.a, fx.b]), SUBTYPE_OF
-        ) == [Constraint(type_var=fx.ts.id, op=SUBTYPE_OF, target=TypeList([fx.a, fx.b]))]
+        ) == [Constraint(type_var=fx.ts, op=SUBTYPE_OF, target=TypeList([fx.a, fx.b]))]
 
     def test_basic_type_var_tuple(self) -> None:
         fx = self.fx
         assert infer_constraints(
             Instance(fx.gvi, [UnpackType(fx.ts)]), Instance(fx.gvi, [fx.a, fx.b]), SUPERTYPE_OF
-        ) == [Constraint(type_var=fx.ts.id, op=SUPERTYPE_OF, target=TypeList([fx.a, fx.b]))]
+        ) == [Constraint(type_var=fx.ts, op=SUPERTYPE_OF, target=TypeList([fx.a, fx.b]))]
 
     def test_type_var_tuple_with_prefix_and_suffix(self) -> None:
         fx = self.fx
@@ -44,7 +44,7 @@ def test_type_var_tuple_with_prefix_and_suffix(self) -> None:
                 SUPERTYPE_OF,
             )
         ) == {
-            Constraint(type_var=fx.t.id, op=SUPERTYPE_OF, target=fx.a),
-            Constraint(type_var=fx.ts.id, op=SUPERTYPE_OF, target=TypeList([fx.b, fx.c])),
-            Constraint(type_var=fx.s.id, op=SUPERTYPE_OF, target=fx.d),
+            Constraint(type_var=fx.t, op=SUPERTYPE_OF, target=fx.a),
+            Constraint(type_var=fx.ts, op=SUPERTYPE_OF, target=TypeList([fx.b, fx.c])),
+            Constraint(type_var=fx.s, op=SUPERTYPE_OF, target=fx.d),
         }
diff --git a/mypy/test/testsolve.py b/mypy/test/testsolve.py
--- a/mypy/test/testsolve.py
+++ b/mypy/test/testsolve.py
@@ -138,7 +138,7 @@ def assert_solve(
         assert_equal(str(actual), str(res))
 
     def supc(self, type_var: TypeVarType, bound: Type) -> Constraint:
-        return Constraint(type_var.id, SUPERTYPE_OF, bound)
+        return Constraint(type_var, SUPERTYPE_OF, bound)
 
     def subc(self, type_var: TypeVarType, bound: Type) -> Constraint:
-        return Constraint(type_var.id, SUBTYPE_OF, bound)
+        return Constraint(type_var, SUBTYPE_OF, bound)
diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test
--- a/test-data/unit/check-functions.test
+++ b/test-data/unit/check-functions.test
@@ -2674,3 +2674,13 @@ class A:
     def h(self, *args, **kwargs) -> int: pass   # OK
 [builtins fixtures/property.pyi]
 [out]
+
+[case testSubtypingUnionGenericBounds]
+from typing import Callable, TypeVar, Union, Sequence
+
+TI = TypeVar("TI", bound=int)
+TS = TypeVar("TS", bound=str)
+
+f: Callable[[Sequence[TI]], None]
+g: Callable[[Union[Sequence[TI], Sequence[TS]]], None]
+f = g
diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test
--- a/test-data/unit/check-overloading.test
+++ b/test-data/unit/check-overloading.test
@@ -6493,3 +6493,20 @@ def foo(x: List[T]) -> str: ...
 @overload
 def foo(x: Sequence[int]) -> int: ...
 [builtins fixtures/list.pyi]
+
+[case testOverloadUnionGenericBounds]
+from typing import overload, TypeVar, Sequence, Union
+
+class Entity: ...
+class Assoc: ...
+
+E = TypeVar("E", bound=Entity)
+A = TypeVar("A", bound=Assoc)
+
+class Test:
+    @overload
+    def foo(self, arg: Sequence[E]) -> None: ...
+    @overload
+    def foo(self, arg: Sequence[A]) -> None: ...
+    def foo(self, arg: Union[Sequence[E], Sequence[A]]) -> None:
+        ...