Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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[<subclass of
"A" and "M">]`, 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,
Expand Down
58 changes: 58 additions & 0 deletions test-data/unit/check-typeis.test
Original file line number Diff line number Diff line change
Expand Up @@ -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__.<subclass of "__main__.A" and "__main__.M">]"
[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]
Loading