From 0fc26dc1affab3c63b650eb344f2601a42370d97 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 21 Jul 2022 11:26:58 +0300 Subject: [PATCH 1/3] Warn when abstract or protocol type is assigned to a callable, refs #13171 --- mypy/checker.py | 12 ++++++++---- test-data/unit/check-abstract.test | 19 +++++++++++++++++++ test-data/unit/check-protocols.test | 17 +++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c131e80d47f0..6645a56a68fa 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2431,10 +2431,14 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type if (isinstance(rvalue_type, CallableType) and rvalue_type.is_type_obj() and (rvalue_type.type_object().is_abstract or rvalue_type.type_object().is_protocol) and - isinstance(lvalue_type, TypeType) and - isinstance(lvalue_type.item, Instance) and - (lvalue_type.item.type.is_abstract or - lvalue_type.item.type.is_protocol)): + ((isinstance(lvalue_type, TypeType) and + isinstance(lvalue_type.item, Instance) and + (lvalue_type.item.type.is_abstract or + lvalue_type.item.type.is_protocol)) or + (isinstance(lvalue_type, CallableType) and + isinstance(lvalue_type.ret_type, Instance) and + (lvalue_type.ret_type.type.is_abstract or + lvalue_type.ret_type.type.is_protocol)))): self.msg.concrete_only_assign(lvalue_type, rvalue) return if rvalue_type and infer_lvalue_type and not isinstance(lvalue_type, PartialType): diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test index beb2d9397e43..db79422885c0 100644 --- a/test-data/unit/check-abstract.test +++ b/test-data/unit/check-abstract.test @@ -250,6 +250,25 @@ if int(): var_old = C # OK [out] +[case testInstantiationAbstractsWithCallables] +from typing import Callable, Type +from abc import abstractmethod + +class A: + @abstractmethod + def m(self) -> None: pass +class B(A): pass +class C(B): + def m(self) -> None: + pass + +var: Callable[[], A] +var() # OK + +var = A # E: Can only assign concrete classes to a variable of type "Callable[[], A]" +var = B # E: Can only assign concrete classes to a variable of type "Callable[[], A]" +var = C # OK + [case testInstantiationAbstractsInTypeForClassMethods] from typing import Type from abc import abstractmethod diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 4c5a0b44d714..0de5f26125f8 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -1619,6 +1619,23 @@ if int(): var_old = B # OK var_old = C # OK +[case testInstantiationProtocolWithCallables] +from typing import Callable, Protocol + +class P(Protocol): + def m(self) -> None: pass +class B(P): pass +class C: + def m(self) -> None: + pass + +var: Callable[[], P] +var() # OK + +var = P # E: Can only assign concrete classes to a variable of type "Callable[[], P]" +var = B # OK +var = C # OK + [case testInstantiationProtocolInTypeForClassMethods] from typing import Type, Protocol From d21151319e99771c5917fcfad90feb1f2f9200a2 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 21 Jul 2022 11:47:43 +0300 Subject: [PATCH 2/3] Fix CI --- mypy/checker.py | 47 ++++++++++++++++++++--------- test-data/unit/check-abstract.test | 8 +++++ test-data/unit/check-protocols.test | 8 +++++ 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6645a56a68fa..74f084eac7e6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2426,20 +2426,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type # Special case: only non-abstract non-protocol classes can be assigned to # variables with explicit type Type[A], where A is protocol or abstract. - rvalue_type = get_proper_type(rvalue_type) - lvalue_type = get_proper_type(lvalue_type) - if (isinstance(rvalue_type, CallableType) and rvalue_type.is_type_obj() and - (rvalue_type.type_object().is_abstract or - rvalue_type.type_object().is_protocol) and - ((isinstance(lvalue_type, TypeType) and - isinstance(lvalue_type.item, Instance) and - (lvalue_type.item.type.is_abstract or - lvalue_type.item.type.is_protocol)) or - (isinstance(lvalue_type, CallableType) and - isinstance(lvalue_type.ret_type, Instance) and - (lvalue_type.ret_type.type.is_abstract or - lvalue_type.ret_type.type.is_protocol)))): - self.msg.concrete_only_assign(lvalue_type, rvalue) + if not self.check_concrete_only_assign(lvalue_type, rvalue_type, rvalue): return if rvalue_type and infer_lvalue_type and not isinstance(lvalue_type, PartialType): # Don't use type binder for definitions of special forms, like named tuples. @@ -2457,6 +2444,38 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue) self.check_assignment_to_slots(lvalue) + def check_concrete_only_assign(self, lvalue_type: Optional[Type], + rvalue_type: Type, rvalue: Expression) -> bool: + rvalue_type = get_proper_type(rvalue_type) + lvalue_type = get_proper_type(lvalue_type) + if not ( + isinstance(rvalue_type, CallableType) and + rvalue_type.is_type_obj() and + (rvalue_type.type_object().is_abstract or + rvalue_type.type_object().is_protocol)): + return True + + lvalue_is_a_type = ( + isinstance(lvalue_type, TypeType) and + isinstance(lvalue_type.item, Instance) and + (lvalue_type.item.type.is_abstract or + lvalue_type.item.type.is_protocol) + ) + + lvalue_is_a_callable = False + if isinstance(lvalue_type, CallableType): + ret_type = get_proper_type(lvalue_type.ret_type) + lvalue_is_a_callable = ( + isinstance(ret_type, Instance) and + (ret_type.type.is_abstract or ret_type.type.is_protocol) + ) + + if lvalue_is_a_type or lvalue_is_a_callable: + # `lvalue_type` here is either `TypeType` or `CallableType`: + self.msg.concrete_only_assign(cast(Type, lvalue_type), rvalue) + return False + return True + # (type, operator) tuples for augmented assignments supported with partial types partial_type_augmented_ops: Final = { ('builtins.list', '+'), diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test index db79422885c0..d5e95a8d85b1 100644 --- a/test-data/unit/check-abstract.test +++ b/test-data/unit/check-abstract.test @@ -269,6 +269,14 @@ var = A # E: Can only assign concrete classes to a variable of type "Callable[[] var = B # E: Can only assign concrete classes to a variable of type "Callable[[], A]" var = C # OK +# Type aliases: +A1 = A +B1 = B +C1 = C +var = A1 # E: Can only assign concrete classes to a variable of type "Callable[[], A]" +var = B1 # E: Can only assign concrete classes to a variable of type "Callable[[], A]" +var = C1 # OK + [case testInstantiationAbstractsInTypeForClassMethods] from typing import Type from abc import abstractmethod diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 0de5f26125f8..ee63f206f61c 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -1636,6 +1636,14 @@ var = P # E: Can only assign concrete classes to a variable of type "Callable[[] var = B # OK var = C # OK +# Type aliases: +P1 = P +B1 = B +C1 = C +var = P1 # E: Can only assign concrete classes to a variable of type "Callable[[], P]" +var = B1 # OK +var = C1 # OK + [case testInstantiationProtocolInTypeForClassMethods] from typing import Type, Protocol From 6a06e369aefe281ae25385e87d3cc8e7178c5555 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 21 Jul 2022 15:27:20 +0300 Subject: [PATCH 3/3] Allow self assignment --- mypy/checker.py | 14 +++++++++++--- test-data/unit/check-abstract.test | 5 +++++ test-data/unit/check-protocols.test | 5 +++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 74f084eac7e6..bf08767d25ab 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2426,7 +2426,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type # Special case: only non-abstract non-protocol classes can be assigned to # variables with explicit type Type[A], where A is protocol or abstract. - if not self.check_concrete_only_assign(lvalue_type, rvalue_type, rvalue): + if not self.check_concrete_only_assign(lvalue_type, lvalue, rvalue_type, rvalue): return if rvalue_type and infer_lvalue_type and not isinstance(lvalue_type, PartialType): # Don't use type binder for definitions of special forms, like named tuples. @@ -2444,8 +2444,16 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue) self.check_assignment_to_slots(lvalue) - def check_concrete_only_assign(self, lvalue_type: Optional[Type], - rvalue_type: Type, rvalue: Expression) -> bool: + def check_concrete_only_assign(self, + lvalue_type: Optional[Type], + lvalue: Expression, + rvalue_type: Type, + rvalue: Expression) -> bool: + if (isinstance(lvalue, NameExpr) and isinstance(rvalue, NameExpr) + and lvalue.node == rvalue.node): + # This means that we reassign abstract class to itself. Like `A = A` + return True + rvalue_type = get_proper_type(rvalue_type) lvalue_type = get_proper_type(lvalue_type) if not ( diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test index d5e95a8d85b1..21cc3466ef35 100644 --- a/test-data/unit/check-abstract.test +++ b/test-data/unit/check-abstract.test @@ -277,6 +277,11 @@ var = A1 # E: Can only assign concrete classes to a variable of type "Callable[[ var = B1 # E: Can only assign concrete classes to a variable of type "Callable[[], A]" var = C1 # OK +# Self assign: +A = A # OK +B = B # OK +C = C # OK + [case testInstantiationAbstractsInTypeForClassMethods] from typing import Type from abc import abstractmethod diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index ee63f206f61c..9c3021a058ab 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -1644,6 +1644,11 @@ var = P1 # E: Can only assign concrete classes to a variable of type "Callable[[ var = B1 # OK var = C1 # OK +# Self assign: +P = P # OK +B = B # OK +C = C # OK + [case testInstantiationProtocolInTypeForClassMethods] from typing import Type, Protocol