Skip to content

Commit 4b8e7df

Browse files
authored
Refactor "==" and "is" type narrowing logic (#18042)
Split a big function to make it easier to modify and understand.
1 parent 9323b88 commit 4b8e7df

File tree

1 file changed

+83
-66
lines changed

1 file changed

+83
-66
lines changed

mypy/checker.py

Lines changed: 83 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6032,72 +6032,14 @@ def find_isinstance_check_helper(
60326032
partial_type_maps = []
60336033
for operator, expr_indices in simplified_operator_list:
60346034
if operator in {"is", "is not", "==", "!="}:
6035-
# is_valid_target:
6036-
# Controls which types we're allowed to narrow exprs to. Note that
6037-
# we cannot use 'is_literal_type_like' in both cases since doing
6038-
# 'x = 10000 + 1; x is 10001' is not always True in all Python
6039-
# implementations.
6040-
#
6041-
# coerce_only_in_literal_context:
6042-
# If true, coerce types into literal types only if one or more of
6043-
# the provided exprs contains an explicit Literal type. This could
6044-
# technically be set to any arbitrary value, but it seems being liberal
6045-
# with narrowing when using 'is' and conservative when using '==' seems
6046-
# to break the least amount of real-world code.
6047-
#
6048-
# should_narrow_by_identity:
6049-
# Set to 'false' only if the user defines custom __eq__ or __ne__ methods
6050-
# that could cause identity-based narrowing to produce invalid results.
6051-
if operator in {"is", "is not"}:
6052-
is_valid_target: Callable[[Type], bool] = is_singleton_type
6053-
coerce_only_in_literal_context = False
6054-
should_narrow_by_identity = True
6055-
else:
6056-
6057-
def is_exactly_literal_type(t: Type) -> bool:
6058-
return isinstance(get_proper_type(t), LiteralType)
6059-
6060-
def has_no_custom_eq_checks(t: Type) -> bool:
6061-
return not custom_special_method(
6062-
t, "__eq__", check_all=False
6063-
) and not custom_special_method(t, "__ne__", check_all=False)
6064-
6065-
is_valid_target = is_exactly_literal_type
6066-
coerce_only_in_literal_context = True
6067-
6068-
expr_types = [operand_types[i] for i in expr_indices]
6069-
should_narrow_by_identity = all(
6070-
map(has_no_custom_eq_checks, expr_types)
6071-
) and not is_ambiguous_mix_of_enums(expr_types)
6072-
6073-
if_map: TypeMap = {}
6074-
else_map: TypeMap = {}
6075-
if should_narrow_by_identity:
6076-
if_map, else_map = self.refine_identity_comparison_expression(
6077-
operands,
6078-
operand_types,
6079-
expr_indices,
6080-
narrowable_operand_index_to_hash.keys(),
6081-
is_valid_target,
6082-
coerce_only_in_literal_context,
6083-
)
6084-
6085-
# Strictly speaking, we should also skip this check if the objects in the expr
6086-
# chain have custom __eq__ or __ne__ methods. But we (maybe optimistically)
6087-
# assume nobody would actually create a custom objects that considers itself
6088-
# equal to None.
6089-
if if_map == {} and else_map == {}:
6090-
if_map, else_map = self.refine_away_none_in_comparison(
6091-
operands,
6092-
operand_types,
6093-
expr_indices,
6094-
narrowable_operand_index_to_hash.keys(),
6095-
)
6096-
6097-
# If we haven't been able to narrow types yet, we might be dealing with a
6098-
# explicit type(x) == some_type check
6099-
if if_map == {} and else_map == {}:
6100-
if_map, else_map = self.find_type_equals_check(node, expr_indices)
6035+
if_map, else_map = self.equality_type_narrowing_helper(
6036+
node,
6037+
operator,
6038+
operands,
6039+
operand_types,
6040+
expr_indices,
6041+
narrowable_operand_index_to_hash,
6042+
)
61016043
elif operator in {"in", "not in"}:
61026044
assert len(expr_indices) == 2
61036045
left_index, right_index = expr_indices
@@ -6242,6 +6184,81 @@ def has_no_custom_eq_checks(t: Type) -> bool:
62426184
else_map = {node: else_type} if not isinstance(else_type, UninhabitedType) else None
62436185
return if_map, else_map
62446186

6187+
def equality_type_narrowing_helper(
6188+
self,
6189+
node: ComparisonExpr,
6190+
operator: str,
6191+
operands: list[Expression],
6192+
operand_types: list[Type],
6193+
expr_indices: list[int],
6194+
narrowable_operand_index_to_hash: dict[int, tuple[Key, ...]],
6195+
) -> tuple[TypeMap, TypeMap]:
6196+
"""Calculate type maps for '==', '!=', 'is' or 'is not' expression."""
6197+
# is_valid_target:
6198+
# Controls which types we're allowed to narrow exprs to. Note that
6199+
# we cannot use 'is_literal_type_like' in both cases since doing
6200+
# 'x = 10000 + 1; x is 10001' is not always True in all Python
6201+
# implementations.
6202+
#
6203+
# coerce_only_in_literal_context:
6204+
# If true, coerce types into literal types only if one or more of
6205+
# the provided exprs contains an explicit Literal type. This could
6206+
# technically be set to any arbitrary value, but it seems being liberal
6207+
# with narrowing when using 'is' and conservative when using '==' seems
6208+
# to break the least amount of real-world code.
6209+
#
6210+
# should_narrow_by_identity:
6211+
# Set to 'false' only if the user defines custom __eq__ or __ne__ methods
6212+
# that could cause identity-based narrowing to produce invalid results.
6213+
if operator in {"is", "is not"}:
6214+
is_valid_target: Callable[[Type], bool] = is_singleton_type
6215+
coerce_only_in_literal_context = False
6216+
should_narrow_by_identity = True
6217+
else:
6218+
6219+
def is_exactly_literal_type(t: Type) -> bool:
6220+
return isinstance(get_proper_type(t), LiteralType)
6221+
6222+
def has_no_custom_eq_checks(t: Type) -> bool:
6223+
return not custom_special_method(
6224+
t, "__eq__", check_all=False
6225+
) and not custom_special_method(t, "__ne__", check_all=False)
6226+
6227+
is_valid_target = is_exactly_literal_type
6228+
coerce_only_in_literal_context = True
6229+
6230+
expr_types = [operand_types[i] for i in expr_indices]
6231+
should_narrow_by_identity = all(
6232+
map(has_no_custom_eq_checks, expr_types)
6233+
) and not is_ambiguous_mix_of_enums(expr_types)
6234+
6235+
if_map: TypeMap = {}
6236+
else_map: TypeMap = {}
6237+
if should_narrow_by_identity:
6238+
if_map, else_map = self.refine_identity_comparison_expression(
6239+
operands,
6240+
operand_types,
6241+
expr_indices,
6242+
narrowable_operand_index_to_hash.keys(),
6243+
is_valid_target,
6244+
coerce_only_in_literal_context,
6245+
)
6246+
6247+
# Strictly speaking, we should also skip this check if the objects in the expr
6248+
# chain have custom __eq__ or __ne__ methods. But we (maybe optimistically)
6249+
# assume nobody would actually create a custom objects that considers itself
6250+
# equal to None.
6251+
if if_map == {} and else_map == {}:
6252+
if_map, else_map = self.refine_away_none_in_comparison(
6253+
operands, operand_types, expr_indices, narrowable_operand_index_to_hash.keys()
6254+
)
6255+
6256+
# If we haven't been able to narrow types yet, we might be dealing with a
6257+
# explicit type(x) == some_type check
6258+
if if_map == {} and else_map == {}:
6259+
if_map, else_map = self.find_type_equals_check(node, expr_indices)
6260+
return if_map, else_map
6261+
62456262
def propagate_up_typemap_info(self, new_types: TypeMap) -> TypeMap:
62466263
"""Attempts refining parent expressions of any MemberExpr or IndexExprs in new_types.
62476264

0 commit comments

Comments
 (0)