From 6115d9aaf7aefa669abff4befc4ec1d7473422cd Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 27 Dec 2024 13:18:33 -0800 Subject: [PATCH 1/5] Disallow assignment of protocol or abstract classes to Callable --- mypy/subtypes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 11f3421331a5..d099788e254a 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1533,6 +1533,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 From 151521907512d9113e526e6b227aa68129594993 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 27 Dec 2024 13:47:34 -0800 Subject: [PATCH 2/5] . --- mypy/typeshed/stdlib/typing.pyi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/typeshed/stdlib/typing.pyi b/mypy/typeshed/stdlib/typing.pyi index 27ae11daba70..eb4552962b41 100644 --- a/mypy/typeshed/stdlib/typing.pyi +++ b/mypy/typeshed/stdlib/typing.pyi @@ -331,6 +331,7 @@ else: _F = TypeVar("_F", bound=Callable[..., Any]) _P = _ParamSpec("_P") _T = TypeVar("_T") +_FT = TypeVar("_FT", bound=Callable[..., Any] | type) # These type variables are used by the container types. _S = TypeVar("_S") @@ -346,7 +347,7 @@ def no_type_check(arg: _F) -> _F: ... def no_type_check_decorator(decorator: Callable[_P, _T]) -> Callable[_P, _T]: ... # This itself is only available during type checking -def type_check_only(func_or_cls: _F) -> _F: ... +def type_check_only(func_or_cls: _FT) -> _FT: ... # Type aliases and type constructors From 99a206319f48c5e1279cfea23a927196ae3e7c78 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 27 Dec 2024 14:55:54 -0800 Subject: [PATCH 3/5] add a test --- test-data/unit/check-abstract.test | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test index 3b0b9c520b75..c8875d5a4a61 100644 --- a/test-data/unit/check-abstract.test +++ b/test-data/unit/check-abstract.test @@ -1687,3 +1687,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]" From c8b23ac0586da31fe00dc3f0c8d7ed5fcd26dec3 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 27 Dec 2024 18:51:48 -0800 Subject: [PATCH 4/5] fix --- mypy/erasetype.py | 3 ++- mypy/meet.py | 2 +- test-data/unit/check-functools.test | 3 ++- test-data/unit/check-optional.test | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 222e7f2a6d7a..37ff89627d2f 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -101,11 +101,12 @@ 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) + 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 f51d354d8f2f..b63af45dbe20 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -588,7 +588,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), diff --git a/test-data/unit/check-functools.test b/test-data/unit/check-functools.test index ea98a902d14b..4d9cb1161a81 100644 --- a/test-data/unit/check-functools.test +++ b/test-data/unit/check-functools.test @@ -592,7 +592,8 @@ 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] diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 683ce0446915..2da202f3603f 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -1343,3 +1343,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 "Union[__main__.CP, Type[__main__.NotSet], None]" +[builtins fixtures/tuple.pyi] From 388ba2ad35af98bad33226de16de3c03ea93eb96 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 27 Dec 2024 19:29:14 -0800 Subject: [PATCH 5/5] just ignore --- mypy/erasetype.py | 1 + mypy/test/testtypes.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 37ff89627d2f..97ad1a0acded 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -101,6 +101,7 @@ 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], diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 0218d33cc124..ab90cefb6698 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -333,7 +333,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, ), )