Skip to content

Commit 68b208d

Browse files
authored
Coalesce Literals when printing Unions (#12205)
Instead of printing `Union[Literal[X], Literal[Y]]`, these are now printed as `Literal[X, Y]`.
1 parent a726892 commit 68b208d

7 files changed

+89
-61
lines changed

mypy/messages.py

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
IS_SETTABLE, IS_CLASSVAR, IS_CLASS_OR_STATIC,
4040
)
4141
from mypy.sametypes import is_same_type
42+
from mypy.typeops import separate_union_literals
4243
from mypy.util import unmangle
4344
from mypy.errorcodes import ErrorCode
4445
from mypy import message_registry, errorcodes as codes
@@ -1664,6 +1665,16 @@ def format_type_inner(typ: Type,
16641665
def format(typ: Type) -> str:
16651666
return format_type_inner(typ, verbosity, fullnames)
16661667

1668+
def format_list(types: Sequence[Type]) -> str:
1669+
return ', '.join(format(typ) for typ in types)
1670+
1671+
def format_literal_value(typ: LiteralType) -> str:
1672+
if typ.is_enum_literal():
1673+
underlying_type = format(typ.fallback)
1674+
return '{}.{}'.format(underlying_type, typ.value)
1675+
else:
1676+
return typ.value_repr()
1677+
16671678
# TODO: show type alias names in errors.
16681679
typ = get_proper_type(typ)
16691680

@@ -1686,15 +1697,10 @@ def format(typ: Type) -> str:
16861697
elif itype.type.fullname in reverse_builtin_aliases:
16871698
alias = reverse_builtin_aliases[itype.type.fullname]
16881699
alias = alias.split('.')[-1]
1689-
items = [format(arg) for arg in itype.args]
1690-
return '{}[{}]'.format(alias, ', '.join(items))
1700+
return '{}[{}]'.format(alias, format_list(itype.args))
16911701
else:
16921702
# There are type arguments. Convert the arguments to strings.
1693-
a: List[str] = []
1694-
for arg in itype.args:
1695-
a.append(format(arg))
1696-
s = ', '.join(a)
1697-
return '{}[{}]'.format(base_str, s)
1703+
return '{}[{}]'.format(base_str, format_list(itype.args))
16981704
elif isinstance(typ, TypeVarType):
16991705
# This is similar to non-generic instance types.
17001706
return typ.name
@@ -1704,10 +1710,7 @@ def format(typ: Type) -> str:
17041710
# Prefer the name of the fallback class (if not tuple), as it's more informative.
17051711
if typ.partial_fallback.type.fullname != 'builtins.tuple':
17061712
return format(typ.partial_fallback)
1707-
items = []
1708-
for t in typ.items:
1709-
items.append(format(t))
1710-
s = 'Tuple[{}]'.format(', '.join(items))
1713+
s = 'Tuple[{}]'.format(format_list(typ.items))
17111714
return s
17121715
elif isinstance(typ, TypedDictType):
17131716
# If the TypedDictType is named, return the name
@@ -1722,24 +1725,34 @@ def format(typ: Type) -> str:
17221725
s = 'TypedDict({{{}}})'.format(', '.join(items))
17231726
return s
17241727
elif isinstance(typ, LiteralType):
1725-
if typ.is_enum_literal():
1726-
underlying_type = format(typ.fallback)
1727-
return 'Literal[{}.{}]'.format(underlying_type, typ.value)
1728-
else:
1729-
return str(typ)
1728+
return 'Literal[{}]'.format(format_literal_value(typ))
17301729
elif isinstance(typ, UnionType):
1731-
# Only print Unions as Optionals if the Optional wouldn't have to contain another Union
1732-
print_as_optional = (len(typ.items) -
1733-
sum(isinstance(get_proper_type(t), NoneType)
1734-
for t in typ.items) == 1)
1735-
if print_as_optional:
1736-
rest = [t for t in typ.items if not isinstance(get_proper_type(t), NoneType)]
1737-
return 'Optional[{}]'.format(format(rest[0]))
1730+
literal_items, union_items = separate_union_literals(typ)
1731+
1732+
# Coalesce multiple Literal[] members. This also changes output order.
1733+
# If there's just one Literal item, retain the original ordering.
1734+
if len(literal_items) > 1:
1735+
literal_str = 'Literal[{}]'.format(
1736+
', '.join(format_literal_value(t) for t in literal_items)
1737+
)
1738+
1739+
if len(union_items) == 1 and isinstance(get_proper_type(union_items[0]), NoneType):
1740+
return 'Optional[{}]'.format(literal_str)
1741+
elif union_items:
1742+
return 'Union[{}, {}]'.format(format_list(union_items), literal_str)
1743+
else:
1744+
return literal_str
17381745
else:
1739-
items = []
1740-
for t in typ.items:
1741-
items.append(format(t))
1742-
s = 'Union[{}]'.format(', '.join(items))
1746+
# Only print Union as Optional if the Optional wouldn't have to contain another Union
1747+
print_as_optional = (len(typ.items) -
1748+
sum(isinstance(get_proper_type(t), NoneType)
1749+
for t in typ.items) == 1)
1750+
if print_as_optional:
1751+
rest = [t for t in typ.items if not isinstance(get_proper_type(t), NoneType)]
1752+
return 'Optional[{}]'.format(format(rest[0]))
1753+
else:
1754+
s = 'Union[{}]'.format(format_list(typ.items))
1755+
17431756
return s
17441757
elif isinstance(typ, NoneType):
17451758
return 'None'

mypy/typeops.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,3 +846,18 @@ def is_redundant_literal_instance(general: ProperType, specific: ProperType) ->
846846
return True
847847

848848
return False
849+
850+
851+
def separate_union_literals(t: UnionType) -> Tuple[Sequence[LiteralType], Sequence[Type]]:
852+
"""Separate literals from other members in a union type."""
853+
literal_items = []
854+
union_items = []
855+
856+
for item in t.items:
857+
proper = get_proper_type(item)
858+
if isinstance(proper, LiteralType):
859+
literal_items.append(proper)
860+
else:
861+
union_items.append(item)
862+
863+
return literal_items, union_items

test-data/unit/check-enum.test

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,9 +1405,9 @@ class E(Enum):
14051405

14061406
e: E
14071407
a: Literal[E.A, E.B, E.C] = e
1408-
b: Literal[E.A, E.B] = e # E: Incompatible types in assignment (expression has type "E", variable has type "Union[Literal[E.A], Literal[E.B]]")
1409-
c: Literal[E.A, E.C] = e # E: Incompatible types in assignment (expression has type "E", variable has type "Union[Literal[E.A], Literal[E.C]]")
1410-
b = a # E: Incompatible types in assignment (expression has type "Union[Literal[E.A], Literal[E.B], Literal[E.C]]", variable has type "Union[Literal[E.A], Literal[E.B]]")
1408+
b: Literal[E.A, E.B] = e # E: Incompatible types in assignment (expression has type "E", variable has type "Literal[E.A, E.B]")
1409+
c: Literal[E.A, E.C] = e # E: Incompatible types in assignment (expression has type "E", variable has type "Literal[E.A, E.C]")
1410+
b = a # E: Incompatible types in assignment (expression has type "Literal[E.A, E.B, E.C]", variable has type "Literal[E.A, E.B]")
14111411
[builtins fixtures/bool.pyi]
14121412

14131413
[case testIntEnumWithNewTypeValue]

test-data/unit/check-expressions.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2153,9 +2153,9 @@ def returns_1_or_2() -> Literal[1, 2]:
21532153
...
21542154
THREE: Final = 3
21552155

2156-
if returns_a_or_b() == 'c': # E: Non-overlapping equality check (left operand type: "Union[Literal['a'], Literal['b']]", right operand type: "Literal['c']")
2156+
if returns_a_or_b() == 'c': # E: Non-overlapping equality check (left operand type: "Literal['a', 'b']", right operand type: "Literal['c']")
21572157
...
2158-
if returns_1_or_2() is THREE: # E: Non-overlapping identity check (left operand type: "Union[Literal[1], Literal[2]]", right operand type: "Literal[3]")
2158+
if returns_1_or_2() is THREE: # E: Non-overlapping identity check (left operand type: "Literal[1, 2]", right operand type: "Literal[3]")
21592159
...
21602160
[builtins fixtures/bool.pyi]
21612161

test-data/unit/check-isinstance.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2390,8 +2390,8 @@ class B:
23902390
def t3(self) -> None:
23912391
if isinstance(self, (A1, A2)):
23922392
reveal_type(self) # N: Revealed type is "Union[__main__.<subclass of "A1" and "B">2, __main__.<subclass of "A2" and "B">]"
2393-
x0: Literal[0] = self.f() # E: Incompatible types in assignment (expression has type "Union[Literal[1], Literal[2]]", variable has type "Literal[0]")
2394-
x1: Literal[1] = self.f() # E: Incompatible types in assignment (expression has type "Union[Literal[1], Literal[2]]", variable has type "Literal[1]")
2393+
x0: Literal[0] = self.f() # E: Incompatible types in assignment (expression has type "Literal[1, 2]", variable has type "Literal[0]")
2394+
x1: Literal[1] = self.f() # E: Incompatible types in assignment (expression has type "Literal[1, 2]", variable has type "Literal[1]")
23952395

23962396
[builtins fixtures/isinstance.pyi]
23972397

test-data/unit/check-literal.test

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -846,7 +846,7 @@ def func(x: Literal['foo', 'bar', ' foo ']) -> None: ...
846846
func('foo')
847847
func('bar')
848848
func(' foo ')
849-
func('baz') # E: Argument 1 to "func" has incompatible type "Literal['baz']"; expected "Union[Literal['foo'], Literal['bar'], Literal[' foo ']]"
849+
func('baz') # E: Argument 1 to "func" has incompatible type "Literal['baz']"; expected "Literal['foo', 'bar', ' foo ']"
850850

851851
a: Literal['foo']
852852
b: Literal['bar']
@@ -860,7 +860,7 @@ func(b)
860860
func(c)
861861
func(d)
862862
func(e)
863-
func(f) # E: Argument 1 to "func" has incompatible type "Union[Literal['foo'], Literal['bar'], Literal['baz']]"; expected "Union[Literal['foo'], Literal['bar'], Literal[' foo ']]"
863+
func(f) # E: Argument 1 to "func" has incompatible type "Literal['foo', 'bar', 'baz']"; expected "Literal['foo', 'bar', ' foo ']"
864864
[builtins fixtures/tuple.pyi]
865865
[out]
866866

@@ -1129,8 +1129,8 @@ d: int
11291129

11301130
foo(a)
11311131
foo(b)
1132-
foo(c) # E: Argument 1 to "foo" has incompatible type "Union[Literal[4], Literal[5]]"; expected "Union[Literal[1], Literal[2], Literal[3]]"
1133-
foo(d) # E: Argument 1 to "foo" has incompatible type "int"; expected "Union[Literal[1], Literal[2], Literal[3]]"
1132+
foo(c) # E: Argument 1 to "foo" has incompatible type "Literal[4, 5]"; expected "Literal[1, 2, 3]"
1133+
foo(d) # E: Argument 1 to "foo" has incompatible type "int"; expected "Literal[1, 2, 3]"
11341134
[builtins fixtures/tuple.pyi]
11351135
[out]
11361136

@@ -1144,7 +1144,7 @@ c: Literal[4, 'foo']
11441144

11451145
foo(a)
11461146
foo(b)
1147-
foo(c) # E: Argument 1 to "foo" has incompatible type "Union[Literal[4], Literal['foo']]"; expected "int"
1147+
foo(c) # E: Argument 1 to "foo" has incompatible type "Literal[4, 'foo']"; expected "int"
11481148
[builtins fixtures/tuple.pyi]
11491149
[out]
11501150

@@ -1248,19 +1248,19 @@ class Contravariant(Generic[T_contra]): pass
12481248
a1: Invariant[Literal[1]]
12491249
a2: Invariant[Literal[1, 2]]
12501250
a3: Invariant[Literal[1, 2, 3]]
1251-
a2 = a1 # E: Incompatible types in assignment (expression has type "Invariant[Literal[1]]", variable has type "Invariant[Union[Literal[1], Literal[2]]]")
1252-
a2 = a3 # E: Incompatible types in assignment (expression has type "Invariant[Union[Literal[1], Literal[2], Literal[3]]]", variable has type "Invariant[Union[Literal[1], Literal[2]]]")
1251+
a2 = a1 # E: Incompatible types in assignment (expression has type "Invariant[Literal[1]]", variable has type "Invariant[Literal[1, 2]]")
1252+
a2 = a3 # E: Incompatible types in assignment (expression has type "Invariant[Literal[1, 2, 3]]", variable has type "Invariant[Literal[1, 2]]")
12531253

12541254
b1: Covariant[Literal[1]]
12551255
b2: Covariant[Literal[1, 2]]
12561256
b3: Covariant[Literal[1, 2, 3]]
12571257
b2 = b1
1258-
b2 = b3 # E: Incompatible types in assignment (expression has type "Covariant[Union[Literal[1], Literal[2], Literal[3]]]", variable has type "Covariant[Union[Literal[1], Literal[2]]]")
1258+
b2 = b3 # E: Incompatible types in assignment (expression has type "Covariant[Literal[1, 2, 3]]", variable has type "Covariant[Literal[1, 2]]")
12591259

12601260
c1: Contravariant[Literal[1]]
12611261
c2: Contravariant[Literal[1, 2]]
12621262
c3: Contravariant[Literal[1, 2, 3]]
1263-
c2 = c1 # E: Incompatible types in assignment (expression has type "Contravariant[Literal[1]]", variable has type "Contravariant[Union[Literal[1], Literal[2]]]")
1263+
c2 = c1 # E: Incompatible types in assignment (expression has type "Contravariant[Literal[1]]", variable has type "Contravariant[Literal[1, 2]]")
12641264
c2 = c3
12651265
[builtins fixtures/tuple.pyi]
12661266
[out]
@@ -1275,12 +1275,12 @@ def bar(x: Sequence[Literal[1, 2]]) -> None: pass
12751275
a: List[Literal[1]]
12761276
b: List[Literal[1, 2, 3]]
12771277

1278-
foo(a) # E: Argument 1 to "foo" has incompatible type "List[Literal[1]]"; expected "List[Union[Literal[1], Literal[2]]]" \
1278+
foo(a) # E: Argument 1 to "foo" has incompatible type "List[Literal[1]]"; expected "List[Literal[1, 2]]" \
12791279
# N: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance \
12801280
# N: Consider using "Sequence" instead, which is covariant
1281-
foo(b) # E: Argument 1 to "foo" has incompatible type "List[Union[Literal[1], Literal[2], Literal[3]]]"; expected "List[Union[Literal[1], Literal[2]]]"
1281+
foo(b) # E: Argument 1 to "foo" has incompatible type "List[Literal[1, 2, 3]]"; expected "List[Literal[1, 2]]"
12821282
bar(a)
1283-
bar(b) # E: Argument 1 to "bar" has incompatible type "List[Union[Literal[1], Literal[2], Literal[3]]]"; expected "Sequence[Union[Literal[1], Literal[2]]]"
1283+
bar(b) # E: Argument 1 to "bar" has incompatible type "List[Literal[1, 2, 3]]"; expected "Sequence[Literal[1, 2]]"
12841284
[builtins fixtures/list.pyi]
12851285
[out]
12861286

@@ -1363,9 +1363,9 @@ x = b # E: Incompatible types in assignment (expression has type "str", variabl
13631363
y = c # E: Incompatible types in assignment (expression has type "bool", variable has type "Literal[True]")
13641364
z = d # This is ok: Literal[None] and None are equivalent.
13651365

1366-
combined = a # E: Incompatible types in assignment (expression has type "int", variable has type "Union[Literal[1], Literal['foo'], Literal[True], None]")
1367-
combined = b # E: Incompatible types in assignment (expression has type "str", variable has type "Union[Literal[1], Literal['foo'], Literal[True], None]")
1368-
combined = c # E: Incompatible types in assignment (expression has type "bool", variable has type "Union[Literal[1], Literal['foo'], Literal[True], None]")
1366+
combined = a # E: Incompatible types in assignment (expression has type "int", variable has type "Optional[Literal[1, 'foo', True]]")
1367+
combined = b # E: Incompatible types in assignment (expression has type "str", variable has type "Optional[Literal[1, 'foo', True]]")
1368+
combined = c # E: Incompatible types in assignment (expression has type "bool", variable has type "Optional[Literal[1, 'foo', True]]")
13691369
combined = d # Also ok, for similar reasons.
13701370

13711371
e: Literal[1] = 1
@@ -1392,9 +1392,9 @@ a: Literal[1] = 2 # E: Incompatible types in assignment (expression ha
13921392
b: Literal["foo"] = "bar" # E: Incompatible types in assignment (expression has type "Literal['bar']", variable has type "Literal['foo']")
13931393
c: Literal[True] = False # E: Incompatible types in assignment (expression has type "Literal[False]", variable has type "Literal[True]")
13941394

1395-
d: Literal[1, 2] = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Union[Literal[1], Literal[2]]")
1396-
e: Literal["foo", "bar"] = "baz" # E: Incompatible types in assignment (expression has type "Literal['baz']", variable has type "Union[Literal['foo'], Literal['bar']]")
1397-
f: Literal[True, 4] = False # E: Incompatible types in assignment (expression has type "Literal[False]", variable has type "Union[Literal[True], Literal[4]]")
1395+
d: Literal[1, 2] = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Literal[1, 2]")
1396+
e: Literal["foo", "bar"] = "baz" # E: Incompatible types in assignment (expression has type "Literal['baz']", variable has type "Literal['foo', 'bar']")
1397+
f: Literal[True, 4] = False # E: Incompatible types in assignment (expression has type "Literal[False]", variable has type "Literal[True, 4]")
13981398

13991399
[builtins fixtures/primitives.pyi]
14001400
[out]
@@ -1504,7 +1504,7 @@ reveal_type(arr3) # N: Revealed type is "builtins.list[builtins.int*]"
15041504
reveal_type(arr4) # N: Revealed type is "builtins.list[builtins.object*]"
15051505
reveal_type(arr5) # N: Revealed type is "builtins.list[builtins.object*]"
15061506

1507-
bad: List[Literal[1, 2]] = [1, 2, 3] # E: List item 2 has incompatible type "Literal[3]"; expected "Union[Literal[1], Literal[2]]"
1507+
bad: List[Literal[1, 2]] = [1, 2, 3] # E: List item 2 has incompatible type "Literal[3]"; expected "Literal[1, 2]"
15081508

15091509
[builtins fixtures/list.pyi]
15101510
[out]
@@ -1619,19 +1619,19 @@ reveal_type(func(a)) # N: Revealed type is "Union[__main__.A, __main__.C]"
16191619
reveal_type(func(b)) # N: Revealed type is "__main__.B"
16201620
reveal_type(func(c)) # N: Revealed type is "Union[__main__.B, __main__.A]"
16211621
reveal_type(func(d)) # N: Revealed type is "__main__.B" \
1622-
# E: Argument 1 to "func" has incompatible type "Union[Literal[6], Literal[7]]"; expected "Union[Literal[3], Literal[4], Literal[5], Literal[6]]"
1622+
# E: Argument 1 to "func" has incompatible type "Literal[6, 7]"; expected "Literal[3, 4, 5, 6]"
16231623

16241624
reveal_type(func(e)) # E: No overload variant of "func" matches argument type "int" \
16251625
# N: Possible overload variants: \
16261626
# N: def func(x: Literal[-40]) -> A \
1627-
# N: def func(x: Union[Literal[3], Literal[4], Literal[5], Literal[6]]) -> B \
1627+
# N: def func(x: Literal[3, 4, 5, 6]) -> B \
16281628
# N: def func(x: Literal['foo']) -> C \
16291629
# N: Revealed type is "Any"
16301630

1631-
reveal_type(func(f)) # E: No overload variant of "func" matches argument type "Union[Literal[7], Literal['bar']]" \
1631+
reveal_type(func(f)) # E: No overload variant of "func" matches argument type "Literal[7, 'bar']" \
16321632
# N: Possible overload variants: \
16331633
# N: def func(x: Literal[-40]) -> A \
1634-
# N: def func(x: Union[Literal[3], Literal[4], Literal[5], Literal[6]]) -> B \
1634+
# N: def func(x: Literal[3, 4, 5, 6]) -> B \
16351635
# N: def func(x: Literal['foo']) -> C \
16361636
# N: Revealed type is "Any"
16371637
[builtins fixtures/tuple.pyi]
@@ -1657,7 +1657,7 @@ reveal_type(f(1)) # N: Revealed type is "builtins.int"
16571657
reveal_type(f(2)) # N: Revealed type is "builtins.int"
16581658
reveal_type(f(y)) # N: Revealed type is "builtins.object"
16591659
reveal_type(f(z)) # N: Revealed type is "builtins.int" \
1660-
# E: Argument 1 to "f" has incompatible type "Union[Literal[1], Literal[2], Literal['three']]"; expected "Union[Literal[1], Literal[2]]"
1660+
# E: Argument 1 to "f" has incompatible type "Literal[1, 2, 'three']"; expected "Literal[1, 2]"
16611661
[builtins fixtures/tuple.pyi]
16621662
[out]
16631663

@@ -1726,8 +1726,8 @@ def f(x):
17261726

17271727
x: Literal['a', 'b']
17281728
y: Literal['a', 'b']
1729-
f(x, y) # E: Argument 1 to "f" has incompatible type "Union[Literal['a'], Literal['b']]"; expected "Literal['a']" \
1730-
# E: Argument 2 to "f" has incompatible type "Union[Literal['a'], Literal['b']]"; expected "Literal['a']" \
1729+
f(x, y) # E: Argument 1 to "f" has incompatible type "Literal['a', 'b']"; expected "Literal['a']" \
1730+
# E: Argument 2 to "f" has incompatible type "Literal['a', 'b']"; expected "Literal['a']" \
17311731
[builtins fixtures/tuple.pyi]
17321732
[out]
17331733

test-data/unit/check-narrowing.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,7 @@ else:
894894
reveal_type(y) # N: Revealed type is "__main__.Custom"
895895

896896
# No contamination here
897-
if 1 == x == z: # E: Non-overlapping equality check (left operand type: "Union[Literal[1], Literal[2], None]", right operand type: "Default")
897+
if 1 == x == z: # E: Non-overlapping equality check (left operand type: "Optional[Literal[1, 2]]", right operand type: "Default")
898898
reveal_type(x) # E: Statement is unreachable
899899
reveal_type(z)
900900
else:

0 commit comments

Comments
 (0)