Skip to content

Commit 4a69e5f

Browse files
improve testing of basic ops with last_known_value
1 parent 8a5c7dc commit 4a69e5f

File tree

7 files changed

+275
-15
lines changed

7 files changed

+275
-15
lines changed

mypy/join.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ def join_instances(self, t: Instance, s: Instance) -> ProperType:
6969
# Simplest case: join two types with the same base type (but
7070
# potentially different arguments).
7171

72+
last_known_value = (
73+
None if t.last_known_value != s.last_known_value else t.last_known_value
74+
)
75+
7276
# Combine type arguments.
7377
args: list[Type] = []
7478
# N.B: We use zip instead of indexing because the lengths might have
@@ -104,10 +108,10 @@ def join_instances(self, t: Instance, s: Instance) -> ProperType:
104108
new_type = join_types(ta, sa, self)
105109
if len(type_var.values) != 0 and new_type not in type_var.values:
106110
self.seen_instances.pop()
107-
return object_from_instance(t)
111+
return object_from_instance(t, last_known_value=last_known_value)
108112
if not is_subtype(new_type, type_var.upper_bound):
109113
self.seen_instances.pop()
110-
return object_from_instance(t)
114+
return object_from_instance(t, last_known_value=last_known_value)
111115
# TODO: contravariant case should use meet but pass seen instances as
112116
# an argument to keep track of recursive checks.
113117
elif type_var.variance in (INVARIANT, CONTRAVARIANT):
@@ -117,7 +121,7 @@ def join_instances(self, t: Instance, s: Instance) -> ProperType:
117121
new_type = ta
118122
elif not is_equivalent(ta, sa):
119123
self.seen_instances.pop()
120-
return object_from_instance(t)
124+
return object_from_instance(t, last_known_value=last_known_value)
121125
else:
122126
# If the types are different but equivalent, then an Any is involved
123127
# so using a join in the contravariant case is also OK.
@@ -141,7 +145,7 @@ def join_instances(self, t: Instance, s: Instance) -> ProperType:
141145
new_type = join_types(ta, sa, self)
142146
assert new_type is not None
143147
args.append(new_type)
144-
result: ProperType = Instance(t.type, args)
148+
result: ProperType = Instance(t.type, args, last_known_value=last_known_value)
145149
elif t.type.bases and is_proper_subtype(
146150
t, s, subtype_context=SubtypeContext(ignore_type_params=True)
147151
):
@@ -270,6 +274,10 @@ def visit_unbound_type(self, t: UnboundType) -> ProperType:
270274
def visit_union_type(self, t: UnionType) -> ProperType:
271275
if is_proper_subtype(self.s, t):
272276
return t
277+
elif isinstance(self.s, LiteralType):
278+
# E.g. join("x", "y" | "z") -> "x" | "y" | "z"
279+
# and join(1, "y" | "z") -> object
280+
return mypy.typeops.make_simplified_union(join_types(self.s, x) for x in t.items)
273281
else:
274282
return mypy.typeops.make_simplified_union([self.s, t])
275283

@@ -621,13 +629,18 @@ def visit_typeddict_type(self, t: TypedDictType) -> ProperType:
621629
def visit_literal_type(self, t: LiteralType) -> ProperType:
622630
if isinstance(self.s, LiteralType):
623631
if t == self.s:
632+
# E.g. Literal["x"], Literal["x"] -> Literal["x"]
624633
return t
625-
if self.s.fallback.type.is_enum and t.fallback.type.is_enum:
634+
if (self.s.fallback.type == t.fallback.type) or (
635+
self.s.fallback.type.is_enum and t.fallback.type.is_enum
636+
):
626637
return mypy.typeops.make_simplified_union([self.s, t])
627638
return join_types(self.s.fallback, t.fallback)
628639
elif isinstance(self.s, Instance) and self.s.last_known_value == t:
640+
# E.g. Literal["x"], Literal["x"]? -> Literal["x"]
629641
return t
630642
else:
643+
# E.g. Literal["x"], Literal["y"]? -> str
631644
return join_types(self.s, t.fallback)
632645

633646
def visit_partial_type(self, t: PartialType) -> ProperType:
@@ -848,10 +861,12 @@ def combine_arg_names(
848861
return new_names
849862

850863

851-
def object_from_instance(instance: Instance) -> Instance:
864+
def object_from_instance(
865+
instance: Instance, last_known_value: LiteralType | None = None
866+
) -> Instance:
852867
"""Construct the type 'builtins.object' from an instance type."""
853868
# Use the fact that 'object' is always the last class in the mro.
854-
res = Instance(instance.type.mro[-1], [])
869+
res = Instance(instance.type.mro[-1], [], last_known_value=last_known_value)
855870
return res
856871

857872

mypy/meet.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,9 +1113,11 @@ def visit_literal_type(self, t: LiteralType) -> ProperType:
11131113
if isinstance(self.s, LiteralType) and self.s == t:
11141114
return t
11151115
elif isinstance(self.s, Instance):
1116-
if is_subtype(t.fallback, self.s):
1117-
return t
1116+
# if is_subtype(t.fallback, self.s):
1117+
# return t
11181118
if self.s.last_known_value is not None:
1119+
# meet(Literal["max"]?, Literal["max"]) -> Literal["max"]
1120+
# meet(Literal["sum"]?, Literal["max"]) -> Never
11191121
return meet_types(self.s.last_known_value, t)
11201122
return self.default(self.s)
11211123
else:

mypy/subtypes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,12 @@ def visit_typeddict_type(self, left: TypedDictType) -> bool:
971971
def visit_literal_type(self, left: LiteralType) -> bool:
972972
if isinstance(self.right, LiteralType):
973973
return left == self.right
974+
elif (
975+
isinstance(self.right, Instance)
976+
and self.right.last_known_value is not None
977+
and self.proper_subtype
978+
):
979+
return self._is_subtype(left, self.right.last_known_value)
974980
else:
975981
return self._is_subtype(left.fallback, self.right)
976982

mypy/test/testsubtypes.py

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from mypy.nodes import CONTRAVARIANT, COVARIANT, INVARIANT
4-
from mypy.subtypes import is_subtype
4+
from mypy.subtypes import is_proper_subtype, is_subtype, restrict_subtype_away
55
from mypy.test.helpers import Suite
66
from mypy.test.typefixture import InterfaceTypeFixture, TypeFixture
77
from mypy.types import Instance, TupleType, Type, UninhabitedType, UnpackType
@@ -277,6 +277,74 @@ def test_type_var_tuple_unpacked_variable_length_tuple(self) -> None:
277277
def test_fallback_not_subtype_of_tuple(self) -> None:
278278
self.assert_not_subtype(self.fx.a, TupleType([self.fx.b], fallback=self.fx.a))
279279

280+
def test_literal(self) -> None:
281+
str1 = self.fx.lit_str1
282+
str2 = self.fx.lit_str2
283+
str1_inst = self.fx.lit_str1_inst
284+
str2_inst = self.fx.lit_str2_inst
285+
str_type = self.fx.str_type
286+
287+
# other operand is the fallback type
288+
# "x" ≲ str -> YES
289+
# str ≲ "x" -> NO
290+
# "x"? ≲ str -> YES
291+
# str ≲ "x"? -> YES
292+
self.assert_subtype(str1, str_type)
293+
self.assert_not_subtype(str_type, str1)
294+
self.assert_subtype(str1_inst, str_type)
295+
self.assert_subtype(str_type, str1_inst)
296+
297+
# other operand is the same literal
298+
# "x" ≲ "x" -> YES
299+
# "x" ≲ "x"? -> YES
300+
# "x"? ≲ "x" -> YES
301+
# "x"? ≲ "x"? -> YES
302+
self.assert_subtype(str1, str1)
303+
self.assert_subtype(str1, str1_inst)
304+
self.assert_subtype(str1_inst, str1)
305+
self.assert_subtype(str1_inst, str1_inst)
306+
307+
# second operand is a different literal
308+
# "x" ≲ "y" -> NO
309+
# "x" ≲ "y"? -> YES
310+
# "x"? ≲ "y" -> NO
311+
# "x"? ≲ "y"? -> YES
312+
self.assert_not_subtype(str1, str2)
313+
self.assert_subtype(str1, str2_inst)
314+
self.assert_not_subtype(str1_inst, str2)
315+
self.assert_subtype(str1_inst, str2_inst)
316+
317+
# check proper subtyping
318+
# second operand is the fallback type
319+
# "x" <: str -> YES
320+
# str <: "x" -> NO
321+
# "x"? <: str -> YES
322+
# str <: "x"? -> YES
323+
self.assert_proper_subtype(str1, str_type)
324+
self.assert_not_proper_subtype(str_type, str1)
325+
self.assert_proper_subtype(str1_inst, str_type)
326+
self.assert_proper_subtype(str_type, str1_inst)
327+
328+
# second operand is the same literal
329+
# "x" <: "x" -> YES
330+
# "x" <: "x"? -> YES
331+
# "x"? <: "x" -> NO
332+
# "x"? <: "x"? -> YES
333+
self.assert_proper_subtype(str1, str1)
334+
self.assert_proper_subtype(str1, str1_inst)
335+
self.assert_not_proper_subtype(str1_inst, str1)
336+
self.assert_proper_subtype(str1_inst, str1_inst)
337+
338+
# second operand is a different literal
339+
# "x" ≲ "y" -> NO
340+
# "x" ≲ "y"? -> NO
341+
# "x"? ≲ "y" -> NO
342+
# "x"? ≲ "y"? -> YES
343+
self.assert_not_proper_subtype(str1, str2)
344+
self.assert_not_proper_subtype(str1, str2_inst)
345+
self.assert_not_proper_subtype(str1_inst, str2)
346+
self.assert_proper_subtype(str1_inst, str2_inst)
347+
280348
# IDEA: Maybe add these test cases (they are tested pretty well in type
281349
# checker tests already):
282350
# * more interface subtyping test cases
@@ -287,6 +355,12 @@ def test_fallback_not_subtype_of_tuple(self) -> None:
287355
# * any type
288356
# * generic function types
289357

358+
def assert_proper_subtype(self, s: Type, t: Type) -> None:
359+
assert is_proper_subtype(s, t), f"{s} not proper subtype of {t}"
360+
361+
def assert_not_proper_subtype(self, s: Type, t: Type) -> None:
362+
assert not is_proper_subtype(s, t), f"{s} not proper subtype of {t}"
363+
290364
def assert_subtype(self, s: Type, t: Type) -> None:
291365
assert is_subtype(s, t), f"{s} not subtype of {t}"
292366

@@ -304,3 +378,53 @@ def assert_equivalent(self, s: Type, t: Type) -> None:
304378
def assert_unrelated(self, s: Type, t: Type) -> None:
305379
self.assert_not_subtype(s, t)
306380
self.assert_not_subtype(t, s)
381+
382+
383+
class RestrictionSuite(Suite):
384+
# Tests for type restrictions "A - B", i.e. ``T <: A and not T <: B``.
385+
386+
def setUp(self) -> None:
387+
self.fx = TypeFixture()
388+
389+
def assert_restriction(self, s: Type, t: Type, expected: Type) -> None:
390+
actual = restrict_subtype_away(s, t)
391+
msg = f"restrict_subtype_away({s}, {t}) == {{}} ({{}} expected)"
392+
self.assertEqual(actual, expected, msg=msg.format(actual, expected))
393+
394+
def test_literal(self) -> None:
395+
str1 = self.fx.lit_str1
396+
str2 = self.fx.lit_str2
397+
str1_inst = self.fx.lit_str1_inst
398+
str2_inst = self.fx.lit_str2_inst
399+
str_type = self.fx.str_type
400+
uninhabited = self.fx.uninhabited
401+
402+
# other operand is the fallback type
403+
# "x" - str -> Never
404+
# str - "x" -> str
405+
# "x"? - str -> Never
406+
# str - "x"? -> Never
407+
self.assert_restriction(str1, str_type, uninhabited)
408+
self.assert_restriction(str_type, str1, str_type)
409+
self.assert_restriction(str1_inst, str_type, uninhabited)
410+
self.assert_restriction(str_type, str1_inst, uninhabited)
411+
412+
# other operand is the same literal
413+
# "x" - "x" -> Never
414+
# "x" - "x"? -> Never
415+
# "x"? - "x" -> Never
416+
# "x"? - "x"? -> Never
417+
self.assert_restriction(str1, str1, uninhabited)
418+
self.assert_restriction(str1, str1_inst, uninhabited)
419+
self.assert_restriction(str1_inst, str1, uninhabited)
420+
self.assert_restriction(str1_inst, str1_inst, uninhabited)
421+
422+
# other operand is a different literal
423+
# "x" - "y" -> "x"
424+
# "x" - "y"? -> Never
425+
# "x"? - "y" -> "x"?
426+
# "x"? - "y"? -> Never
427+
self.assert_restriction(str1, str2, str1)
428+
self.assert_restriction(str1, str2_inst, uninhabited)
429+
self.assert_restriction(str1_inst, str2, str1_inst)
430+
self.assert_restriction(str1_inst, str2_inst, uninhabited)

0 commit comments

Comments
 (0)