diff --git a/mypy/meet.py b/mypy/meet.py index bfc6d88a1e209..6f48b8f186232 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -348,6 +348,23 @@ def are_related_types( ) +def _type_form_item_can_be_union(item: Type) -> bool: + """Can a value of TypeForm[item] be an instance of types.UnionType? + + A type expression is a types.UnionType instance at runtime exactly when it + is a union expression such as `int | str`. + """ + item = get_proper_type(item) + if isinstance(item, (AnyType, UnionType)): + return True + if isinstance(item, TypeVarType): + if item.values: + return any(_type_form_item_can_be_union(v) for v in item.values) + # An unconstrained type variable can be bound to a union. + return True + return False + + def is_overlapping_types( left: Type, right: Type, @@ -522,6 +539,18 @@ def _type_object_overlap(left: Type, right: Type) -> bool: # 1. Type[C] vs Callable[..., C] overlap even if the latter is not class object. if isinstance(left, TypeType) and isinstance(right, CallableType): return _is_overlapping_types(left.item, right.ret_type) + # 2b. TypeForm[T] vs types.UnionType: a type expression like `int | str` + # is an instance of types.UnionType at runtime, so e.g. + # `isinstance(t, UnionType)` where `t: TypeForm[Any]` can be true. + # (Checked before case 2 below, which would otherwise return False.) + if ( + isinstance(left, TypeType) + and left.is_type_form + and isinstance(right, Instance) + and right.type.fullname == "types.UnionType" + and _type_form_item_can_be_union(left.item) + ): + return True # 2. Type[C] vs Meta, where Meta is a metaclass for C. if isinstance(left, TypeType) and isinstance(right, Instance): if isinstance(left.item, Instance): diff --git a/test-data/unit/check-typeform.test b/test-data/unit/check-typeform.test index d08826e233f76..826d9c412a00e 100644 --- a/test-data/unit/check-typeform.test +++ b/test-data/unit/check-typeform.test @@ -808,3 +808,28 @@ from typing import Optional, TypeForm typx: TypeForm = Optional # E: Incompatible types in assignment (expression has type "int", variable has type "TypeForm[Any]") [builtins fixtures/primitives.pyi] [typing fixtures/typing-full.pyi] + + +-- isinstance() with types.UnionType and TypeForm (see #21910) + +[case testTypeFormIsinstanceUnionType] +# flags: --warn-unreachable +from types import UnionType +from typing import Any +from typing_extensions import TypeForm + +def f(t: TypeForm[Any]) -> None: + if isinstance(t, UnionType): + reveal_type(t) # N: Revealed type is "types.UnionType" + else: + reveal_type(t) # N: Revealed type is "TypeForm[Any]" + +def g(t: TypeForm[int | str]) -> None: + if isinstance(t, UnionType): + reveal_type(t) # N: Revealed type is "types.UnionType" + +def h(t: TypeForm[int]) -> None: + if isinstance(t, UnionType): + pass # E: Statement is unreachable +[builtins fixtures/isinstance_python3_10.pyi] +[typing fixtures/typing-full.pyi]