From b747c027ca9620804da9b492db5711e964d488fd Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 8 Dec 2024 03:08:37 +0100 Subject: [PATCH 1/4] Unwrap type[Union[...]] when solving typevar constraints --- mypy/constraints.py | 23 ++++++++++- test-data/unit/check-typevar-unbound.test | 47 +++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 5c815bf2af65..80368c551bc3 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final, Iterable, List, Sequence +from typing import TYPE_CHECKING, Final, Iterable, List, Sequence, cast +from typing_extensions import TypeGuard import mypy.subtypes import mypy.typeops @@ -339,6 +340,26 @@ def _infer_constraints( if isinstance(actual, AnyType) and actual.type_of_any == TypeOfAny.suggestion_engine: return [] + # type[A | B] is always represented as type[A] | type[B] internally. + # This makes our constraint solver choke on type[T] <: type[A] | type[B], + # solving T as generic meet(A, B) which is often `object`. Force unwrap such unions + # if both sides are type[...] or unions thereof. See `testTypeVarType` test + def _is_type_type(tp: ProperType) -> TypeGuard[TypeType | UnionType]: + return ( + isinstance(tp, TypeType) + or isinstance(tp, UnionType) + and all(isinstance(get_proper_type(o), TypeType) for o in tp.items) + ) + + def _unwrap_type_type(tp: TypeType | UnionType) -> ProperType: + if isinstance(tp, TypeType): + return tp.item + return UnionType.make_union([cast(TypeType, o).item for o in tp.items]) + + if _is_type_type(template) and _is_type_type(actual): + template = _unwrap_type_type(template) + actual = _unwrap_type_type(actual) + # If the template is simply a type variable, emit a Constraint directly. # We need to handle this case before handling Unions for two reasons: # 1. "T <: Union[U1, U2]" is not equivalent to "T <: U1 or T <: U2", diff --git a/test-data/unit/check-typevar-unbound.test b/test-data/unit/check-typevar-unbound.test index ed6beaa100db..587ae6577328 100644 --- a/test-data/unit/check-typevar-unbound.test +++ b/test-data/unit/check-typevar-unbound.test @@ -69,3 +69,50 @@ from typing import TypeVar T = TypeVar("T") def f(t: T) -> None: a, *b = t # E: "object" object is not iterable + +[case testTypeVarType] +from typing import Mapping, Type, TypeVar, Union +T = TypeVar("T") + +class A: ... +class B: ... + +lookup_table: Mapping[str, Type[Union[A,B]]] +def load(lookup_table: Mapping[str, Type[T]], lookup_key: str) -> T: + ... +reveal_type(load(lookup_table, "a")) # N: Revealed type is "Union[__main__.A, __main__.B]" + +lookup_table_a: Mapping[str, Type[A]] +def load2(lookup_table: Mapping[str, Type[Union[T, int]]], lookup_key: str) -> T: + ... +reveal_type(load2(lookup_table_a, "a")) # N: Revealed type is "__main__.A" + +[builtins fixtures/tuple.pyi] + +[case testTypeVarTypeAssignment] +# Adapted from https://github.com/python/mypy/issues/12115 +from typing import TypeVar, Type, Callable, Union, Any + +t1: Type[bool] = bool +t2: Union[Type[bool], Type[str]] = bool + +T1 = TypeVar("T1", bound=Union[bool, str]) +def foo1(t: Type[T1]) -> None: ... +foo1(t1) +foo1(t2) + +T2 = TypeVar("T2", bool, str) +def foo2(t: Type[T2]) -> None: ... +foo2(t1) +# Rejected correctly: T2 cannot be Union[bool, str] +foo2(t2) # E: Value of type variable "T2" of "foo2" cannot be "Union[bool, str]" + +T3 = TypeVar("T3") +def foo3(t: Type[T3]) -> None: ... +foo3(t1) +foo3(t2) + +def foo4(t: Type[Union[bool, str]]) -> None: ... +foo4(t1) +foo4(t2) +[builtins fixtures/tuple.pyi] From fce2baf128da563e443c52883c0a0c085ea66d9f Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 20 Dec 2024 18:02:04 +0100 Subject: [PATCH 2/4] Extract functions to module level, add lost `get_proper_type` --- mypy/constraints.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 80368c551bc3..562499afd0c2 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -344,18 +344,6 @@ def _infer_constraints( # This makes our constraint solver choke on type[T] <: type[A] | type[B], # solving T as generic meet(A, B) which is often `object`. Force unwrap such unions # if both sides are type[...] or unions thereof. See `testTypeVarType` test - def _is_type_type(tp: ProperType) -> TypeGuard[TypeType | UnionType]: - return ( - isinstance(tp, TypeType) - or isinstance(tp, UnionType) - and all(isinstance(get_proper_type(o), TypeType) for o in tp.items) - ) - - def _unwrap_type_type(tp: TypeType | UnionType) -> ProperType: - if isinstance(tp, TypeType): - return tp.item - return UnionType.make_union([cast(TypeType, o).item for o in tp.items]) - if _is_type_type(template) and _is_type_type(actual): template = _unwrap_type_type(template) actual = _unwrap_type_type(actual) @@ -431,6 +419,30 @@ def _unwrap_type_type(tp: TypeType | UnionType) -> ProperType: return template.accept(ConstraintBuilderVisitor(actual, direction, skip_neg_op)) +def _is_type_type(tp: ProperType) -> TypeGuard[TypeType | UnionType]: + """Is ``tp`` a type[...] or union thereof? + + Type[A | B] is internally represented as type[A] | type[B], and this troubles + the solver sometimes. + """ + return ( + isinstance(tp, TypeType) + or isinstance(tp, UnionType) + and all(isinstance(get_proper_type(o), TypeType) for o in tp.items) + ) + + +def _unwrap_type_type(tp: TypeType | UnionType) -> ProperType: + """Rewrite `type[A] | type[B]` as `type[A | B]`. + + This is an opposite of normalized form used elsewhere, necessary to solve type[...] + constraints on typevars. + """ + if isinstance(tp, TypeType): + return tp.item + return UnionType.make_union([cast(TypeType, get_proper_type(o)).item for o in tp.items]) + + def infer_constraints_if_possible( template: Type, actual: Type, direction: int ) -> list[Constraint] | None: From 8ad301a3fdfcbc2a60af1d307bc7a33f57e8426c Mon Sep 17 00:00:00 2001 From: STerliakov Date: Thu, 26 Dec 2024 02:42:09 +0100 Subject: [PATCH 3/4] Fix a bug discovered by primer: `orig_template` is not and should not be unwrapped, so type should be restored when passing it further --- mypy/constraints.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mypy/constraints.py b/mypy/constraints.py index 562499afd0c2..59e31f5c2c48 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -344,7 +344,9 @@ def _infer_constraints( # This makes our constraint solver choke on type[T] <: type[A] | type[B], # solving T as generic meet(A, B) which is often `object`. Force unwrap such unions # if both sides are type[...] or unions thereof. See `testTypeVarType` test + type_type_unwrapped = False if _is_type_type(template) and _is_type_type(actual): + type_type_unwrapped = True template = _unwrap_type_type(template) actual = _unwrap_type_type(actual) @@ -381,6 +383,11 @@ def _infer_constraints( if direction == SUPERTYPE_OF and isinstance(actual, UnionType): res = [] for a_item in actual.items: + # `orig_template` has to be preserved intact in case it's recursive. + # If we unwraped ``type[...]`` previously, wrap the item back again, + # as ``type[...]`` can't be removed from `orig_template`. + if type_type_unwrapped: + a_item = TypeType.make_normalized(a_item) res.extend(infer_constraints(orig_template, a_item, direction)) return res From 24d6ea5ad3cfd8791fc9344443376b946db28e63 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 10 Jan 2025 19:29:39 +0100 Subject: [PATCH 4/4] Fix wrong docstring, use code fences consistently --- mypy/constraints.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 9ec2f3f3a382..45a96b993563 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -428,10 +428,10 @@ def _infer_constraints( def _is_type_type(tp: ProperType) -> TypeGuard[TypeType | UnionType]: - """Is ``tp`` a type[...] or union thereof? + """Is ``tp`` a ``type[...]`` or a union thereof? - Type[A | B] is internally represented as type[A] | type[B], and this troubles - the solver sometimes. + ``Type[A | B]`` is internally represented as ``type[A] | type[B]``, and this + troubles the solver sometimes. """ return ( isinstance(tp, TypeType) @@ -441,11 +441,7 @@ def _is_type_type(tp: ProperType) -> TypeGuard[TypeType | UnionType]: def _unwrap_type_type(tp: TypeType | UnionType) -> ProperType: - """Rewrite `type[A] | type[B]` as `type[A | B]`. - - This is an opposite of normalized form used elsewhere, necessary to solve type[...] - constraints on typevars. - """ + """Extract the inner type from ``type[...]`` expression or a union thereof.""" if isinstance(tp, TypeType): return tp.item return UnionType.make_union([cast(TypeType, get_proper_type(o)).item for o in tp.items])