From e093f646dc304b7cf3deedef35d769a7dc0dba2b Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 20 Jan 2025 23:48:48 +0100 Subject: [PATCH 1/4] Format callables with argnames if their return types are compatible --- mypy/messages.py | 21 +++++++++++++++++++- test-data/unit/check-functions.test | 30 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/mypy/messages.py b/mypy/messages.py index b63310825f7d7..4b662353bea65 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2855,7 +2855,26 @@ 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): + + 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 is_subtype(left.ret_type, right.ret_type) + and any(right.arg_names) + ): + 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 diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 18425efb9cb08..09bcf26776109 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3472,3 +3472,33 @@ 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: + ... +[builtins fixtures/tuple.pyi] From c64d00b45299a4a8c223eac12cacfe013594c2d1 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 20 Jan 2025 23:58:34 +0100 Subject: [PATCH 2/4] Add test with inheritance --- test-data/unit/check-functions.test | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 09bcf26776109..5076af5e2aa91 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3501,4 +3501,12 @@ 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] From 74f5a5792ad1816421ab0e3a10e286ba2bbe74a7 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Tue, 21 Jan 2025 00:11:50 +0100 Subject: [PATCH 3/4] Restrict to cases with args formatted to the same string --- mypy/messages.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mypy/messages.py b/mypy/messages.py index 4b662353bea65..e332ee4f1db6e 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2856,6 +2856,9 @@ def format_type_distinctly(*types: Type, options: Options, bare: bool = False) - """ overlapping = find_type_overlaps(*types) + 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]" @@ -2871,6 +2874,11 @@ def format_type_distinctly(*types: Type, options: Options, bare: bool = False) - and isinstance(right, CallableType) and is_subtype(left.ret_type, right.ret_type) and any(right.arg_names) + and len(right.arg_types) == len(left.arg_types) + and all( + format_single(aleft) == format_single(aright) + for aleft, aright in zip(left.arg_types, right.arg_types) + ) ): min_verbosity = 1 From 6c986cc7e5ee57659c2a4fa89c3c2f981b3168fe Mon Sep 17 00:00:00 2001 From: STerliakov Date: Tue, 21 Jan 2025 00:54:31 +0100 Subject: [PATCH 4/4] Use a simpler heuristic: just check if callables are subtypes if we ignore argnames --- mypy/messages.py | 7 +------ test-data/unit/check-functions.test | 10 ++++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index e332ee4f1db6e..8e614f02277a9 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2872,13 +2872,8 @@ def format_single(arg: Type) -> str: if ( isinstance(left, CallableType) and isinstance(right, CallableType) - and is_subtype(left.ret_type, right.ret_type) and any(right.arg_names) - and len(right.arg_types) == len(left.arg_types) - and all( - format_single(aleft) == format_single(aright) - for aleft, aright in zip(left.arg_types, right.arg_types) - ) + and is_subtype(left, right, ignore_pos_arg_names=True) ): min_verbosity = 1 diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 5076af5e2aa91..58973307a1ae7 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3510,3 +3510,13 @@ 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]