Skip to content

Use (simplified) unions instead of joins for tuple fallbacks #17408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add some shortening for long unions
  • Loading branch information
ilevkivskyi committed Jun 22, 2024
commit 59ab31c578516dd72782b347ce1044c0b0b0a1fd
27 changes: 20 additions & 7 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@
"numbers.Integral",
}

MAX_TUPLE_ITEMS = 10
MAX_UNION_ITEMS = 10


class MessageBuilder:
"""Helper class for reporting type checker error messages with parameters.
Expand Down Expand Up @@ -2338,7 +2341,7 @@ def try_report_long_tuple_assignment_error(
"""
if isinstance(subtype, TupleType):
if (
len(subtype.items) > 10
len(subtype.items) > MAX_TUPLE_ITEMS
and isinstance(supertype, Instance)
and supertype.type.fullname == "builtins.tuple"
):
Expand All @@ -2347,7 +2350,7 @@ def try_report_long_tuple_assignment_error(
self.generate_incompatible_tuple_error(lhs_types, subtype.items, context, msg)
return True
elif isinstance(supertype, TupleType) and (
len(subtype.items) > 10 or len(supertype.items) > 10
len(subtype.items) > MAX_TUPLE_ITEMS or len(supertype.items) > MAX_TUPLE_ITEMS
):
if len(subtype.items) != len(supertype.items):
if supertype_label is not None and subtype_label is not None:
Expand All @@ -2370,7 +2373,7 @@ def try_report_long_tuple_assignment_error(
def format_long_tuple_type(self, typ: TupleType) -> str:
"""Format very long tuple type using an ellipsis notation"""
item_cnt = len(typ.items)
if item_cnt > 10:
if item_cnt > MAX_TUPLE_ITEMS:
return "{}[{}, {}, ... <{} more items>]".format(
"tuple" if self.options.use_lowercase_names() else "Tuple",
format_type_bare(typ.items[0], self.options),
Expand Down Expand Up @@ -2497,11 +2500,21 @@ def format(typ: Type) -> str:
def format_list(types: Sequence[Type]) -> str:
return ", ".join(format(typ) for typ in types)

def format_union(types: Sequence[Type]) -> str:
def format_union_items(types: Sequence[Type]) -> list[str]:
formatted = [format(typ) for typ in types if format(typ) != "None"]
if len(formatted) > MAX_UNION_ITEMS and verbosity == 0:
more = len(formatted) - MAX_UNION_ITEMS // 2
formatted = formatted[:MAX_UNION_ITEMS // 2]
else:
more = 0
if more:
formatted.append(f"<{more} more items>")
if any(format(typ) == "None" for typ in types):
formatted.append("None")
return " | ".join(formatted)
return formatted

def format_union(types: Sequence[Type]) -> str:
return " | ".join(format_union_items(types))

def format_literal_value(typ: LiteralType) -> str:
if typ.is_enum_literal():
Expand Down Expand Up @@ -2627,7 +2640,7 @@ def format_literal_value(typ: LiteralType) -> str:
return (
f"{literal_str} | {format_union(union_items)}"
if options.use_or_syntax()
else f"Union[{format_list(union_items)}, {literal_str}]"
else f"Union[{', '.join(format_union_items(union_items))}, {literal_str}]"
)
else:
return literal_str
Expand All @@ -2648,7 +2661,7 @@ def format_literal_value(typ: LiteralType) -> str:
s = (
format_union(typ.items)
if options.use_or_syntax()
else f"Union[{format_list(typ.items)}]"
else f"Union[{', '.join(format_union_items(typ.items))}]"
)
return s
elif isinstance(typ, NoneType):
Expand Down
12 changes: 9 additions & 3 deletions test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,7 @@ _empty: Final = Empty.token
def func(x: Union[int, None, Empty] = _empty) -> int:
boom = x + 42 # E: Unsupported left operand type for + ("None") \
# E: Unsupported left operand type for + ("Empty") \
# N: Left operand is of type "Union[int, None, Empty]"
# N: Left operand is of type "Union[int, Empty, None]"
if x is _empty:
reveal_type(x) # N: Revealed type is "Literal[__main__.Empty.token]"
return 0
Expand All @@ -1020,6 +1020,8 @@ def func(x: Union[int, None, Empty] = _empty) -> int:
else: # At this point typechecker knows that x can only have type int
reveal_type(x) # N: Revealed type is "builtins.int"
return x + 2


[builtins fixtures/primitives.pyi]

[case testEnumReachabilityPEP484ExampleWithMultipleValues]
Expand Down Expand Up @@ -1056,7 +1058,7 @@ _empty = Empty.token
def func(x: Union[int, None, Empty] = _empty) -> int:
boom = x + 42 # E: Unsupported left operand type for + ("None") \
# E: Unsupported left operand type for + ("Empty") \
# N: Left operand is of type "Union[int, None, Empty]"
# N: Left operand is of type "Union[int, Empty, None]"
if x is _empty:
reveal_type(x) # N: Revealed type is "Literal[__main__.Empty.token]"
return 0
Expand All @@ -1066,6 +1068,8 @@ def func(x: Union[int, None, Empty] = _empty) -> int:
else: # At this point typechecker knows that x can only have type int
reveal_type(x) # N: Revealed type is "builtins.int"
return x + 2


[builtins fixtures/primitives.pyi]

[case testEnumReachabilityPEP484ExampleSingletonWithMethod]
Expand All @@ -1084,7 +1088,7 @@ _empty = Empty.token
def func(x: Union[int, None, Empty] = _empty) -> int:
boom = x + 42 # E: Unsupported left operand type for + ("None") \
# E: Unsupported left operand type for + ("Empty") \
# N: Left operand is of type "Union[int, None, Empty]"
# N: Left operand is of type "Union[int, Empty, None]"
if x is _empty:
reveal_type(x) # N: Revealed type is "Literal[__main__.Empty.token]"
return 0
Expand All @@ -1094,6 +1098,8 @@ def func(x: Union[int, None, Empty] = _empty) -> int:
else: # At this point typechecker knows that x can only have type int
reveal_type(x) # N: Revealed type is "builtins.int"
return x + 2


[builtins fixtures/primitives.pyi]

[case testAssignEnumAsAttribute]
Expand Down
55 changes: 55 additions & 0 deletions test-data/unit/check-unions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1289,3 +1289,58 @@ x: str = a_class_or_none.field
a_or_none: Optional[A]
y: int = a_or_none.field
[builtins fixtures/list.pyi]

[case testLargeUnionsShort]
from typing import Union

class C1: ...
class C2: ...
class C3: ...
class C4: ...
class C5: ...
class C6: ...
class C7: ...
class C8: ...
class C9: ...
class C10: ...
class C11: ...

u: Union[C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11]
x: int = u # E: Incompatible types in assignment (expression has type "Union[C1, C2, C3, C4, C5, <6 more items>]", variable has type "int")

[case testLargeUnionsLongIfNeeded]
from typing import Union

class C1: ...
class C2: ...
class C3: ...
class C4: ...
class C5: ...
class C6: ...
class C7: ...
class C8: ...
class C9: ...
class C10: ...

x: Union[C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, int]
y: Union[C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, str]
x = y # E: Incompatible types in assignment (expression has type "Union[C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, str]", variable has type "Union[C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, int]")

[case testLargeUnionsNoneShown]
from typing import Union

class C1: ...
class C2: ...
class C3: ...
class C4: ...
class C5: ...
class C6: ...
class C7: ...
class C8: ...
class C9: ...
class C10: ...
class C11: ...

x: Union[C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11]
y: Union[C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11, None]
x = y # E: Incompatible types in assignment (expression has type "Union[C1, C2, C3, C4, C5, <6 more items>, None]", variable has type "Union[C1, C2, C3, C4, C5, <6 more items>]")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I like this idea. I think I'd prefer to see the exact type mypy has inferred in an error message, even if it means the message is very verbose. I'd much prefer to have all the information I might need to debug the issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For debugging you can still use some reveal_type()s. In any case, we already do this in similar scenarios for long overloads, long tuples, and large protocols (and maybe some other situations I am not aware of), so this makes it more consistent.

Actually now that I am writing this, one thing that may be worth considering is some kind of stable sort for union items and/or flattening nesting unions (we already flatten most unions, but not those that are targets of type aliases). I will play with this locally now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, actually it looks worse when sorted. It seems more natural to see the same order as in the definitions. I would propose to just move on. I will be happy to revert this, if people will actually start complaining.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree having them in the order given in the source code is definitely better.

I can remember times when omitting some overloads in the error message given to the user has made some issues much harder for me to debug over at typeshed, so I'm still not sure about abbreviating the union here. But I can see that it's consistent with what mypy does in other areas, so I won't die on this hill :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last thing that I just tried locally and seems good is to add a specific note for long unions about which subtype item(s) cause problem.

Loading