66
77from typing import (
88 Dict , Set , List , cast , Tuple , TypeVar , Union , Optional , NamedTuple , Iterator , Iterable ,
9- Sequence , Mapping , Generic , AbstractSet
9+ Sequence , Mapping , Generic , AbstractSet , Callable
1010)
1111from typing_extensions import Final
1212
5050 erase_def_to_union_or_bound , erase_to_union_or_bound , coerce_to_literal ,
5151 try_getting_str_literals_from_type , try_getting_int_literals_from_type ,
5252 tuple_fallback , is_singleton_type , try_expanding_enum_to_union ,
53- true_only , false_only , function_type , TypeVarExtractor ,
53+ true_only , false_only , function_type , TypeVarExtractor , custom_special_method ,
54+ is_literal_type_like ,
5455)
5556from mypy import message_registry
5657from mypy .subtypes import (
@@ -3890,20 +3891,59 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM
38903891
38913892 partial_type_maps = []
38923893 for operator , expr_indices in simplified_operator_list :
3893- if operator in {'is' , 'is not' }:
3894- if_map , else_map = self .refine_identity_comparison_expression (
3895- operands ,
3896- operand_types ,
3897- expr_indices ,
3898- narrowable_operand_index_to_hash .keys (),
3899- )
3900- elif operator in {'==' , '!=' }:
3901- if_map , else_map = self .refine_equality_comparison_expression (
3902- operands ,
3903- operand_types ,
3904- expr_indices ,
3905- narrowable_operand_index_to_hash .keys (),
3906- )
3894+ if operator in {'is' , 'is not' , '==' , '!=' }:
3895+ # is_valid_target:
3896+ # Controls which types we're allowed to narrow exprs to. Note that
3897+ # we cannot use 'is_literal_type_like' in both cases since doing
3898+ # 'x = 10000 + 1; x is 10001' is not always True in all Python impls.
3899+ #
3900+ # coerce_only_in_literal_context:
3901+ # If true, coerce types into literal types only if one or more of
3902+ # the provided exprs contains an explicit Literal type. This could
3903+ # technically be set to any arbitrary value, but it seems being liberal
3904+ # with narrowing when using 'is' and conservative when using '==' seems
3905+ # to break the least amount of real-world code.
3906+ #
3907+ # should_narrow_by_identity:
3908+ # Set to 'false' only if the user defines custom __eq__ or __ne__ methods
3909+ # that could cause identity-based narrowing to produce invalid results.
3910+ if operator in {'is' , 'is not' }:
3911+ is_valid_target = is_singleton_type # type: Callable[[Type], bool]
3912+ coerce_only_in_literal_context = False
3913+ should_narrow_by_identity = True
3914+ else :
3915+ is_valid_target = is_exactly_literal_type
3916+ coerce_only_in_literal_context = True
3917+
3918+ def has_no_custom_eq_checks (t : Type ) -> bool :
3919+ return not custom_special_method (t , '__eq__' , check_all = False ) \
3920+ and not custom_special_method (t , '__ne__' , check_all = False )
3921+ expr_types = [operand_types [i ] for i in expr_indices ]
3922+ should_narrow_by_identity = all (map (has_no_custom_eq_checks , expr_types ))
3923+
3924+ if_map = {} # type: TypeMap
3925+ else_map = {} # type: TypeMap
3926+ if should_narrow_by_identity :
3927+ if_map , else_map = self .refine_identity_comparison_expression (
3928+ operands ,
3929+ operand_types ,
3930+ expr_indices ,
3931+ narrowable_operand_index_to_hash .keys (),
3932+ is_valid_target ,
3933+ coerce_only_in_literal_context ,
3934+ )
3935+
3936+ # Strictly speaking, we should also skip this check if the objects in the expr
3937+ # chain have custom __eq__ or __ne__ methods. But we (maybe optimistically)
3938+ # assume nobody would actually create a custom objects that considers itself
3939+ # equal to None.
3940+ if if_map == {} and else_map == {}:
3941+ if_map , else_map = self .refine_away_none_in_comparison (
3942+ operands ,
3943+ operand_types ,
3944+ expr_indices ,
3945+ narrowable_operand_index_to_hash .keys (),
3946+ )
39073947 elif operator in {'in' , 'not in' }:
39083948 assert len (expr_indices ) == 2
39093949 left_index , right_index = expr_indices
@@ -4146,8 +4186,10 @@ def refine_identity_comparison_expression(self,
41464186 operand_types : List [Type ],
41474187 chain_indices : List [int ],
41484188 narrowable_operand_indices : AbstractSet [int ],
4189+ is_valid_target : Callable [[ProperType ], bool ],
4190+ coerce_only_in_literal_context : bool ,
41494191 ) -> Tuple [TypeMap , TypeMap ]:
4150- """Produces conditional type maps refining expressions used in an identity comparison.
4192+ """Produces conditional type maps refining exprs used in an identity/equality comparison.
41514193
41524194 The 'operands' and 'operand_types' lists should be the full list of operands used
41534195 in the overall comparison expression. The 'chain_indices' list is the list of indices
@@ -4163,30 +4205,45 @@ def refine_identity_comparison_expression(self,
41634205 The 'narrowable_operand_indices' parameter is the set of all indices we are allowed
41644206 to refine the types of: that is, all operands that will potentially be a part of
41654207 the output TypeMaps.
4208+
4209+ Although this function could theoretically try setting the types of the operands
4210+ in the chains to the meet, doing that causes too many issues in real-world code.
4211+ Instead, we use 'is_valid_target' to identify which of the given chain types
4212+ we could plausibly use as the refined type for the expressions in the chain.
4213+
4214+ Similarly, 'coerce_only_in_literal_context' controls whether we should try coercing
4215+ expressions in the chain to a Literal type. Performing this coercion is sometimes
4216+ too aggressive of a narrowing, depending on context.
41664217 """
4167- singleton = None # type: Optional[ProperType]
4168- possible_singleton_indices = []
4218+ should_coerce = True
4219+ if coerce_only_in_literal_context :
4220+ should_coerce = any (is_literal_type_like (operand_types [i ]) for i in chain_indices )
4221+
4222+ target = None # type: Optional[Type]
4223+ possible_target_indices = []
41694224 for i in chain_indices :
4170- coerced_type = coerce_to_literal (operand_types [i ])
4171- if not is_singleton_type (coerced_type ):
4225+ expr_type = operand_types [i ]
4226+ if should_coerce :
4227+ expr_type = coerce_to_literal (expr_type )
4228+ if not is_valid_target (get_proper_type (expr_type )):
41724229 continue
4173- if singleton and not is_same_type (singleton , coerced_type ):
4174- # We have multiple disjoint singleton types. So the 'if' branch
4230+ if target and not is_same_type (target , expr_type ):
4231+ # We have multiple disjoint target types. So the 'if' branch
41754232 # must be unreachable.
41764233 return None , {}
4177- singleton = coerced_type
4178- possible_singleton_indices .append (i )
4234+ target = expr_type
4235+ possible_target_indices .append (i )
41794236
4180- # There's nothing we can currently infer if none of the operands are singleton types ,
4237+ # There's nothing we can currently infer if none of the operands are valid targets ,
41814238 # so we end early and infer nothing.
4182- if singleton is None :
4239+ if target is None :
41834240 return {}, {}
41844241
4185- # If possible, use an unassignable expression as the singleton .
4186- # We skip refining the type of the singleton below, so ideally we'd
4242+ # If possible, use an unassignable expression as the target .
4243+ # We skip refining the type of the target below, so ideally we'd
41874244 # want to pick an expression we were going to skip anyways.
41884245 singleton_index = - 1
4189- for i in possible_singleton_indices :
4246+ for i in possible_target_indices :
41904247 if i not in narrowable_operand_indices :
41914248 singleton_index = i
41924249
@@ -4215,20 +4272,21 @@ def refine_identity_comparison_expression(self,
42154272 # currently will just mark the whole branch as unreachable if either operand is
42164273 # narrowed to <uninhabited>.
42174274 if singleton_index == - 1 :
4218- singleton_index = possible_singleton_indices [- 1 ]
4275+ singleton_index = possible_target_indices [- 1 ]
42194276
42204277 enum_name = None
4221- if isinstance (singleton , LiteralType ) and singleton .is_enum_literal ():
4222- enum_name = singleton .fallback .type .fullname
4278+ target = get_proper_type (target )
4279+ if isinstance (target , LiteralType ) and target .is_enum_literal ():
4280+ enum_name = target .fallback .type .fullname
42234281
4224- target_type = [TypeRange (singleton , is_upper_bound = False )]
4282+ target_type = [TypeRange (target , is_upper_bound = False )]
42254283
42264284 partial_type_maps = []
42274285 for i in chain_indices :
4228- # If we try refining a singleton against itself, conditional_type_map
4286+ # If we try refining a type against itself, conditional_type_map
42294287 # will end up assuming that the 'else' branch is unreachable. This is
42304288 # typically not what we want: generally the user will intend for the
4231- # singleton type to be some fixed 'sentinel' value and will want to refine
4289+ # target type to be some fixed 'sentinel' value and will want to refine
42324290 # the other exprs against this one instead.
42334291 if i == singleton_index :
42344292 continue
@@ -4246,17 +4304,16 @@ def refine_identity_comparison_expression(self,
42464304
42474305 return reduce_partial_conditional_maps (partial_type_maps )
42484306
4249- def refine_equality_comparison_expression (self ,
4250- operands : List [Expression ],
4251- operand_types : List [Type ],
4252- chain_indices : List [int ],
4253- narrowable_operand_indices : AbstractSet [int ],
4254- ) -> Tuple [TypeMap , TypeMap ]:
4255- """Produces conditional type maps refining expressions used in an equality comparison .
4307+ def refine_away_none_in_comparison (self ,
4308+ operands : List [Expression ],
4309+ operand_types : List [Type ],
4310+ chain_indices : List [int ],
4311+ narrowable_operand_indices : AbstractSet [int ],
4312+ ) -> Tuple [TypeMap , TypeMap ]:
4313+ """Produces conditional type maps refining away None in an identity/ equality chain .
42564314
4257- For more details, see the docstring of 'refine_equality_comparison' up above.
4258- The only difference is that this function is for refining equality operations
4259- (e.g. 'a == b == c') instead of identity ('a is b is c').
4315+ For more details about what the different arguments mean, see the
4316+ docstring of 'refine_identity_comparison_expression' up above.
42604317 """
42614318 non_optional_types = []
42624319 for i in chain_indices :
@@ -4749,7 +4806,7 @@ class Foo(Enum):
47494806 return False
47504807
47514808 parent_type = get_proper_type (parent_type )
4752- member_type = coerce_to_literal (member_type )
4809+ member_type = get_proper_type ( coerce_to_literal (member_type ) )
47534810 if not isinstance (parent_type , FunctionLike ) or not isinstance (member_type , LiteralType ):
47544811 return False
47554812
@@ -5540,3 +5597,9 @@ def has_bool_item(typ: ProperType) -> bool:
55405597 return any (is_named_instance (item , 'builtins.bool' )
55415598 for item in typ .items )
55425599 return False
5600+
5601+
5602+ # TODO: why can't we define this as an inline function?
5603+ # Does mypyc not support them?
5604+ def is_exactly_literal_type (t : Type ) -> bool :
5605+ return isinstance (get_proper_type (t ), LiteralType )
0 commit comments