Skip to content

Commit 9a8a8c0

Browse files
dmoissetddfisher
authored andcommitted
Fix for #1698 (#1989)
This is an implementation of my proposed fix for #1698 ( #1698 (comment) ) Relevant differences from the description there: After the suggestion from @ddfisher I used class attributes in Type instead of wrapping Type objects. When I want to restrict a type I make a copy and override the can_be_true or can_be_false with instance attributes I didn't implement the rules for not after realising that I couldn't find a relevant example where it helped. It's easy to add later if we discover otherwise but I didn't want to bloat an already large PR I noticed that the deductive power was good enough to start detecting more dead code in unrelated test cases (see modifications in check-statements, check-modules and check-generics). There are some other improvements that I saw that could be made but I decided to stop here; if you think any of these are necessary I can add them: Covering the "not" operator, as mentioned above Renaming "find_isinstance_check" to something like "types_specialized_by_condition" which is more descriptive of its current purpose. I used copy.copy() followed by an update to create restricted version of types, perhaps there's a better/safer way to copy those (and even cache the restricted versions to reduce memory usage)
1 parent d4e15f9 commit 9a8a8c0

File tree

9 files changed

+272
-40
lines changed

9 files changed

+272
-40
lines changed

mypy/checker.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
from mypy.types import (
3434
Type, AnyType, CallableType, Void, FunctionLike, Overloaded, TupleType,
3535
Instance, NoneTyp, ErrorType, strip_type,
36-
UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType
36+
UnionType, TypeVarId, TypeVarType, PartialType, DeletedType, UninhabitedType,
37+
true_only, false_only
3738
)
3839
from mypy.sametypes import is_same_type
3940
from mypy.messages import MessageBuilder
@@ -2447,11 +2448,16 @@ def find_isinstance_check(node: Node,
24472448
if is_not:
24482449
if_vars, else_vars = else_vars, if_vars
24492450
return if_vars, else_vars
2450-
elif isinstance(node, RefExpr) and experiments.STRICT_OPTIONAL:
2451-
# The type could be falsy, so we can't deduce anything new about the else branch
2451+
elif isinstance(node, RefExpr):
2452+
# Restrict the type of the variable to True-ish/False-ish in the if and else branches
2453+
# respectively
24522454
vartype = type_map[node]
2453-
_, if_vars = conditional_type_map(node, vartype, NoneTyp(), weak=weak)
2454-
return if_vars, {}
2455+
if_type = true_only(vartype)
2456+
else_type = false_only(vartype)
2457+
ref = node # type: Node
2458+
if_map = {ref: if_type} if not isinstance(if_type, UninhabitedType) else None
2459+
else_map = {ref: else_type} if not isinstance(else_type, UninhabitedType) else None
2460+
return if_map, else_map
24552461
elif isinstance(node, OpExpr) and node.op == 'and':
24562462
left_if_vars, left_else_vars = find_isinstance_check(
24572463
node.left,

mypy/checkexpr.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from mypy.types import (
66
Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef,
77
TupleType, Instance, TypeVarId, TypeVarType, ErasedType, UnionType,
8-
PartialType, DeletedType, UnboundType, UninhabitedType, TypeType
8+
PartialType, DeletedType, UnboundType, UninhabitedType, TypeType,
9+
true_only, false_only
910
)
1011
from mypy.nodes import (
1112
NameExpr, RefExpr, Var, FuncDef, OverloadedFuncDef, TypeInfo, CallExpr,
@@ -1094,22 +1095,20 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type:
10941095
ctx = self.chk.type_context[-1]
10951096
left_type = self.accept(e.left, ctx)
10961097

1098+
assert e.op in ('and', 'or') # Checked by visit_op_expr
1099+
10971100
if e.op == 'and':
10981101
right_map, left_map = \
10991102
mypy.checker.find_isinstance_check(e.left, self.chk.type_map,
11001103
self.chk.typing_mode_weak())
1104+
restricted_left_type = false_only(left_type)
1105+
result_is_left = not left_type.can_be_true
11011106
elif e.op == 'or':
11021107
left_map, right_map = \
11031108
mypy.checker.find_isinstance_check(e.left, self.chk.type_map,
11041109
self.chk.typing_mode_weak())
1105-
else:
1106-
left_map = None
1107-
right_map = None
1108-
1109-
if left_map and e.left in left_map:
1110-
# The type of expressions in left_map is the type they'll have if
1111-
# the left operand is the result of the operator.
1112-
left_type = left_map[e.left]
1110+
restricted_left_type = true_only(left_type)
1111+
result_is_left = not left_type.can_be_false
11131112

11141113
with self.chk.binder.frame_context():
11151114
if right_map:
@@ -1121,13 +1120,23 @@ def check_boolean_op(self, e: OpExpr, context: Context) -> Type:
11211120
self.check_usable_type(left_type, context)
11221121
self.check_usable_type(right_type, context)
11231122

1124-
# If either of the type maps is None that means that result cannot happen.
1125-
# If both of the type maps are None we just have no information.
1126-
if left_map is not None and right_map is None:
1123+
if right_map is None:
1124+
# The boolean expression is statically known to be the left value
1125+
assert left_map is not None # find_isinstance_check guarantees this
11271126
return left_type
1128-
elif left_map is None and right_map is not None:
1127+
if left_map is None:
1128+
# The boolean expression is statically known to be the right value
1129+
assert right_map is not None # find_isinstance_check guarantees this
11291130
return right_type
1130-
return UnionType.make_simplified_union([left_type, right_type])
1131+
1132+
if isinstance(restricted_left_type, UninhabitedType):
1133+
# The left operand can never be the result
1134+
return right_type
1135+
elif result_is_left:
1136+
# The left operand is always the result
1137+
return left_type
1138+
else:
1139+
return UnionType.make_simplified_union([restricted_left_type, right_type])
11311140

11321141
def check_list_multiply(self, e: OpExpr) -> Type:
11331142
"""Type check an expression of form '[...] * e'.

mypy/join.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Type, AnyType, NoneTyp, Void, TypeVisitor, Instance, UnboundType,
77
ErrorType, TypeVarType, CallableType, TupleType, ErasedType, TypeList,
88
UnionType, FunctionLike, Overloaded, PartialType, DeletedType,
9-
UninhabitedType, TypeType
9+
UninhabitedType, TypeType, true_or_false
1010
)
1111
from mypy.maptype import map_instance_to_supertype
1212
from mypy.subtypes import is_subtype, is_equivalent, is_subtype_ignoring_tvars
@@ -17,6 +17,11 @@
1717
def join_simple(declaration: Type, s: Type, t: Type) -> Type:
1818
"""Return a simple least upper bound given the declared type."""
1919

20+
if (s.can_be_true, s.can_be_false) != (t.can_be_true, t.can_be_false):
21+
# if types are restricted in different ways, use the more general versions
22+
s = true_or_false(s)
23+
t = true_or_false(t)
24+
2025
if isinstance(s, AnyType):
2126
return s
2227

@@ -60,6 +65,11 @@ def join_types(s: Type, t: Type) -> Type:
6065

6166
If the join does not exist, return an ErrorType instance.
6267
"""
68+
if (s.can_be_true, s.can_be_false) != (t.can_be_true, t.can_be_false):
69+
# if types are restricted in different ways, use the more general versions
70+
s = true_or_false(s)
71+
t = true_or_false(t)
72+
6373
if isinstance(s, AnyType):
6474
return s
6575

mypy/test/testtypes.py

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
from typing import List
44

55
from mypy.myunit import (
6-
Suite, assert_equal, assert_true, assert_false
6+
Suite, assert_equal, assert_true, assert_false, assert_type
77
)
88
from mypy.erasetype import erase_type
99
from mypy.expandtype import expand_type
10-
from mypy.join import join_types
10+
from mypy.join import join_types, join_simple
1111
from mypy.meet import meet_types
1212
from mypy.types import (
1313
UnboundType, AnyType, Void, CallableType, TupleType, TypeVarDef, Type,
14-
Instance, NoneTyp, ErrorType, Overloaded, TypeType,
14+
Instance, NoneTyp, ErrorType, Overloaded, TypeType, UnionType, UninhabitedType,
15+
true_only, false_only
1516
)
1617
from mypy.nodes import ARG_POS, ARG_OPT, ARG_STAR, CONTRAVARIANT, INVARIANT, COVARIANT
1718
from mypy.subtypes import is_subtype, is_more_precise, is_proper_subtype
@@ -232,6 +233,95 @@ def test_is_proper_subtype_invariance(self):
232233
assert_false(is_proper_subtype(fx.gb, fx.ga))
233234
assert_false(is_proper_subtype(fx.ga, fx.gb))
234235

236+
# can_be_true / can_be_false
237+
238+
def test_empty_tuple_always_false(self):
239+
tuple_type = self.tuple()
240+
assert_true(tuple_type.can_be_false)
241+
assert_false(tuple_type.can_be_true)
242+
243+
def test_nonempty_tuple_always_true(self):
244+
tuple_type = self.tuple(AnyType(), AnyType())
245+
assert_true(tuple_type.can_be_true)
246+
assert_false(tuple_type.can_be_false)
247+
248+
def test_union_can_be_true_if_any_true(self):
249+
union_type = UnionType([self.fx.a, self.tuple()])
250+
assert_true(union_type.can_be_true)
251+
252+
def test_union_can_not_be_true_if_none_true(self):
253+
union_type = UnionType([self.tuple(), self.tuple()])
254+
assert_false(union_type.can_be_true)
255+
256+
def test_union_can_be_false_if_any_false(self):
257+
union_type = UnionType([self.fx.a, self.tuple()])
258+
assert_true(union_type.can_be_false)
259+
260+
def test_union_can_not_be_false_if_none_false(self):
261+
union_type = UnionType([self.tuple(self.fx.a), self.tuple(self.fx.d)])
262+
assert_false(union_type.can_be_false)
263+
264+
# true_only / false_only
265+
266+
def test_true_only_of_false_type_is_uninhabited(self):
267+
to = true_only(NoneTyp())
268+
assert_type(UninhabitedType, to)
269+
270+
def test_true_only_of_true_type_is_idempotent(self):
271+
always_true = self.tuple(AnyType())
272+
to = true_only(always_true)
273+
assert_true(always_true is to)
274+
275+
def test_true_only_of_instance(self):
276+
to = true_only(self.fx.a)
277+
assert_equal(str(to), "A")
278+
assert_true(to.can_be_true)
279+
assert_false(to.can_be_false)
280+
assert_type(Instance, to)
281+
# The original class still can be false
282+
assert_true(self.fx.a.can_be_false)
283+
284+
def test_true_only_of_union(self):
285+
tup_type = self.tuple(AnyType())
286+
# Union of something that is unknown, something that is always true, something
287+
# that is always false
288+
union_type = UnionType([self.fx.a, tup_type, self.tuple()])
289+
to = true_only(union_type)
290+
assert_equal(len(to.items), 2)
291+
assert_true(to.items[0].can_be_true)
292+
assert_false(to.items[0].can_be_false)
293+
assert_true(to.items[1] is tup_type)
294+
295+
def test_false_only_of_true_type_is_uninhabited(self):
296+
fo = false_only(self.tuple(AnyType()))
297+
assert_type(UninhabitedType, fo)
298+
299+
def test_false_only_of_false_type_is_idempotent(self):
300+
always_false = NoneTyp()
301+
fo = false_only(always_false)
302+
assert_true(always_false is fo)
303+
304+
def test_false_only_of_instance(self):
305+
fo = false_only(self.fx.a)
306+
assert_equal(str(fo), "A")
307+
assert_false(fo.can_be_true)
308+
assert_true(fo.can_be_false)
309+
assert_type(Instance, fo)
310+
# The original class still can be true
311+
assert_true(self.fx.a.can_be_true)
312+
313+
def test_false_only_of_union(self):
314+
tup_type = self.tuple()
315+
# Union of something that is unknown, something that is always true, something
316+
# that is always false
317+
union_type = UnionType([self.fx.a, self.tuple(AnyType()), tup_type])
318+
assert_equal(len(union_type.items), 3)
319+
fo = false_only(union_type)
320+
assert_equal(len(fo.items), 2)
321+
assert_false(fo.items[0].can_be_true)
322+
assert_true(fo.items[0].can_be_false)
323+
assert_true(fo.items[1] is tup_type)
324+
235325
# Helpers
236326

237327
def tuple(self, *a):
@@ -343,6 +433,22 @@ def test_any_type(self):
343433
self.callable(self.fx.a, self.fx.b)]:
344434
self.assert_join(t, self.fx.anyt, self.fx.anyt)
345435

436+
def test_mixed_truth_restricted_type_simple(self):
437+
# join_simple against differently restricted truthiness types drops restrictions.
438+
true_a = true_only(self.fx.a)
439+
false_o = false_only(self.fx.o)
440+
j = join_simple(self.fx.o, true_a, false_o)
441+
assert_true(j.can_be_true)
442+
assert_true(j.can_be_false)
443+
444+
def test_mixed_truth_restricted_type(self):
445+
# join_types against differently restricted truthiness types drops restrictions.
446+
true_any = true_only(AnyType())
447+
false_o = false_only(self.fx.o)
448+
j = join_types(true_any, false_o)
449+
assert_true(j.can_be_true)
450+
assert_true(j.can_be_false)
451+
346452
def test_other_mixed_types(self):
347453
# In general, joining unrelated types produces object.
348454
for t1 in [self.fx.a, self.fx.t, self.tuple(),

0 commit comments

Comments
 (0)