Skip to content

Commit 469b4e4

Browse files
authored
Unwrap type[Union[...]] when solving typevar constraints (#18266)
Closes #18265, closes #12115. `type[A | B]` is internally represented as `type[A] | type[B]`, and this causes problems for a typevar solver. Prevent using meet in such cases by unwraping `type[...]` if both sides have such shape.
1 parent ee1f4c9 commit 469b4e4

File tree

2 files changed

+84
-1
lines changed

2 files changed

+84
-1
lines changed

mypy/constraints.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from __future__ import annotations
44

55
from collections.abc import Iterable, Sequence
6-
from typing import TYPE_CHECKING, Final
6+
from typing import TYPE_CHECKING, Final, cast
7+
from typing_extensions import TypeGuard
78

89
import mypy.subtypes
910
import mypy.typeops
@@ -340,6 +341,16 @@ def _infer_constraints(
340341
if isinstance(actual, AnyType) and actual.type_of_any == TypeOfAny.suggestion_engine:
341342
return []
342343

344+
# type[A | B] is always represented as type[A] | type[B] internally.
345+
# This makes our constraint solver choke on type[T] <: type[A] | type[B],
346+
# solving T as generic meet(A, B) which is often `object`. Force unwrap such unions
347+
# if both sides are type[...] or unions thereof. See `testTypeVarType` test
348+
type_type_unwrapped = False
349+
if _is_type_type(template) and _is_type_type(actual):
350+
type_type_unwrapped = True
351+
template = _unwrap_type_type(template)
352+
actual = _unwrap_type_type(actual)
353+
343354
# If the template is simply a type variable, emit a Constraint directly.
344355
# We need to handle this case before handling Unions for two reasons:
345356
# 1. "T <: Union[U1, U2]" is not equivalent to "T <: U1 or T <: U2",
@@ -373,6 +384,11 @@ def _infer_constraints(
373384
if direction == SUPERTYPE_OF and isinstance(actual, UnionType):
374385
res = []
375386
for a_item in actual.items:
387+
# `orig_template` has to be preserved intact in case it's recursive.
388+
# If we unwraped ``type[...]`` previously, wrap the item back again,
389+
# as ``type[...]`` can't be removed from `orig_template`.
390+
if type_type_unwrapped:
391+
a_item = TypeType.make_normalized(a_item)
376392
res.extend(infer_constraints(orig_template, a_item, direction))
377393
return res
378394

@@ -411,6 +427,26 @@ def _infer_constraints(
411427
return template.accept(ConstraintBuilderVisitor(actual, direction, skip_neg_op))
412428

413429

430+
def _is_type_type(tp: ProperType) -> TypeGuard[TypeType | UnionType]:
431+
"""Is ``tp`` a ``type[...]`` or a union thereof?
432+
433+
``Type[A | B]`` is internally represented as ``type[A] | type[B]``, and this
434+
troubles the solver sometimes.
435+
"""
436+
return (
437+
isinstance(tp, TypeType)
438+
or isinstance(tp, UnionType)
439+
and all(isinstance(get_proper_type(o), TypeType) for o in tp.items)
440+
)
441+
442+
443+
def _unwrap_type_type(tp: TypeType | UnionType) -> ProperType:
444+
"""Extract the inner type from ``type[...]`` expression or a union thereof."""
445+
if isinstance(tp, TypeType):
446+
return tp.item
447+
return UnionType.make_union([cast(TypeType, get_proper_type(o)).item for o in tp.items])
448+
449+
414450
def infer_constraints_if_possible(
415451
template: Type, actual: Type, direction: int
416452
) -> list[Constraint] | None:

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)