Skip to content

Commit 3cebc97

Browse files
authored
Use type(x) == T for type narrowing (#10284)
* Use type(x) == T for type narrowing This makes mypy use expressions like type(some_expression) == some_type for type narrowing similar to how it already does for isinstance checks. This also adds some tests to make sure that this is actually being used in type narrowing. * Avoid type narrowing in the else case `type(x) == T` is False when x is an instance of a subclass of T, which isn't the same as `isinstance(x, T)`, which checks if x is an instance of T or a subclass of T. That means that x could be a subclass of T in the else case, so we can't narrow x's type to exclude this possibility. * Move check for type(x) == T to new function Refactor code for narrowing types based on checks that look like type(x) == T into a new function * Don't narrow if multiple types found Avoid narrowing in a comparison with multiple types being compared to each other even if there is a type(x) call being compared to one of them. * Avoid narrowing if no type calls found Return early if we haven't found any calls to type * Fix type signature and documentation * Add type is not equal tests Add tests to make sure that type(x) != T and type(x) is not T work as expected (the same as type(x) == T and type(x) is T but with the if and else branches switched. Currently the type(x) != T check is failing because of a mypy error about an unsupported left operand type. * Fix "Unsupported left operand type" error in tests Add fixtures to some of the tests to make mypy not show an error when we try to compare types * Narrow types in else case if type is final Final types cannot be subclassed, so it's impossible for a subclass of a final type to be used in the else case of a comparison of type(x) to a final type. That means we can narrow types in the else case the same way we would do for isinstance checks if type(x) is being compared to a final type.
1 parent b049e6a commit 3cebc97

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed

mypy/checker.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3954,6 +3954,81 @@ def conditional_callable_type_map(self, expr: Expression,
39543954

39553955
return None, {}
39563956

3957+
def find_type_equals_check(self, node: ComparisonExpr, expr_indices: List[int]
3958+
) -> Tuple[TypeMap, TypeMap]:
3959+
"""Narrow types based on any checks of the type ``type(x) == T``
3960+
3961+
:param node: The node that might contain the comparison
3962+
3963+
:param expr_indices: The list of indices of expressions in ``node`` that are being compared
3964+
"""
3965+
type_map = self.type_map
3966+
3967+
def is_type_call(expr: CallExpr) -> bool:
3968+
"""Is expr a call to type with one argument?"""
3969+
return (refers_to_fullname(expr.callee, 'builtins.type')
3970+
and len(expr.args) == 1)
3971+
# exprs that are being passed into type
3972+
exprs_in_type_calls = [] # type: List[Expression]
3973+
# type that is being compared to type(expr)
3974+
type_being_compared = None # type: Optional[List[TypeRange]]
3975+
# whether the type being compared to is final
3976+
is_final = False
3977+
3978+
for index in expr_indices:
3979+
expr = node.operands[index]
3980+
3981+
if isinstance(expr, CallExpr) and is_type_call(expr):
3982+
exprs_in_type_calls.append(expr.args[0])
3983+
else:
3984+
current_type = get_isinstance_type(expr, type_map)
3985+
if current_type is None:
3986+
continue
3987+
if type_being_compared is not None:
3988+
# It doesn't really make sense to have several types being
3989+
# compared to the output of type (like type(x) == int == str)
3990+
# because whether that's true is solely dependent on what the
3991+
# types being compared are, so we don't try to narrow types any
3992+
# further because we can't really get any information about the
3993+
# type of x from that check
3994+
return {}, {}
3995+
else:
3996+
if isinstance(expr, RefExpr) and isinstance(expr.node, TypeInfo):
3997+
is_final = expr.node.is_final
3998+
type_being_compared = current_type
3999+
4000+
if not exprs_in_type_calls:
4001+
return {}, {}
4002+
4003+
if_maps = [] # type: List[TypeMap]
4004+
else_maps = [] # type: List[TypeMap]
4005+
for expr in exprs_in_type_calls:
4006+
current_if_map, current_else_map = self.conditional_type_map_with_intersection(
4007+
expr,
4008+
type_map[expr],
4009+
type_being_compared
4010+
)
4011+
if_maps.append(current_if_map)
4012+
else_maps.append(current_else_map)
4013+
4014+
def combine_maps(list_maps: List[TypeMap]) -> TypeMap:
4015+
"""Combine all typemaps in list_maps into one typemap"""
4016+
result_map = {}
4017+
for d in list_maps:
4018+
if d is not None:
4019+
result_map.update(d)
4020+
return result_map
4021+
if_map = combine_maps(if_maps)
4022+
# type(x) == T is only true when x has the same type as T, meaning
4023+
# that it can be false if x is an instance of a subclass of T. That means
4024+
# we can't do any narrowing in the else case unless T is final, in which
4025+
# case T can't be subclassed
4026+
if is_final:
4027+
else_map = combine_maps(else_maps)
4028+
else:
4029+
else_map = {}
4030+
return if_map, else_map
4031+
39574032
def find_isinstance_check(self, node: Expression
39584033
) -> Tuple[TypeMap, TypeMap]:
39594034
"""Find any isinstance checks (within a chain of ands). Includes
@@ -4118,6 +4193,11 @@ def has_no_custom_eq_checks(t: Type) -> bool:
41184193
expr_indices,
41194194
narrowable_operand_index_to_hash.keys(),
41204195
)
4196+
4197+
# If we haven't been able to narrow types yet, we might be dealing with a
4198+
# explicit type(x) == some_type check
4199+
if if_map == {} and else_map == {}:
4200+
if_map, else_map = self.find_type_equals_check(node, expr_indices)
41214201
elif operator in {'in', 'not in'}:
41224202
assert len(expr_indices) == 2
41234203
left_index, right_index = expr_indices

test-data/unit/check-isinstance.test

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2573,3 +2573,101 @@ if issubclass(x, B):
25732573
else:
25742574
reveal_type(x) # N: Revealed type is "Type[__main__.A]"
25752575
[builtins fixtures/isinstance.pyi]
2576+
2577+
[case testTypeEqualsCheck]
2578+
from typing import Any
2579+
2580+
y: Any
2581+
if type(y) == int:
2582+
reveal_type(y) # N: Revealed type is "builtins.int"
2583+
2584+
2585+
[case testMultipleTypeEqualsCheck]
2586+
from typing import Any
2587+
2588+
x: Any
2589+
y: Any
2590+
if type(x) == type(y) == int:
2591+
reveal_type(y) # N: Revealed type is "builtins.int"
2592+
reveal_type(x) # N: Revealed type is "builtins.int"
2593+
2594+
[case testTypeEqualsCheckUsingIs]
2595+
from typing import Any
2596+
2597+
y: Any
2598+
if type(y) is int:
2599+
reveal_type(y) # N: Revealed type is "builtins.int"
2600+
2601+
[case testTypeEqualsNarrowingUnionWithElse]
2602+
from typing import Union
2603+
2604+
x: Union[int, str]
2605+
if type(x) is int:
2606+
reveal_type(x) # N: Revealed type is "builtins.int"
2607+
else:
2608+
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
2609+
2610+
[case testTypeEqualsMultipleTypesShouldntNarrow]
2611+
# make sure we don't do any narrowing if there are multiple types being compared
2612+
2613+
from typing import Union
2614+
2615+
x: Union[int, str]
2616+
if type(x) == int == str:
2617+
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
2618+
else:
2619+
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
2620+
2621+
# mypy shows an error about "Unsupported left operand type for !=" if we don't include this
2622+
[builtins fixtures/typing-medium.pyi]
2623+
# mypy thinks int isn't defined unless we include this
2624+
[builtins fixtures/primitives.pyi]
2625+
[case testTypeNotEqualsCheck]
2626+
from typing import Union
2627+
2628+
x: Union[int, str]
2629+
if type(x) != int:
2630+
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
2631+
else:
2632+
reveal_type(x) # N: Revealed type is "builtins.int"
2633+
2634+
# mypy shows an error about "Unsupported left operand type for !=" if we don't include this
2635+
[builtins fixtures/typing-medium.pyi]
2636+
# mypy thinks int isn't defined unless we include this
2637+
[builtins fixtures/primitives.pyi]
2638+
2639+
[case testTypeNotEqualsCheckUsingIsNot]
2640+
from typing import Union
2641+
2642+
x: Union[int, str]
2643+
if type(x) is not int:
2644+
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
2645+
else:
2646+
reveal_type(x) # N: Revealed type is "builtins.int"
2647+
2648+
[case testNarrowInElseCaseIfFinal]
2649+
from typing import final, Union
2650+
@final
2651+
class C:
2652+
pass
2653+
class D:
2654+
pass
2655+
2656+
x: Union[C, D]
2657+
if type(x) is C:
2658+
reveal_type(x) # N: Revealed type is "__main__.C"
2659+
else:
2660+
reveal_type(x) # N: Revealed type is "__main__.D"
2661+
[case testNarrowInIfCaseIfFinalUsingIsNot]
2662+
from typing import final, Union
2663+
@final
2664+
class C:
2665+
pass
2666+
class D:
2667+
pass
2668+
2669+
x: Union[C, D]
2670+
if type(x) is not C:
2671+
reveal_type(x) # N: Revealed type is "__main__.D"
2672+
else:
2673+
reveal_type(x) # N: Revealed type is "__main__.C"

0 commit comments

Comments
 (0)