Skip to content

Commit

Permalink
Hint at argument names when formatting callables with compatible retu…
Browse files Browse the repository at this point in the history
…rn types in error messages (#18495)

Fixes #18493.

Improves message in #12013 and #4530, but probably still doesn't make it
clear enough.

Use higher verbosity for type formatting in error message if callables'
return types are compatible and supertype has some named arguments, as
that is a popular source of confusion.
  • Loading branch information
sterliakov authored Jan 22, 2025
1 parent 878d892 commit 48f9fc5
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 1 deletion.
24 changes: 23 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2855,7 +2855,29 @@ def format_type_distinctly(*types: Type, options: Options, bare: bool = False) -
quoting them (such as prepending * or **) should use this.
"""
overlapping = find_type_overlaps(*types)
for verbosity in range(2):

def format_single(arg: Type) -> str:
return format_type_inner(arg, verbosity=0, options=options, fullnames=overlapping)

min_verbosity = 0
# Prevent emitting weird errors like:
# ... has incompatible type "Callable[[int], Child]"; expected "Callable[[int], Parent]"
if len(types) == 2:
left, right = types
left = get_proper_type(left)
right = get_proper_type(right)
# If the right type has named arguments, they may be the reason for incompatibility.
# This excludes cases when right is Callable[[Something], None] without named args,
# because that's usually the right thing to do.
if (
isinstance(left, CallableType)
and isinstance(right, CallableType)
and any(right.arg_names)
and is_subtype(left, right, ignore_pos_arg_names=True)
):
min_verbosity = 1

for verbosity in range(min_verbosity, 2):
strs = [
format_type_inner(type, verbosity=verbosity, options=options, fullnames=overlapping)
for type in types
Expand Down
48 changes: 48 additions & 0 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -3472,3 +3472,51 @@ class Qux(Bar):
def baz(self, x) -> None:
pass
[builtins fixtures/tuple.pyi]

[case testDistinctFormatting]
from typing import Awaitable, Callable, ParamSpec

P = ParamSpec("P")

class A: pass
class B(A): pass

def decorator(f: Callable[P, None]) -> Callable[[Callable[P, A]], None]:
return lambda _: None

def key(x: int) -> None: ...
def fn_b(b: int) -> B: ...

decorator(key)(fn_b) # E: Argument 1 has incompatible type "Callable[[Arg(int, 'b')], B]"; expected "Callable[[Arg(int, 'x')], A]"

def decorator2(f: Callable[P, None]) -> Callable[
[Callable[P, Awaitable[None]]],
Callable[P, Awaitable[None]],
]:
return lambda f: f

def key2(x: int) -> None:
...

@decorator2(key2) # E: Argument 1 has incompatible type "Callable[[Arg(int, 'y')], Coroutine[Any, Any, None]]"; expected "Callable[[Arg(int, 'x')], Awaitable[None]]"
async def foo2(y: int) -> None:
...

class Parent:
def method_without(self) -> "Parent": ...
def method_with(self, param: str) -> "Parent": ...

class Child(Parent):
method_without: Callable[["Child"], "Child"]
method_with: Callable[["Child", str], "Child"] # E: Incompatible types in assignment (expression has type "Callable[[str], Child]", base class "Parent" defined the type as "Callable[[Arg(str, 'param')], Parent]")
[builtins fixtures/tuple.pyi]

[case testDistinctFormattingUnion]
from typing import Callable, Union
from mypy_extensions import Arg

def f(x: Callable[[Arg(int, 'x')], None]) -> None: pass

y: Callable[[Union[int, str]], None]
f(y) # E: Argument 1 to "f" has incompatible type "Callable[[Union[int, str]], None]"; expected "Callable[[Arg(int, 'x')], None]"
[builtins fixtures/tuple.pyi]

0 comments on commit 48f9fc5

Please sign in to comment.