6
6
7
7
from typing import (
8
8
Dict , Set , List , cast , Tuple , TypeVar , Union , Optional , NamedTuple , Iterator , Iterable ,
9
- Sequence , Mapping , Generic , AbstractSet
9
+ Sequence , Mapping , Generic , AbstractSet , Callable
10
10
)
11
11
from typing_extensions import Final
12
12
50
50
erase_def_to_union_or_bound , erase_to_union_or_bound , coerce_to_literal ,
51
51
try_getting_str_literals_from_type , try_getting_int_literals_from_type ,
52
52
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 ,
54
55
)
55
56
from mypy import message_registry
56
57
from mypy .subtypes import (
@@ -3890,20 +3891,59 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM
3890
3891
3891
3892
partial_type_maps = []
3892
3893
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
+ )
3907
3947
elif operator in {'in' , 'not in' }:
3908
3948
assert len (expr_indices ) == 2
3909
3949
left_index , right_index = expr_indices
@@ -4146,8 +4186,10 @@ def refine_identity_comparison_expression(self,
4146
4186
operand_types : List [Type ],
4147
4187
chain_indices : List [int ],
4148
4188
narrowable_operand_indices : AbstractSet [int ],
4189
+ is_valid_target : Callable [[ProperType ], bool ],
4190
+ coerce_only_in_literal_context : bool ,
4149
4191
) -> 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.
4151
4193
4152
4194
The 'operands' and 'operand_types' lists should be the full list of operands used
4153
4195
in the overall comparison expression. The 'chain_indices' list is the list of indices
@@ -4163,30 +4205,45 @@ def refine_identity_comparison_expression(self,
4163
4205
The 'narrowable_operand_indices' parameter is the set of all indices we are allowed
4164
4206
to refine the types of: that is, all operands that will potentially be a part of
4165
4207
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.
4166
4217
"""
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 = []
4169
4224
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 )):
4172
4229
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
4175
4232
# must be unreachable.
4176
4233
return None , {}
4177
- singleton = coerced_type
4178
- possible_singleton_indices .append (i )
4234
+ target = expr_type
4235
+ possible_target_indices .append (i )
4179
4236
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 ,
4181
4238
# so we end early and infer nothing.
4182
- if singleton is None :
4239
+ if target is None :
4183
4240
return {}, {}
4184
4241
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
4187
4244
# want to pick an expression we were going to skip anyways.
4188
4245
singleton_index = - 1
4189
- for i in possible_singleton_indices :
4246
+ for i in possible_target_indices :
4190
4247
if i not in narrowable_operand_indices :
4191
4248
singleton_index = i
4192
4249
@@ -4215,20 +4272,21 @@ def refine_identity_comparison_expression(self,
4215
4272
# currently will just mark the whole branch as unreachable if either operand is
4216
4273
# narrowed to <uninhabited>.
4217
4274
if singleton_index == - 1 :
4218
- singleton_index = possible_singleton_indices [- 1 ]
4275
+ singleton_index = possible_target_indices [- 1 ]
4219
4276
4220
4277
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
4223
4281
4224
- target_type = [TypeRange (singleton , is_upper_bound = False )]
4282
+ target_type = [TypeRange (target , is_upper_bound = False )]
4225
4283
4226
4284
partial_type_maps = []
4227
4285
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
4229
4287
# will end up assuming that the 'else' branch is unreachable. This is
4230
4288
# 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
4232
4290
# the other exprs against this one instead.
4233
4291
if i == singleton_index :
4234
4292
continue
@@ -4246,17 +4304,16 @@ def refine_identity_comparison_expression(self,
4246
4304
4247
4305
return reduce_partial_conditional_maps (partial_type_maps )
4248
4306
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 .
4256
4314
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.
4260
4317
"""
4261
4318
non_optional_types = []
4262
4319
for i in chain_indices :
@@ -4749,7 +4806,7 @@ class Foo(Enum):
4749
4806
return False
4750
4807
4751
4808
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 ) )
4753
4810
if not isinstance (parent_type , FunctionLike ) or not isinstance (member_type , LiteralType ):
4754
4811
return False
4755
4812
@@ -5540,3 +5597,9 @@ def has_bool_item(typ: ProperType) -> bool:
5540
5597
return any (is_named_instance (item , 'builtins.bool' )
5541
5598
for item in typ .items )
5542
5599
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