Skip to content
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

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

Merged
merged 7 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
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
3 changes: 3 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
SUGGESTED_TEST_FIXTURES,
MessageBuilder,
append_invariance_notes,
append_union_note,
format_type,
format_type_bare,
format_type_distinctly,
Expand Down Expand Up @@ -6814,6 +6815,8 @@ def check_subtype(
)
if isinstance(subtype, Instance) and isinstance(supertype, Instance):
notes = append_invariance_notes(notes, subtype, supertype)
if isinstance(subtype, UnionType) and isinstance(supertype, UnionType):
notes = append_union_note(notes, subtype, supertype, self.options)
if extra_info:
msg = msg.with_additional_msg(" (" + ", ".join(extra_info) + ")")

Expand Down
68 changes: 61 additions & 7 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
UninhabitedType,
UnionType,
UnpackType,
flatten_nested_unions,
get_proper_type,
get_proper_types,
)
Expand Down Expand Up @@ -145,6 +146,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 +2342,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 +2351,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 +2374,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 +2501,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 @@ -2605,6 +2619,9 @@ def format_literal_value(typ: LiteralType) -> str:
elif isinstance(typ, LiteralType):
return f"Literal[{format_literal_value(typ)}]"
elif isinstance(typ, UnionType):
typ = get_proper_type(ignore_last_known_values(typ))
if not isinstance(typ, UnionType):
return format(typ)
literal_items, union_items = separate_union_literals(typ)

# Coalesce multiple Literal[] members. This also changes output order.
Expand All @@ -2624,7 +2641,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 @@ -2645,7 +2662,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 Expand Up @@ -3182,6 +3199,23 @@ def append_invariance_notes(
return notes


def append_union_note(
notes: list[str], arg_type: UnionType, expected_type: UnionType, options: Options
) -> list[str]:
"""Point to specific union item(s) that may cause failure in subtype check."""
non_matching = []
items = flatten_nested_unions(arg_type.items)
if len(items) < MAX_UNION_ITEMS:
return notes
for item in items:
if not is_subtype(item, expected_type):
non_matching.append(item)
if non_matching:
types = ", ".join([format_type(typ, options) for typ in non_matching])
notes.append(f"Item{plural_s(non_matching)} in the first union not in the second: {types}")
return notes


def append_numbers_notes(
notes: list[str], arg_type: Instance, expected_type: Instance
) -> list[str]:
Expand Down Expand Up @@ -3235,3 +3269,23 @@ def format_key_list(keys: list[str], *, short: bool = False) -> str:
return f"{td}key {formatted_keys[0]}"
else:
return f"{td}keys ({', '.join(formatted_keys)})"


def ignore_last_known_values(t: UnionType) -> Type:
"""This will avoid types like str | str in error messages.

last_known_values are kept during union simplification, but may cause
weird formatting for e.g. tuples of literals.
"""
union_items: list[Type] = []
seen_instances = set()
for item in t.items:
if isinstance(item, ProperType) and isinstance(item, Instance):
erased = item.copy_modified(last_known_value=None)
if erased in seen_instances:
continue
seen_instances.add(erased)
union_items.append(erased)
else:
union_items.append(item)
return UnionType.make_union(union_items, t.line, t.column)
6 changes: 3 additions & 3 deletions mypy/semanal_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

from mypy_extensions import trait

from mypy import join
from mypy.errorcodes import LITERAL_REQ, ErrorCode
from mypy.nodes import (
CallExpr,
Expand All @@ -30,6 +29,7 @@
from mypy.plugin import SemanticAnalyzerPluginInterface
from mypy.tvar_scope import TypeVarLikeScope
from mypy.type_visitor import ANY_STRATEGY, BoolTypeQuery
from mypy.typeops import make_simplified_union
from mypy.types import (
TPDICT_FB_NAMES,
AnyType,
Expand Down Expand Up @@ -58,7 +58,7 @@
# Priorities for ordering of patches within the "patch" phase of semantic analysis
# (after the main pass):

# Fix fallbacks (does joins)
# Fix fallbacks (does subtype checks).
PRIORITY_FALLBACKS: Final = 1


Expand Down Expand Up @@ -304,7 +304,7 @@ def calculate_tuple_fallback(typ: TupleType) -> None:
raise NotImplementedError
else:
items.append(item)
fallback.args = (join.join_type_list(items),)
fallback.args = (make_simplified_union(items),)


class _NamedTypeCallback(Protocol):
Expand Down
7 changes: 3 additions & 4 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,6 @@ def is_recursive_pair(s: Type, t: Type) -> bool:

def tuple_fallback(typ: TupleType) -> Instance:
"""Return fallback type for a tuple."""
from mypy.join import join_type_list

info = typ.partial_fallback.type
if info.fullname != "builtins.tuple":
return typ.partial_fallback
Expand All @@ -115,8 +113,9 @@ def tuple_fallback(typ: TupleType) -> Instance:
raise NotImplementedError
else:
items.append(item)
# TODO: we should really use a union here, tuple types are special.
return Instance(info, [join_type_list(items)], extra_attrs=typ.partial_fallback.extra_attrs)
return Instance(
info, [make_simplified_union(items)], extra_attrs=typ.partial_fallback.extra_attrs
)


def get_self_type(func: CallableType, default_self: Instance | TupleType) -> Type | None:
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


ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved
[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


ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved
[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


ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved
[builtins fixtures/primitives.pyi]

[case testAssignEnumAsAttribute]
Expand Down
4 changes: 3 additions & 1 deletion test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1640,10 +1640,12 @@ from typing import Generator
def g() -> Generator[int, None, None]:
x = yield from () # E: Function does not return a value (it only ever returns None)
x = yield from (0, 1, 2) # E: Function does not return a value (it only ever returns None)
x = yield from (0, "ERROR") # E: Incompatible types in "yield from" (actual type "object", expected type "int") \
x = yield from (0, "ERROR") # E: Incompatible types in "yield from" (actual type "Union[int, str]", expected type "int") \
# E: Function does not return a value (it only ever returns None)
x = yield from ("ERROR",) # E: Incompatible types in "yield from" (actual type "str", expected type "int") \
# E: Function does not return a value (it only ever returns None)


ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved
[builtins fixtures/tuple.pyi]

-- dict(...)
Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -1249,7 +1249,7 @@ nti: NT[int]
reveal_type(nti * x) # N: Revealed type is "builtins.tuple[builtins.int, ...]"

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

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

reveal_type(foo(nti, x)) # N: Revealed type is "builtins.tuple[builtins.int, ...]"
reveal_type(foo(nts, x)) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
reveal_type(foo(nts, x)) # N: Revealed type is "builtins.tuple[Union[builtins.int, builtins.str], ...]"
reveal_type(foo(x, nti)) # N: Revealed type is "builtins.tuple[builtins.int, ...]"
reveal_type(foo(x, nts)) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
reveal_type(foo(x, nts)) # N: Revealed type is "builtins.tuple[Union[builtins.int, builtins.str], ...]"
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-namedtuple.pyi]

Expand Down
16 changes: 9 additions & 7 deletions test-data/unit/check-newsemanal.test
Original file line number Diff line number Diff line change
Expand Up @@ -1947,7 +1947,7 @@ class NTStr(NamedTuple):
y: str

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

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

[case testNewAnalyzerFallbackUpperBoundCheckAndFallbacks]
from typing import TypeVar, Generic, Tuple
Expand All @@ -1973,18 +1972,21 @@ S = TypeVar('S', bound='Tuple[G[A], ...]')

class GG(Generic[S]): pass

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], ...]" \
# E: Type argument "B" of "G" must be a subtype of "A" \
# E: Type argument "C" of "G" must be a subtype of "A"
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], ...]" \
# E: Type argument "B" of "G" must be a subtype of "A" \
# E: Type argument "C" of "G" must be a subtype of "A"

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

class G(Generic[T]): pass

t: Tuple[G[B], G[C]] # E: Type argument "B" of "G" must be a subtype of "A" \
# E: Type argument "C" of "G" must be a subtype of "A"
reveal_type(t.__iter__) # N: Revealed type is "def () -> typing.Iterator[builtins.object]"
reveal_type(t.__iter__) # N: Revealed type is "def () -> typing.Iterator[__main__.G[__main__.B]]"

ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved



ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved
[builtins fixtures/tuple.pyi]

[case testNewAnalyzerClassKeywordsForward]
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-statements.test
Original file line number Diff line number Diff line change
Expand Up @@ -1339,7 +1339,7 @@ from typing import Generator
def g() -> Generator[int, None, None]:
yield from ()
yield from (0, 1, 2)
yield from (0, "ERROR") # E: Incompatible types in "yield from" (actual type "object", expected type "int")
yield from (0, "ERROR") # E: Incompatible types in "yield from" (actual type "Union[int, str]", expected type "int")
yield from ("ERROR",) # E: Incompatible types in "yield from" (actual type "str", expected type "int")
[builtins fixtures/tuple.pyi]

Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-tuples.test
Original file line number Diff line number Diff line change
Expand Up @@ -1408,8 +1408,8 @@ y = ""
reveal_type(t[x]) # N: Revealed type is "Union[builtins.int, builtins.str]"
t[y] # E: No overload variant of "__getitem__" of "tuple" matches argument type "str" \
# N: Possible overload variants: \
# N: def __getitem__(self, int, /) -> object \
# N: def __getitem__(self, slice, /) -> Tuple[object, ...]
# N: def __getitem__(self, int, /) -> Union[int, str] \
# N: def __getitem__(self, slice, /) -> Tuple[Union[int, str], ...]

[builtins fixtures/tuple.pyi]

Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-typevar-tuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def g(a: Tuple[Unpack[Ts]], b: Tuple[Unpack[Ts]]) -> Tuple[Unpack[Ts]]:

reveal_type(g(args, args)) # N: Revealed type is "Tuple[builtins.int, builtins.str]"
reveal_type(g(args, args2)) # N: Revealed type is "Tuple[builtins.int, builtins.str]"
reveal_type(g(args, args3)) # N: Revealed type is "builtins.tuple[builtins.object, ...]"
reveal_type(g(args, args3)) # N: Revealed type is "builtins.tuple[Union[builtins.int, builtins.str], ...]"
reveal_type(g(any, any)) # N: Revealed type is "builtins.tuple[Any, ...]"
[builtins fixtures/tuple.pyi]

Expand Down Expand Up @@ -989,7 +989,7 @@ from typing_extensions import Unpack

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

[case testFixedUnpackItemInInstanceArguments]
Expand Down Expand Up @@ -1715,7 +1715,7 @@ vt: Tuple[int, Unpack[Tuple[float, ...]], int]

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

Expand Down
Loading
Loading