Skip to content

Commit

Permalink
Fix error for callback protocol matching against callable type object (
Browse files Browse the repository at this point in the history
  • Loading branch information
hauntsaninja authored Apr 14, 2023
1 parent 11166b7 commit 1a47b19
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 19 deletions.
3 changes: 2 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 1 addition & 16 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
38 changes: 36 additions & 2 deletions test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down

0 comments on commit 1a47b19

Please sign in to comment.