diff --git a/mypy/erasetype.py b/mypy/erasetype.py index cb8d66f292dd3..def3e5f5d7879 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -102,11 +102,13 @@ def visit_unpack_type(self, t: UnpackType) -> ProperType: def visit_callable_type(self, t: CallableType) -> ProperType: # We must preserve the fallback type for overload resolution to work. any_type = AnyType(TypeOfAny.special_form) + # If we're a type object, make sure we continue to be a valid type object + ret_type = t.ret_type if t.is_type_obj() else any_type return CallableType( arg_types=[any_type, any_type], arg_kinds=[ARG_STAR, ARG_STAR2], arg_names=[None, None], - ret_type=any_type, + ret_type=ret_type, fallback=t.fallback, is_ellipsis_args=True, implicit=True, diff --git a/mypy/meet.py b/mypy/meet.py index ee32f239df8c3..470c03490d480 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -657,7 +657,7 @@ def _type_object_overlap(left: Type, right: Type) -> bool: def is_overlapping_erased_types( left: Type, right: Type, *, ignore_promotions: bool = False ) -> bool: - """The same as 'is_overlapping_erased_types', except the types are erased first.""" + """The same as 'is_overlapping_types', except the types are erased first.""" return is_overlapping_types( erase_type(left), erase_type(right), ignore_promotions=ignore_promotions ) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 66d7a95eb4252..c5ef0b6636ca7 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1696,6 +1696,11 @@ def g(x: int) -> int: ... if right.is_type_obj() and not left.is_type_obj() and not allow_partial_overlap: return False + if left.is_type_obj(): + left_type_obj = left.type_object() + if (left_type_obj.is_protocol or left_type_obj.is_abstract) and not right.is_type_obj(): + return False + # A callable L is a subtype of a generic callable R if L is a # subtype of every type obtained from R by substituting types for # the variables of R. We can check this by simply leaving the diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 6562f541d73bc..0e3635fad5a7b 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -318,7 +318,7 @@ def test_erase_with_type_object(self) -> None: arg_types=[self.fx.anyt, self.fx.anyt], arg_kinds=[ARG_STAR, ARG_STAR2], arg_names=[None, None], - ret_type=self.fx.anyt, + ret_type=self.fx.b, fallback=self.fx.type_type, ), ) diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test index 7507a31d115a9..b9feb9ff68ac9 100644 --- a/test-data/unit/check-abstract.test +++ b/test-data/unit/check-abstract.test @@ -1688,3 +1688,24 @@ from typing import TYPE_CHECKING class C: if TYPE_CHECKING: def dynamic(self) -> int: ... # OK + +[case testAbstractCallableSubtyping] +import abc +from typing import Callable, Protocol + +class Proto(Protocol): + def meth(self): ... + +def foo(t: Callable[..., Proto]): + t() + +foo(Proto) # E: Argument 1 to "foo" has incompatible type "type[Proto]"; expected "Callable[..., Proto]" + +class Abstract(abc.ABC): + @abc.abstractmethod + def meth(self): ... + +def bar(t: Callable[..., Abstract]): + t() + +bar(Abstract) # E: Argument 1 to "bar" has incompatible type "type[Abstract]"; expected "Callable[..., Abstract]" diff --git a/test-data/unit/check-functools.test b/test-data/unit/check-functools.test index 77070d61a013c..fca1e65e984a7 100644 --- a/test-data/unit/check-functools.test +++ b/test-data/unit/check-functools.test @@ -594,8 +594,10 @@ def f1(cls: type[A]) -> None: def f2() -> None: A() # E: Cannot instantiate abstract class "A" with abstract attribute "method" - partial_cls = partial(A) # E: Cannot instantiate abstract class "A" with abstract attribute "method" + partial_cls = partial(A) # E: Cannot instantiate abstract class "A" with abstract attribute "method" \ + # E: Argument 1 to "partial" has incompatible type "type[A]"; expected "Callable[..., A]" partial_cls() # E: Cannot instantiate abstract class "A" with abstract attribute "method" + [builtins fixtures/tuple.pyi] [case testFunctoolsPartialSelfType] diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 2ccb571792ac0..eeb324fbf3ffb 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -1355,3 +1355,19 @@ def f(x: object) -> None: with C(): pass [builtins fixtures/tuple.pyi] + +[case testRefineAwayNoneCallbackProtocol] +# Regression test for issue encountered in https://github.com/python/mypy/pull/18347#issuecomment-2564062070 +from __future__ import annotations +from typing import Protocol + +class CP(Protocol): + def __call__(self, parameters: str) -> str: ... + +class NotSet: ... + +class Task: + def with_opt(self, trn: CP | type[NotSet] | None): + if trn is not NotSet: + reveal_type(trn) # N: Revealed type is "__main__.CP | type[__main__.NotSet] | None" +[builtins fixtures/tuple.pyi]