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

Fix error for callback protocol matching against callable type object #15042

Merged
merged 2 commits into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from all 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: 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