From 1f6711567d7cd7531275de19c5afd4c0589904d5 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 1 Jun 2025 01:46:09 +0100 Subject: [PATCH] Tighten metaclass __call__ handling in protocols --- mypy/constraints.py | 4 ++-- mypy/nodes.py | 4 ++-- mypy/typeops.py | 2 +- test-data/unit/check-protocols.test | 22 ++++++++++++++++++++++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 8e7a30e05ffb..73b7ec7954a4 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1066,8 +1066,8 @@ def infer_constraints_from_protocol_members( inst, erase_typevars(temp), ignore_pos_arg_names=True ): continue - # This exception matches the one in subtypes.py, see PR #14121 for context. - if member == "__call__" and instance.type.is_metaclass(): + # This exception matches the one in typeops.py, see PR #14121 for context. + if member == "__call__" and instance.type.is_metaclass(precise=True): continue res.extend(infer_constraints(temp, inst, self.direction)) if mypy.subtypes.IS_SETTABLE in mypy.subtypes.get_member_flags(member, protocol): diff --git a/mypy/nodes.py b/mypy/nodes.py index fae0bb1cc61f..7db32240c33e 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3359,11 +3359,11 @@ def calculate_metaclass_type(self) -> mypy.types.Instance | None: return c return None - def is_metaclass(self) -> bool: + def is_metaclass(self, *, precise: bool = False) -> bool: return ( self.has_base("builtins.type") or self.fullname == "abc.ABCMeta" - or self.fallback_to_any + or (self.fallback_to_any and not precise) ) def has_base(self, fullname: str) -> bool: diff --git a/mypy/typeops.py b/mypy/typeops.py index bcf946900563..3715081ae173 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1257,7 +1257,7 @@ def named_type(fullname: str) -> Instance: return type_object_type(left.type, named_type) - if member == "__call__" and left.type.is_metaclass(): + if member == "__call__" and left.type.is_metaclass(precise=True): # 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 diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 5e34d5223907..ea44054bd782 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -4505,3 +4505,25 @@ def bad() -> Proto: class Impl: @defer def f(self) -> int: ... + +[case testInferCallableProtoWithAnySubclass] +from typing import Any, Generic, Protocol, TypeVar + +T = TypeVar("T", covariant=True) + +Unknown: Any +class Mock(Unknown): + def __init__(self, **kwargs: Any) -> None: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + +class Factory(Protocol[T]): + def __call__(self, **kwargs: Any) -> T: ... + + +class Test(Generic[T]): + def __init__(self, f: Factory[T]) -> None: + ... + +t = Test(Mock()) +reveal_type(t) # N: Revealed type is "__main__.Test[Any]" +[builtins fixtures/dict.pyi]