Skip to content

Commit abdaf6a

Browse files
ilevkivskyipre-commit-ci[bot]AlexWaygood
authored
Use (simplified) unions instead of joins for tuple fallbacks (#17408)
Ref #12056 If `mypy_primer` will look good, I will add some logic to shorted unions in error messages. cc @JukkaL --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Alex Waygood <[email protected]>
1 parent 9012fc9 commit abdaf6a

13 files changed

+146
-35
lines changed

mypy/checker.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
SUGGESTED_TEST_FIXTURES,
5050
MessageBuilder,
5151
append_invariance_notes,
52+
append_union_note,
5253
format_type,
5354
format_type_bare,
5455
format_type_distinctly,
@@ -6814,6 +6815,8 @@ def check_subtype(
68146815
)
68156816
if isinstance(subtype, Instance) and isinstance(supertype, Instance):
68166817
notes = append_invariance_notes(notes, subtype, supertype)
6818+
if isinstance(subtype, UnionType) and isinstance(supertype, UnionType):
6819+
notes = append_union_note(notes, subtype, supertype, self.options)
68176820
if extra_info:
68186821
msg = msg.with_additional_msg(" (" + ", ".join(extra_info) + ")")
68196822

mypy/messages.py

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
UninhabitedType,
9191
UnionType,
9292
UnpackType,
93+
flatten_nested_unions,
9394
get_proper_type,
9495
get_proper_types,
9596
)
@@ -145,6 +146,9 @@
145146
"numbers.Integral",
146147
}
147148

149+
MAX_TUPLE_ITEMS = 10
150+
MAX_UNION_ITEMS = 10
151+
148152

149153
class MessageBuilder:
150154
"""Helper class for reporting type checker error messages with parameters.
@@ -2338,7 +2342,7 @@ def try_report_long_tuple_assignment_error(
23382342
"""
23392343
if isinstance(subtype, TupleType):
23402344
if (
2341-
len(subtype.items) > 10
2345+
len(subtype.items) > MAX_TUPLE_ITEMS
23422346
and isinstance(supertype, Instance)
23432347
and supertype.type.fullname == "builtins.tuple"
23442348
):
@@ -2347,7 +2351,7 @@ def try_report_long_tuple_assignment_error(
23472351
self.generate_incompatible_tuple_error(lhs_types, subtype.items, context, msg)
23482352
return True
23492353
elif isinstance(supertype, TupleType) and (
2350-
len(subtype.items) > 10 or len(supertype.items) > 10
2354+
len(subtype.items) > MAX_TUPLE_ITEMS or len(supertype.items) > MAX_TUPLE_ITEMS
23512355
):
23522356
if len(subtype.items) != len(supertype.items):
23532357
if supertype_label is not None and subtype_label is not None:
@@ -2370,7 +2374,7 @@ def try_report_long_tuple_assignment_error(
23702374
def format_long_tuple_type(self, typ: TupleType) -> str:
23712375
"""Format very long tuple type using an ellipsis notation"""
23722376
item_cnt = len(typ.items)
2373-
if item_cnt > 10:
2377+
if item_cnt > MAX_TUPLE_ITEMS:
23742378
return "{}[{}, {}, ... <{} more items>]".format(
23752379
"tuple" if self.options.use_lowercase_names() else "Tuple",
23762380
format_type_bare(typ.items[0], self.options),
@@ -2497,11 +2501,21 @@ def format(typ: Type) -> str:
24972501
def format_list(types: Sequence[Type]) -> str:
24982502
return ", ".join(format(typ) for typ in types)
24992503

2500-
def format_union(types: Sequence[Type]) -> str:
2504+
def format_union_items(types: Sequence[Type]) -> list[str]:
25012505
formatted = [format(typ) for typ in types if format(typ) != "None"]
2506+
if len(formatted) > MAX_UNION_ITEMS and verbosity == 0:
2507+
more = len(formatted) - MAX_UNION_ITEMS // 2
2508+
formatted = formatted[: MAX_UNION_ITEMS // 2]
2509+
else:
2510+
more = 0
2511+
if more:
2512+
formatted.append(f"<{more} more items>")
25022513
if any(format(typ) == "None" for typ in types):
25032514
formatted.append("None")
2504-
return " | ".join(formatted)
2515+
return formatted
2516+
2517+
def format_union(types: Sequence[Type]) -> str:
2518+
return " | ".join(format_union_items(types))
25052519

25062520
def format_literal_value(typ: LiteralType) -> str:
25072521
if typ.is_enum_literal():
@@ -2605,6 +2619,9 @@ def format_literal_value(typ: LiteralType) -> str:
26052619
elif isinstance(typ, LiteralType):
26062620
return f"Literal[{format_literal_value(typ)}]"
26072621
elif isinstance(typ, UnionType):
2622+
typ = get_proper_type(ignore_last_known_values(typ))
2623+
if not isinstance(typ, UnionType):
2624+
return format(typ)
26082625
literal_items, union_items = separate_union_literals(typ)
26092626

26102627
# Coalesce multiple Literal[] members. This also changes output order.
@@ -2624,7 +2641,7 @@ def format_literal_value(typ: LiteralType) -> str:
26242641
return (
26252642
f"{literal_str} | {format_union(union_items)}"
26262643
if options.use_or_syntax()
2627-
else f"Union[{format_list(union_items)}, {literal_str}]"
2644+
else f"Union[{', '.join(format_union_items(union_items))}, {literal_str}]"
26282645
)
26292646
else:
26302647
return literal_str
@@ -2645,7 +2662,7 @@ def format_literal_value(typ: LiteralType) -> str:
26452662
s = (
26462663
format_union(typ.items)
26472664
if options.use_or_syntax()
2648-
else f"Union[{format_list(typ.items)}]"
2665+
else f"Union[{', '.join(format_union_items(typ.items))}]"
26492666
)
26502667
return s
26512668
elif isinstance(typ, NoneType):
@@ -3182,6 +3199,23 @@ def append_invariance_notes(
31823199
return notes
31833200

31843201

3202+
def append_union_note(
3203+
notes: list[str], arg_type: UnionType, expected_type: UnionType, options: Options
3204+
) -> list[str]:
3205+
"""Point to specific union item(s) that may cause failure in subtype check."""
3206+
non_matching = []
3207+
items = flatten_nested_unions(arg_type.items)
3208+
if len(items) < MAX_UNION_ITEMS:
3209+
return notes
3210+
for item in items:
3211+
if not is_subtype(item, expected_type):
3212+
non_matching.append(item)
3213+
if non_matching:
3214+
types = ", ".join([format_type(typ, options) for typ in non_matching])
3215+
notes.append(f"Item{plural_s(non_matching)} in the first union not in the second: {types}")
3216+
return notes
3217+
3218+
31853219
def append_numbers_notes(
31863220
notes: list[str], arg_type: Instance, expected_type: Instance
31873221
) -> list[str]:
@@ -3235,3 +3269,23 @@ def format_key_list(keys: list[str], *, short: bool = False) -> str:
32353269
return f"{td}key {formatted_keys[0]}"
32363270
else:
32373271
return f"{td}keys ({', '.join(formatted_keys)})"
3272+
3273+
3274+
def ignore_last_known_values(t: UnionType) -> Type:
3275+
"""This will avoid types like str | str in error messages.
3276+
3277+
last_known_values are kept during union simplification, but may cause
3278+
weird formatting for e.g. tuples of literals.
3279+
"""
3280+
union_items: list[Type] = []
3281+
seen_instances = set()
3282+
for item in t.items:
3283+
if isinstance(item, ProperType) and isinstance(item, Instance):
3284+
erased = item.copy_modified(last_known_value=None)
3285+
if erased in seen_instances:
3286+
continue
3287+
seen_instances.add(erased)
3288+
union_items.append(erased)
3289+
else:
3290+
union_items.append(item)
3291+
return UnionType.make_union(union_items, t.line, t.column)

mypy/semanal_shared.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
from mypy_extensions import trait
1010

11-
from mypy import join
1211
from mypy.errorcodes import LITERAL_REQ, ErrorCode
1312
from mypy.nodes import (
1413
CallExpr,
@@ -30,6 +29,7 @@
3029
from mypy.plugin import SemanticAnalyzerPluginInterface
3130
from mypy.tvar_scope import TypeVarLikeScope
3231
from mypy.type_visitor import ANY_STRATEGY, BoolTypeQuery
32+
from mypy.typeops import make_simplified_union
3333
from mypy.types import (
3434
TPDICT_FB_NAMES,
3535
AnyType,
@@ -58,7 +58,7 @@
5858
# Priorities for ordering of patches within the "patch" phase of semantic analysis
5959
# (after the main pass):
6060

61-
# Fix fallbacks (does joins)
61+
# Fix fallbacks (does subtype checks).
6262
PRIORITY_FALLBACKS: Final = 1
6363

6464

@@ -304,7 +304,7 @@ def calculate_tuple_fallback(typ: TupleType) -> None:
304304
raise NotImplementedError
305305
else:
306306
items.append(item)
307-
fallback.args = (join.join_type_list(items),)
307+
fallback.args = (make_simplified_union(items),)
308308

309309

310310
class _NamedTypeCallback(Protocol):

mypy/typeops.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ def is_recursive_pair(s: Type, t: Type) -> bool:
9595

9696
def tuple_fallback(typ: TupleType) -> Instance:
9797
"""Return fallback type for a tuple."""
98-
from mypy.join import join_type_list
99-
10098
info = typ.partial_fallback.type
10199
if info.fullname != "builtins.tuple":
102100
return typ.partial_fallback
@@ -115,8 +113,9 @@ def tuple_fallback(typ: TupleType) -> Instance:
115113
raise NotImplementedError
116114
else:
117115
items.append(item)
118-
# TODO: we should really use a union here, tuple types are special.
119-
return Instance(info, [join_type_list(items)], extra_attrs=typ.partial_fallback.extra_attrs)
116+
return Instance(
117+
info, [make_simplified_union(items)], extra_attrs=typ.partial_fallback.extra_attrs
118+
)
120119

121120

122121
def get_self_type(func: CallableType, default_self: Instance | TupleType) -> Type | None:

test-data/unit/check-enum.test

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,7 @@ _empty: Final = Empty.token
10101010
def func(x: Union[int, None, Empty] = _empty) -> int:
10111011
boom = x + 42 # E: Unsupported left operand type for + ("None") \
10121012
# E: Unsupported left operand type for + ("Empty") \
1013-
# N: Left operand is of type "Union[int, None, Empty]"
1013+
# N: Left operand is of type "Union[int, Empty, None]"
10141014
if x is _empty:
10151015
reveal_type(x) # N: Revealed type is "Literal[__main__.Empty.token]"
10161016
return 0
@@ -1056,7 +1056,7 @@ _empty = Empty.token
10561056
def func(x: Union[int, None, Empty] = _empty) -> int:
10571057
boom = x + 42 # E: Unsupported left operand type for + ("None") \
10581058
# E: Unsupported left operand type for + ("Empty") \
1059-
# N: Left operand is of type "Union[int, None, Empty]"
1059+
# N: Left operand is of type "Union[int, Empty, None]"
10601060
if x is _empty:
10611061
reveal_type(x) # N: Revealed type is "Literal[__main__.Empty.token]"
10621062
return 0
@@ -1084,7 +1084,7 @@ _empty = Empty.token
10841084
def func(x: Union[int, None, Empty] = _empty) -> int:
10851085
boom = x + 42 # E: Unsupported left operand type for + ("None") \
10861086
# E: Unsupported left operand type for + ("Empty") \
1087-
# N: Left operand is of type "Union[int, None, Empty]"
1087+
# N: Left operand is of type "Union[int, Empty, None]"
10881088
if x is _empty:
10891089
reveal_type(x) # N: Revealed type is "Literal[__main__.Empty.token]"
10901090
return 0

test-data/unit/check-expressions.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1640,7 +1640,7 @@ from typing import Generator
16401640
def g() -> Generator[int, None, None]:
16411641
x = yield from () # E: Function does not return a value (it only ever returns None)
16421642
x = yield from (0, 1, 2) # E: Function does not return a value (it only ever returns None)
1643-
x = yield from (0, "ERROR") # E: Incompatible types in "yield from" (actual type "object", expected type "int") \
1643+
x = yield from (0, "ERROR") # E: Incompatible types in "yield from" (actual type "Union[int, str]", expected type "int") \
16441644
# E: Function does not return a value (it only ever returns None)
16451645
x = yield from ("ERROR",) # E: Incompatible types in "yield from" (actual type "str", expected type "int") \
16461646
# E: Function does not return a value (it only ever returns None)

test-data/unit/check-namedtuple.test

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,7 @@ nti: NT[int]
12491249
reveal_type(nti * x) # N: Revealed type is "builtins.tuple[builtins.int, ...]"
12501250

12511251
nts: NT[str]
1252-
reveal_type(nts * x) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
1252+
reveal_type(nts * x) # N: Revealed type is "builtins.tuple[Union[builtins.int, builtins.str], ...]"
12531253
[builtins fixtures/tuple.pyi]
12541254
[typing fixtures/typing-namedtuple.pyi]
12551255

@@ -1310,9 +1310,9 @@ reveal_type(foo(nti, nts)) # N: Revealed type is "Tuple[builtins.int, builtins.
13101310
reveal_type(foo(nts, nti)) # N: Revealed type is "Tuple[builtins.int, builtins.object, fallback=__main__.NT[builtins.object]]"
13111311

13121312
reveal_type(foo(nti, x)) # N: Revealed type is "builtins.tuple[builtins.int, ...]"
1313-
reveal_type(foo(nts, x)) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
1313+
reveal_type(foo(nts, x)) # N: Revealed type is "builtins.tuple[Union[builtins.int, builtins.str], ...]"
13141314
reveal_type(foo(x, nti)) # N: Revealed type is "builtins.tuple[builtins.int, ...]"
1315-
reveal_type(foo(x, nts)) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
1315+
reveal_type(foo(x, nts)) # N: Revealed type is "builtins.tuple[Union[builtins.int, builtins.str], ...]"
13161316
[builtins fixtures/tuple.pyi]
13171317
[typing fixtures/typing-namedtuple.pyi]
13181318

test-data/unit/check-newsemanal.test

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1947,7 +1947,7 @@ class NTStr(NamedTuple):
19471947
y: str
19481948

19491949
t1: T
1950-
reveal_type(t1.__iter__) # N: Revealed type is "def () -> typing.Iterator[__main__.A]"
1950+
reveal_type(t1.__iter__) # N: Revealed type is "def () -> typing.Iterator[Union[__main__.B, __main__.C]]"
19511951

19521952
t2: NTInt
19531953
reveal_type(t2.__iter__) # N: Revealed type is "def () -> typing.Iterator[builtins.int]"
@@ -1960,7 +1960,6 @@ t: Union[Tuple[int, int], Tuple[str, str]]
19601960
for x in t:
19611961
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
19621962
[builtins fixtures/for.pyi]
1963-
[out]
19641963

19651964
[case testNewAnalyzerFallbackUpperBoundCheckAndFallbacks]
19661965
from typing import TypeVar, Generic, Tuple
@@ -1973,18 +1972,17 @@ S = TypeVar('S', bound='Tuple[G[A], ...]')
19731972

19741973
class GG(Generic[S]): pass
19751974

1976-
g: GG[Tuple[G[B], G[C]]] \
1977-
# E: Type argument "Tuple[G[B], G[C]]" of "GG" must be a subtype of "Tuple[G[A], ...]" \
1978-
# E: Type argument "B" of "G" must be a subtype of "A" \
1979-
# E: Type argument "C" of "G" must be a subtype of "A"
1975+
g: GG[Tuple[G[B], G[C]]] # E: Type argument "Tuple[G[B], G[C]]" of "GG" must be a subtype of "Tuple[G[A], ...]" \
1976+
# E: Type argument "B" of "G" must be a subtype of "A" \
1977+
# E: Type argument "C" of "G" must be a subtype of "A"
19801978

19811979
T = TypeVar('T', bound=A, covariant=True)
19821980

19831981
class G(Generic[T]): pass
19841982

19851983
t: Tuple[G[B], G[C]] # E: Type argument "B" of "G" must be a subtype of "A" \
19861984
# E: Type argument "C" of "G" must be a subtype of "A"
1987-
reveal_type(t.__iter__) # N: Revealed type is "def () -> typing.Iterator[builtins.object]"
1985+
reveal_type(t.__iter__) # N: Revealed type is "def () -> typing.Iterator[__main__.G[__main__.B]]"
19881986
[builtins fixtures/tuple.pyi]
19891987

19901988
[case testNewAnalyzerClassKeywordsForward]

test-data/unit/check-statements.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1339,7 +1339,7 @@ from typing import Generator
13391339
def g() -> Generator[int, None, None]:
13401340
yield from ()
13411341
yield from (0, 1, 2)
1342-
yield from (0, "ERROR") # E: Incompatible types in "yield from" (actual type "object", expected type "int")
1342+
yield from (0, "ERROR") # E: Incompatible types in "yield from" (actual type "Union[int, str]", expected type "int")
13431343
yield from ("ERROR",) # E: Incompatible types in "yield from" (actual type "str", expected type "int")
13441344
[builtins fixtures/tuple.pyi]
13451345

test-data/unit/check-tuples.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,8 +1408,8 @@ y = ""
14081408
reveal_type(t[x]) # N: Revealed type is "Union[builtins.int, builtins.str]"
14091409
t[y] # E: No overload variant of "__getitem__" of "tuple" matches argument type "str" \
14101410
# N: Possible overload variants: \
1411-
# N: def __getitem__(self, int, /) -> object \
1412-
# N: def __getitem__(self, slice, /) -> Tuple[object, ...]
1411+
# N: def __getitem__(self, int, /) -> Union[int, str] \
1412+
# N: def __getitem__(self, slice, /) -> Tuple[Union[int, str], ...]
14131413

14141414
[builtins fixtures/tuple.pyi]
14151415

test-data/unit/check-typevar-tuple.test

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def g(a: Tuple[Unpack[Ts]], b: Tuple[Unpack[Ts]]) -> Tuple[Unpack[Ts]]:
2424

2525
reveal_type(g(args, args)) # N: Revealed type is "Tuple[builtins.int, builtins.str]"
2626
reveal_type(g(args, args2)) # N: Revealed type is "Tuple[builtins.int, builtins.str]"
27-
reveal_type(g(args, args3)) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
27+
reveal_type(g(args, args3)) # N: Revealed type is "builtins.tuple[Union[builtins.int, builtins.str], ...]"
2828
reveal_type(g(any, any)) # N: Revealed type is "builtins.tuple[Any, ...]"
2929
[builtins fixtures/tuple.pyi]
3030

@@ -989,7 +989,7 @@ from typing_extensions import Unpack
989989

990990
def pipeline(*xs: Unpack[Tuple[int, Unpack[Tuple[float, ...]], bool]]) -> None:
991991
for x in xs:
992-
reveal_type(x) # N: Revealed type is "builtins.float"
992+
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.float]"
993993
[builtins fixtures/tuple.pyi]
994994

995995
[case testFixedUnpackItemInInstanceArguments]
@@ -1715,7 +1715,7 @@ vt: Tuple[int, Unpack[Tuple[float, ...]], int]
17151715

17161716
reveal_type(vt + (1, 2)) # N: Revealed type is "Tuple[builtins.int, Unpack[builtins.tuple[builtins.float, ...]], builtins.int, Literal[1]?, Literal[2]?]"
17171717
reveal_type((1, 2) + vt) # N: Revealed type is "Tuple[Literal[1]?, Literal[2]?, builtins.int, Unpack[builtins.tuple[builtins.float, ...]], builtins.int]"
1718-
reveal_type(vt + vt) # N: Revealed type is "builtins.tuple[builtins.float, ...]"
1718+
reveal_type(vt + vt) # N: Revealed type is "builtins.tuple[Union[builtins.int, builtins.float], ...]"
17191719
reveal_type(vtf + (1, 2)) # N: Revealed type is "Tuple[Unpack[builtins.tuple[builtins.float, ...]], Literal[1]?, Literal[2]?]"
17201720
reveal_type((1, 2) + vtf) # N: Revealed type is "Tuple[Literal[1]?, Literal[2]?, Unpack[builtins.tuple[builtins.float, ...]]]"
17211721

0 commit comments

Comments
 (0)