diff --git a/mypy/messages.py b/mypy/messages.py index 8c3d5327be320..9a91761558af2 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -19,6 +19,7 @@ from typing import Any, Callable, Collection, Iterable, Iterator, List, Sequence, cast from typing_extensions import Final +import mypy.typeops from mypy import errorcodes as codes, message_registry from mypy.erasetype import erase_type from mypy.errorcodes import ErrorCode @@ -2711,7 +2712,7 @@ def get_conflict_protocol_types( continue supertype = find_member(member, right, left) assert supertype is not None - subtype = find_member(member, left, left, class_obj=class_obj) + subtype = mypy.typeops.get_protocol_member(left, member, class_obj) if not subtype: continue is_compat = is_subtype(subtype, supertype, ignore_pos_arg_names=True) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 882858adc961f..7ffa2ac68eef1 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1046,23 +1046,8 @@ def f(self) -> A: ... # We always bind self to the subtype. (Similarly to nominal types). supertype = get_proper_type(find_member(member, right, left)) assert supertype is not None - if member == "__call__" and class_obj: - # Special case: class objects always have __call__ that is just the constructor. - # TODO: move this helper function to typeops.py? - import mypy.checkmember - def named_type(fullname: str) -> Instance: - return Instance(left.type.mro[-1], []) - - subtype: ProperType | None = mypy.checkmember.type_object_type( - left.type, named_type - ) - elif member == "__call__" and left.type.is_metaclass(): - # Special case: we want to avoid falling back to metaclass __call__ - # if constructor signature didn't match, this can cause many false negatives. - subtype = None - else: - subtype = get_proper_type(find_member(member, left, left, class_obj=class_obj)) + subtype = mypy.typeops.get_protocol_member(left, member, class_obj) # Useful for debugging: # print(member, 'of', left, 'has type', subtype) # print(member, 'of', right, 'has type', supertype) diff --git a/mypy/typeops.py b/mypy/typeops.py index 8c01fb1180765..8ed59b6fbe55d 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1050,3 +1050,23 @@ def fixup_partial_type(typ: Type) -> Type: return UnionType.make_union([AnyType(TypeOfAny.unannotated), NoneType()]) else: return Instance(typ.type, [AnyType(TypeOfAny.unannotated)] * len(typ.type.type_vars)) + + +def get_protocol_member(left: Instance, member: str, class_obj: bool) -> ProperType | None: + if member == "__call__" and class_obj: + # Special case: class objects always have __call__ that is just the constructor. + from mypy.checkmember import type_object_type + + def named_type(fullname: str) -> Instance: + return Instance(left.type.mro[-1], []) + + return type_object_type(left.type, named_type) + + if member == "__call__" and left.type.is_metaclass(): + # Special case: we want to avoid falling back to metaclass __call__ + # if constructor signature didn't match, this can cause many false negatives. + return None + + from mypy.subtypes import find_member + + return get_proper_type(find_member(member, left, left, class_obj=class_obj)) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 182745b99e406..4c2560641d97c 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -3522,7 +3522,12 @@ class C: def test(arg: P) -> None: ... test(B) # OK test(C) # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \ - # N: "C" has constructor incompatible with "__call__" of "P" + # N: "C" has constructor incompatible with "__call__" of "P" \ + # N: Following member(s) of "C" have conflicts: \ + # N: Expected: \ + # N: def __call__(x: int, y: int) -> Any \ + # N: Got: \ + # N: def __init__(x: int, y: str) -> C [case testProtocolClassObjectPureCallback] from typing import Any, ClassVar, Protocol @@ -3538,7 +3543,36 @@ class C: def test(arg: P) -> None: ... test(B) # OK test(C) # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \ - # N: "C" has constructor incompatible with "__call__" of "P" + # N: "C" has constructor incompatible with "__call__" of "P" \ + # N: Following member(s) of "C" have conflicts: \ + # N: Expected: \ + # N: def __call__(x: int, y: int) -> Any \ + # N: Got: \ + # N: def __init__(x: int, y: str) -> C +[builtins fixtures/type.pyi] + +[case testProtocolClassObjectCallableError] +from typing import Protocol, Any, Callable + +class P(Protocol): + def __call__(self, app: int) -> Callable[[str], None]: + ... + +class C: + def __init__(self, app: str) -> None: + pass + + def __call__(self, el: str) -> None: + return None + +p: P = C # E: Incompatible types in assignment (expression has type "Type[C]", variable has type "P") \ + # N: Following member(s) of "C" have conflicts: \ + # N: Expected: \ + # N: def __call__(app: int) -> Callable[[str], None] \ + # N: Got: \ + # N: def __init__(app: str) -> C \ + # N: "P.__call__" has type "Callable[[Arg(int, 'app')], Callable[[str], None]]" + [builtins fixtures/type.pyi] [case testProtocolTypeTypeAttribute]