Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix TypeIs negative narrowing of union of generics #18193

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
44 changes: 38 additions & 6 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6009,6 +6009,7 @@ def find_isinstance_check_helper(
self.lookup_type(expr),
[TypeRange(node.callee.type_is, is_upper_bound=False)],
expr,
consider_runtime_isinstance=False,
),
)
elif isinstance(node, ComparisonExpr):
Expand Down Expand Up @@ -7419,11 +7420,19 @@ def conditional_types_with_intersection(
type_ranges: list[TypeRange] | None,
ctx: Context,
default: None = None,
*,
consider_runtime_isinstance: bool = True,
) -> tuple[Type | None, Type | None]: ...

@overload
def conditional_types_with_intersection(
self, expr_type: Type, type_ranges: list[TypeRange] | None, ctx: Context, default: Type
self,
expr_type: Type,
type_ranges: list[TypeRange] | None,
ctx: Context,
default: Type,
*,
consider_runtime_isinstance: bool = True,
) -> tuple[Type, Type]: ...

def conditional_types_with_intersection(
Expand All @@ -7432,8 +7441,15 @@ def conditional_types_with_intersection(
type_ranges: list[TypeRange] | None,
ctx: Context,
default: Type | None = None,
*,
consider_runtime_isinstance: bool = True,
) -> tuple[Type | None, Type | None]:
initial_types = conditional_types(expr_type, type_ranges, default)
initial_types = conditional_types(
expr_type,
type_ranges,
default,
consider_runtime_isinstance=consider_runtime_isinstance,
)
# For some reason, doing "yes_map, no_map = conditional_types_to_typemaps(...)"
# doesn't work: mypyc will decide that 'yes_map' is of type None if we try.
yes_type: Type | None = initial_types[0]
Expand Down Expand Up @@ -7712,18 +7728,30 @@ def visit_type_var(self, t: TypeVarType) -> None:

@overload
def conditional_types(
current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: None = None
current_type: Type,
proposed_type_ranges: list[TypeRange] | None,
default: None = None,
*,
consider_runtime_isinstance: bool = True,
) -> tuple[Type | None, Type | None]: ...


@overload
def conditional_types(
current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: Type
current_type: Type,
proposed_type_ranges: list[TypeRange] | None,
default: Type,
*,
consider_runtime_isinstance: bool = True,
) -> tuple[Type, Type]: ...


def conditional_types(
current_type: Type, proposed_type_ranges: list[TypeRange] | None, default: Type | None = None
current_type: Type,
proposed_type_ranges: list[TypeRange] | None,
default: Type | None = None,
*,
consider_runtime_isinstance: bool = True,
) -> tuple[Type | None, Type | None]:
"""Takes in the current type and a proposed type of an expression.

Expand Down Expand Up @@ -7765,7 +7793,11 @@ def conditional_types(
if not type_range.is_upper_bound
]
)
remaining_type = restrict_subtype_away(current_type, proposed_precise_type)
remaining_type = restrict_subtype_away(
current_type,
proposed_precise_type,
consider_runtime_isinstance=consider_runtime_isinstance,
)
return proposed_type, remaining_type
else:
# An isinstance check, but we don't understand the type
Expand Down
17 changes: 12 additions & 5 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1925,7 +1925,7 @@ def try_restrict_literal_union(t: UnionType, s: Type) -> list[Type] | None:
return new_items


def restrict_subtype_away(t: Type, s: Type) -> Type:
def restrict_subtype_away(t: Type, s: Type, *, consider_runtime_isinstance: bool = True) -> Type:
"""Return t minus s for runtime type assertions.

If we can't determine a precise result, return a supertype of the
Expand All @@ -1939,14 +1939,21 @@ def restrict_subtype_away(t: Type, s: Type) -> Type:
new_items = try_restrict_literal_union(p_t, s)
if new_items is None:
new_items = [
restrict_subtype_away(item, s)
restrict_subtype_away(
item, s, consider_runtime_isinstance=consider_runtime_isinstance
)
for item in p_t.relevant_items()
if (isinstance(get_proper_type(item), AnyType) or not covers_at_runtime(item, s))
]
return UnionType.make_union(new_items)
return UnionType.make_union(
[item for item in new_items if not isinstance(get_proper_type(item), UninhabitedType)]
)
elif isinstance(p_t, TypeVarType):
return p_t.copy_modified(upper_bound=restrict_subtype_away(p_t.upper_bound, s))
elif covers_at_runtime(t, s):
elif consider_runtime_isinstance and covers_at_runtime(t, s):
return UninhabitedType()
elif is_proper_subtype(t, s, ignore_promotions=True):
return UninhabitedType()
elif is_proper_subtype(t, s, ignore_promotions=True, erase_instances=True):
return UninhabitedType()
else:
return t
Expand Down
37 changes: 37 additions & 0 deletions test-data/unit/check-typeis.test
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,43 @@ def main(a: object) -> None:
reveal_type(a) # N: Revealed type is "Union[builtins.int, builtins.str]"
[builtins fixtures/tuple.pyi]

[case testTypeIsUnionWithGeneric]
from typing import Any, List, Sequence, Union
from typing_extensions import TypeIs

def is_int_list(a: object) -> TypeIs[List[int]]: pass
def is_int_seq(a: object) -> TypeIs[Sequence[int]]: pass
def is_seq(a: object) -> TypeIs[Sequence[Any]]: pass

def f1(a: Union[List[int], List[str]]) -> None:
if is_int_list(a):
reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]"
else:
reveal_type(a) # N: Revealed type is "builtins.list[builtins.str]"
reveal_type(a) # N: Revealed type is "Union[builtins.list[builtins.int], builtins.list[builtins.str]]"

def f2(a: Union[List[int], int]) -> None:
if is_int_list(a):
reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]"
else:
reveal_type(a) # N: Revealed type is "builtins.int"
reveal_type(a) # N: Revealed type is "Union[builtins.list[builtins.int], builtins.int]"

def f3(a: Union[List[bool], List[str]]) -> None:
if is_int_seq(a):
reveal_type(a) # N: Revealed type is "builtins.list[builtins.bool]"
else:
reveal_type(a) # N: Revealed type is "builtins.list[builtins.str]"
reveal_type(a) # N: Revealed type is "Union[builtins.list[builtins.bool], builtins.list[builtins.str]]"

def f4(a: Union[List[int], int]) -> None:
if is_seq(a):
reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]"
else:
reveal_type(a) # N: Revealed type is "builtins.int"
reveal_type(a) # N: Revealed type is "Union[builtins.list[builtins.int], builtins.int]"
[builtins fixtures/tuple.pyi]

[case testTypeIsNonzeroFloat]
from typing_extensions import TypeIs
def is_nonzero(a: object) -> TypeIs[float]: pass
Expand Down
Loading