Skip to content

Commit b747c02

Browse files
committed
Unwrap type[Union[...]] when solving typevar constraints
1 parent ec4ccb0 commit b747c02

File tree

2 files changed

+69
-1
lines changed

2 files changed

+69
-1
lines changed

mypy/constraints.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Final, Iterable, List, Sequence
5+
from typing import TYPE_CHECKING, Final, Iterable, List, Sequence, cast
6+
from typing_extensions import TypeGuard
67

78
import mypy.subtypes
89
import mypy.typeops
@@ -339,6 +340,26 @@ def _infer_constraints(
339340
if isinstance(actual, AnyType) and actual.type_of_any == TypeOfAny.suggestion_engine:
340341
return []
341342

343+
# type[A | B] is always represented as type[A] | type[B] internally.
344+
# This makes our constraint solver choke on type[T] <: type[A] | type[B],
345+
# solving T as generic meet(A, B) which is often `object`. Force unwrap such unions
346+
# if both sides are type[...] or unions thereof. See `testTypeVarType` test
347+
def _is_type_type(tp: ProperType) -> TypeGuard[TypeType | UnionType]:
348+
return (
349+
isinstance(tp, TypeType)
350+
or isinstance(tp, UnionType)
351+
and all(isinstance(get_proper_type(o), TypeType) for o in tp.items)
352+
)
353+
354+
def _unwrap_type_type(tp: TypeType | UnionType) -> ProperType:
355+
if isinstance(tp, TypeType):
356+
return tp.item
357+
return UnionType.make_union([cast(TypeType, o).item for o in tp.items])
358+
359+
if _is_type_type(template) and _is_type_type(actual):
360+
template = _unwrap_type_type(template)
361+
actual = _unwrap_type_type(actual)
362+
342363
# If the template is simply a type variable, emit a Constraint directly.
343364
# We need to handle this case before handling Unions for two reasons:
344365
# 1. "T <: Union[U1, U2]" is not equivalent to "T <: U1 or T <: U2",

test-data/unit/check-typevar-unbound.test

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,50 @@ from typing import TypeVar
6969
T = TypeVar("T")
7070
def f(t: T) -> None:
7171
a, *b = t # E: "object" object is not iterable
72+
73+
[case testTypeVarType]
74+
from typing import Mapping, Type, TypeVar, Union
75+
T = TypeVar("T")
76+
77+
class A: ...
78+
class B: ...
79+
80+
lookup_table: Mapping[str, Type[Union[A,B]]]
81+
def load(lookup_table: Mapping[str, Type[T]], lookup_key: str) -> T:
82+
...
83+
reveal_type(load(lookup_table, "a")) # N: Revealed type is "Union[__main__.A, __main__.B]"
84+
85+
lookup_table_a: Mapping[str, Type[A]]
86+
def load2(lookup_table: Mapping[str, Type[Union[T, int]]], lookup_key: str) -> T:
87+
...
88+
reveal_type(load2(lookup_table_a, "a")) # N: Revealed type is "__main__.A"
89+
90+
[builtins fixtures/tuple.pyi]
91+
92+
[case testTypeVarTypeAssignment]
93+
# Adapted from https://github.com/python/mypy/issues/12115
94+
from typing import TypeVar, Type, Callable, Union, Any
95+
96+
t1: Type[bool] = bool
97+
t2: Union[Type[bool], Type[str]] = bool
98+
99+
T1 = TypeVar("T1", bound=Union[bool, str])
100+
def foo1(t: Type[T1]) -> None: ...
101+
foo1(t1)
102+
foo1(t2)
103+
104+
T2 = TypeVar("T2", bool, str)
105+
def foo2(t: Type[T2]) -> None: ...
106+
foo2(t1)
107+
# Rejected correctly: T2 cannot be Union[bool, str]
108+
foo2(t2) # E: Value of type variable "T2" of "foo2" cannot be "Union[bool, str]"
109+
110+
T3 = TypeVar("T3")
111+
def foo3(t: Type[T3]) -> None: ...
112+
foo3(t1)
113+
foo3(t2)
114+
115+
def foo4(t: Type[Union[bool, str]]) -> None: ...
116+
foo4(t1)
117+
foo4(t2)
118+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)