diff --git a/mypy/checker.py b/mypy/checker.py index d13b927b28f2..859791188afe 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6662,15 +6662,7 @@ def find_isinstance_check_helper( return {expr: TypeGuardedType(type_guard)}, {} else: assert type_is is not None - return conditional_types_to_typemaps( - expr, - *self.conditional_types_with_intersection( - self.lookup_type(expr), - [TypeRange(type_is, is_upper_bound=False)], - expr, - consider_runtime_isinstance=False, - ), - ) + return self.conditional_types_for_type_is(expr, type_is) elif isinstance(node, ComparisonExpr): return self.comparison_type_narrowing_helper(node) elif isinstance(node, AssignmentExpr): @@ -8168,6 +8160,47 @@ def infer_issubclass_maps(self, node: CallExpr, expr: Expression) -> tuple[TypeM yes_map, no_map = map(convert_to_typetype, (yes_map, no_map)) return yes_map, no_map + def conditional_types_for_type_is( + self, expr: Expression, type_is: Type + ) -> tuple[TypeMap, TypeMap]: + """Infer type restrictions for an expression narrowed by a TypeIs guard. + + This mirrors infer_issubclass_maps: when both the current type of + `expr` and the guarded `type_is` are `type[...]` objects (as opposed + to plain instances), we must unwrap the `TypeType` layer before + attempting to compute an ad-hoc intersection, then rewrap the + result. Without this, a guard such as `TypeIs[type[M]]` applied to + an expression of type `type[A]` never reaches the ad-hoc + intersection logic in `conditional_types_with_intersection` (which + only handles plain `Instance`/`NoneType` operands), and the checked + type silently collapses to `Never` instead of `type[]`, the same result `issubclass()` narrowing produces. + """ + vartype = self.lookup_type(expr) + if isinstance(vartype, TypeVarType): + vartype = vartype.upper_bound + vartype = get_proper_type(vartype) + proper_type_is = get_proper_type(type_is) + if isinstance(vartype, TypeType) and isinstance(proper_type_is, TypeType): + yes_type, no_type = self.conditional_types_with_intersection( + vartype.item, + [TypeRange(proper_type_is.item, is_upper_bound=False)], + expr, + consider_runtime_isinstance=False, + ) + yes_map, no_map = conditional_types_to_typemaps(expr, yes_type, no_type) + yes_map, no_map = map(convert_to_typetype, (yes_map, no_map)) + return yes_map, no_map + return conditional_types_to_typemaps( + expr, + *self.conditional_types_with_intersection( + vartype, + [TypeRange(type_is, is_upper_bound=False)], + expr, + consider_runtime_isinstance=False, + ), + ) + @overload def conditional_types_with_intersection( self, diff --git a/test-data/unit/check-typeis.test b/test-data/unit/check-typeis.test index 65ee837452f5..19fb10bab86a 100644 --- a/test-data/unit/check-typeis.test +++ b/test-data/unit/check-typeis.test @@ -997,3 +997,61 @@ def main(f: Callable[[], int | Awaitable[int]] | Callable[[], Awaitable[int]]) - reveal_type(f) # N: Revealed type is "def () -> builtins.int | typing.Awaitable[builtins.int]" [builtins fixtures/tuple.pyi] + +[case testTypeIsNarrowsTypeObject] +# https://github.com/python/mypy/issues/21677 +from typing_extensions import TypeIs + +class A: ... +class M: ... +class B(A): ... +class C(M, A): ... + +def is_m(t: type) -> TypeIs[type[M]]: + return issubclass(t, M) + +cls: type[A] = C +if is_m(cls): + reveal_type(cls) # N: Revealed type is "type[__main__.]" +[builtins fixtures/isinstancelist.pyi] + +[case testTypeIsNarrowsTypeObjectInComprehension] +# https://github.com/python/mypy/issues/21677 +from typing import Type +from typing_extensions import TypeIs + +class A: ... +class M: ... +class B(A): ... +class C(M, A): ... + +def is_m(t: type) -> TypeIs[Type[M]]: + return issubclass(t, M) + +alist: list[Type[A]] = [B, C] +mlist: list[Type[M]] = [cls for cls in alist if is_m(cls)] +reveal_type(mlist) # N: Revealed type is "builtins.list[type[__main__.M]]" +[builtins fixtures/isinstancelist.pyi] + +[case testTypeIsNarrowsTypeObjectInComprehensionDataclass] +# https://github.com/python/mypy/issues/21677 +from dataclasses import dataclass +from typing import Type +from typing_extensions import TypeIs + +@dataclass +class A: ... +@dataclass +class M: ... +@dataclass +class B(A): ... +@dataclass +class C(M, A): ... + +def is_m(t: type) -> TypeIs[Type[M]]: + return issubclass(t, M) + +alist: list[Type[A]] = [B, C] +mlist: list[Type[M]] = [cls for cls in alist if is_m(cls)] +reveal_type(mlist) # N: Revealed type is "builtins.list[type[__main__.M]]" +[builtins fixtures/isinstancelist.pyi]