Skip to content

Commit 985f464

Browse files
committed
Treat methods with empty bodies in Protocols as abstract
Closes #8005 Closes #8926 Methods in Protocols are considered abstract if they have an empty function body, have a return type that is not compatible with `None`, and are not in a stub file.
1 parent a3fc35a commit 985f464

File tree

4 files changed

+241
-54
lines changed

4 files changed

+241
-54
lines changed

mypy/checker.py

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
ClassDef, Block, AssignmentStmt, NameExpr, MemberExpr, IndexExpr,
2020
TupleExpr, ListExpr, ExpressionStmt, ReturnStmt, IfStmt,
2121
WhileStmt, OperatorAssignmentStmt, WithStmt, AssertStmt,
22-
RaiseStmt, TryStmt, ForStmt, DelStmt, CallExpr, IntExpr, StrExpr,
23-
UnicodeExpr, OpExpr, UnaryExpr, LambdaExpr, TempNode, SymbolTableNode,
22+
RaiseStmt, TryStmt, ForStmt, DelStmt, CallExpr, IntExpr,
23+
OpExpr, UnaryExpr, LambdaExpr, TempNode, SymbolTableNode,
2424
Context, Decorator, PrintStmt, BreakStmt, PassStmt, ContinueStmt,
2525
ComparisonExpr, StarExpr, EllipsisExpr, RefExpr, PromoteExpr,
2626
Import, ImportFrom, ImportAll, ImportBase, TypeAlias,
@@ -70,7 +70,7 @@
7070
from mypy.constraints import SUPERTYPE_OF
7171
from mypy.maptype import map_instance_to_supertype
7272
from mypy.typevars import fill_typevars, has_no_typevars, fill_typevars_with_any
73-
from mypy.semanal import set_callable_name, refers_to_fullname
73+
from mypy.semanal import set_callable_name, refers_to_fullname, is_trivial_body
7474
from mypy.mro import calculate_mro, MroError
7575
from mypy.erasetype import erase_typevars, remove_instance_last_known_values, erase_type
7676
from mypy.expandtype import expand_type, expand_type_by_instance
@@ -1008,7 +1008,7 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
10081008
item.arguments[i].variable.type = arg_type
10091009

10101010
# Type check initialization expressions.
1011-
body_is_trivial = self.is_trivial_body(defn.body)
1011+
body_is_trivial = is_trivial_body(defn.body)
10121012
self.check_default_args(item, body_is_trivial)
10131013

10141014
# Type check body in a new scope.
@@ -1152,51 +1152,6 @@ def check___new___signature(self, fdef: FuncDef, typ: CallableType) -> None:
11521152
'but must return a subtype of'
11531153
)
11541154

1155-
def is_trivial_body(self, block: Block) -> bool:
1156-
"""Returns 'true' if the given body is "trivial" -- if it contains just a "pass",
1157-
"..." (ellipsis), or "raise NotImplementedError()". A trivial body may also
1158-
start with a statement containing just a string (e.g. a docstring).
1159-
1160-
Note: functions that raise other kinds of exceptions do not count as
1161-
"trivial". We use this function to help us determine when it's ok to
1162-
relax certain checks on body, but functions that raise arbitrary exceptions
1163-
are more likely to do non-trivial work. For example:
1164-
1165-
def halt(self, reason: str = ...) -> NoReturn:
1166-
raise MyCustomError("Fatal error: " + reason, self.line, self.context)
1167-
1168-
A function that raises just NotImplementedError is much less likely to be
1169-
this complex.
1170-
"""
1171-
body = block.body
1172-
1173-
# Skip a docstring
1174-
if (body and isinstance(body[0], ExpressionStmt) and
1175-
isinstance(body[0].expr, (StrExpr, UnicodeExpr))):
1176-
body = block.body[1:]
1177-
1178-
if len(body) == 0:
1179-
# There's only a docstring (or no body at all).
1180-
return True
1181-
elif len(body) > 1:
1182-
return False
1183-
1184-
stmt = body[0]
1185-
1186-
if isinstance(stmt, RaiseStmt):
1187-
expr = stmt.expr
1188-
if expr is None:
1189-
return False
1190-
if isinstance(expr, CallExpr):
1191-
expr = expr.callee
1192-
1193-
return (isinstance(expr, NameExpr)
1194-
and expr.fullname == 'builtins.NotImplementedError')
1195-
1196-
return (isinstance(stmt, PassStmt) or
1197-
(isinstance(stmt, ExpressionStmt) and
1198-
isinstance(stmt.expr, EllipsisExpr)))
1199-
12001155
def check_reverse_op_method(self, defn: FuncItem,
12011156
reverse_type: CallableType, reverse_name: str,
12021157
context: Context) -> None:

mypy/semanal.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
ClassDef, Var, GDEF, FuncItem, Import, Expression, Lvalue,
6161
ImportFrom, ImportAll, Block, LDEF, NameExpr, MemberExpr,
6262
IndexExpr, TupleExpr, ListExpr, ExpressionStmt, ReturnStmt,
63-
RaiseStmt, AssertStmt, OperatorAssignmentStmt, WhileStmt,
63+
RaiseStmt, AssertStmt, OperatorAssignmentStmt, WhileStmt, PassStmt,
6464
ForStmt, BreakStmt, ContinueStmt, IfStmt, TryStmt, WithStmt, DelStmt,
6565
GlobalDecl, SuperExpr, DictExpr, CallExpr, RefExpr, OpExpr, UnaryExpr,
6666
SliceExpr, CastExpr, RevealExpr, TypeApplication, Context, SymbolTable,
@@ -669,6 +669,16 @@ def analyze_func_def(self, defn: FuncDef) -> None:
669669

670670
self.analyze_arg_initializers(defn)
671671
self.analyze_function_body(defn)
672+
673+
# Mark protocol methods with empty bodies and None-incompatible return types as abstract.
674+
if self.is_class_scope() and defn.type is not None:
675+
assert self.type is not None and isinstance(defn.type, CallableType)
676+
if (not self.is_stub_file and self.type.is_protocol and
677+
(not isinstance(self.scope.function, OverloadedFuncDef)
678+
or defn.is_property) and
679+
not can_be_none(defn.type.ret_type) and is_trivial_body(defn.body)):
680+
defn.is_abstract = True
681+
672682
if (defn.is_coroutine and
673683
isinstance(defn.type, CallableType) and
674684
self.wrapped_coro_return_types.get(defn) != defn.type):
@@ -803,6 +813,21 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
803813
# We know this is an overload def. Infer properties and perform some checks.
804814
self.process_final_in_overload(defn)
805815
self.process_static_or_class_method_in_overload(defn)
816+
if defn.impl:
817+
self.process_overload_impl(defn)
818+
819+
def process_overload_impl(self, defn: OverloadedFuncDef) -> None:
820+
"""Set flags for an overload implementation.
821+
822+
Currently, this checks for a trivial body in protocols classes,
823+
where it makes the method implicitly abstract.
824+
"""
825+
assert defn.impl is not None
826+
impl = defn.impl if isinstance(defn.impl, FuncDef) else defn.impl.func
827+
if is_trivial_body(impl.body) and self.is_class_scope() and not self.is_stub_file:
828+
assert self.type is not None
829+
if self.type.is_protocol:
830+
impl.is_abstract = True
806831

807832
def analyze_overload_sigs_and_impl(
808833
self,
@@ -876,7 +901,8 @@ def handle_missing_overload_implementation(self, defn: OverloadedFuncDef) -> Non
876901
"""Generate error about missing overload implementation (only if needed)."""
877902
if not self.is_stub_file:
878903
if self.type and self.type.is_protocol and not self.is_func_scope():
879-
# An overloaded protocol method doesn't need an implementation.
904+
# An overloaded protocol method doesn't need an implementation,
905+
# but if it doesn't have one, then it is considered implicitly abstract.
880906
for item in defn.items:
881907
if isinstance(item, Decorator):
882908
item.func.is_abstract = True
@@ -5533,3 +5559,59 @@ def is_same_symbol(a: Optional[SymbolNode], b: Optional[SymbolNode]) -> bool:
55335559
or (isinstance(a, PlaceholderNode)
55345560
and isinstance(b, PlaceholderNode))
55355561
or is_same_var_from_getattr(a, b))
5562+
5563+
5564+
def is_trivial_body(block: Block) -> bool:
5565+
"""Returns 'true' if the given body is "trivial" -- if it contains just a "pass",
5566+
"..." (ellipsis), or "raise NotImplementedError()". A trivial body may also
5567+
start with a statement containing just a string (e.g. a docstring).
5568+
5569+
Note: functions that raise other kinds of exceptions do not count as
5570+
"trivial". We use this function to help us determine when it's ok to
5571+
relax certain checks on body, but functions that raise arbitrary exceptions
5572+
are more likely to do non-trivial work. For example:
5573+
5574+
def halt(self, reason: str = ...) -> NoReturn:
5575+
raise MyCustomError("Fatal error: " + reason, self.line, self.context)
5576+
5577+
A function that raises just NotImplementedError is much less likely to be
5578+
this complex.
5579+
"""
5580+
body = block.body
5581+
5582+
# Skip a docstring
5583+
if (body and isinstance(body[0], ExpressionStmt) and
5584+
isinstance(body[0].expr, (StrExpr, UnicodeExpr))):
5585+
body = block.body[1:]
5586+
5587+
if len(body) == 0:
5588+
# There's only a docstring (or no body at all).
5589+
return True
5590+
elif len(body) > 1:
5591+
return False
5592+
5593+
stmt = body[0]
5594+
5595+
if isinstance(stmt, RaiseStmt):
5596+
expr = stmt.expr
5597+
if expr is None:
5598+
return False
5599+
if isinstance(expr, CallExpr):
5600+
expr = expr.callee
5601+
5602+
return (isinstance(expr, NameExpr)
5603+
and expr.fullname == 'builtins.NotImplementedError')
5604+
5605+
return (isinstance(stmt, PassStmt) or
5606+
(isinstance(stmt, ExpressionStmt) and
5607+
isinstance(stmt.expr, EllipsisExpr)))
5608+
5609+
5610+
def can_be_none(t: Type) -> bool:
5611+
"""Can a variable of the given type be None?"""
5612+
t = get_proper_type(t)
5613+
return (
5614+
isinstance(t, NoneType) or
5615+
isinstance(t, AnyType) or
5616+
(isinstance(t, UnionType) and any(can_be_none(ut) for ut in t.items))
5617+
)

mypy/semanal_classprop.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing_extensions import Final
88

99
from mypy.nodes import (
10-
Node, TypeInfo, Var, Decorator, OverloadedFuncDef, SymbolTable, CallExpr, PromoteExpr,
10+
Node, TypeInfo, Var, Decorator, OverloadedFuncDef, SymbolTable, CallExpr, PromoteExpr, FuncDef
1111
)
1212
from mypy.types import Instance, Type
1313
from mypy.errors import Errors
@@ -79,8 +79,9 @@ def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: E
7979
else:
8080
func = node
8181
if isinstance(func, Decorator):
82-
fdef = func.func
83-
if fdef.is_abstract and name not in concrete:
82+
func = func.func
83+
if isinstance(func, FuncDef):
84+
if func.is_abstract and name not in concrete:
8485
typ.is_abstract = True
8586
abstract.append(name)
8687
if base is typ:

test-data/unit/check-protocols.test

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2910,3 +2910,152 @@ class C:
29102910
def round(number: SupportsRound[_T], ndigits: int) -> _T: ...
29112911

29122912
round(C(), 1)
2913+
2914+
[case testEmptyBodyImplicitlyAbstractProtocol]
2915+
from typing import Protocol, overload, Union
2916+
2917+
class P1(Protocol):
2918+
def meth(self) -> int: ...
2919+
class B1(P1): ...
2920+
class C1(P1):
2921+
def meth(self) -> int:
2922+
return 0
2923+
B1() # E: Cannot instantiate abstract class "B1" with abstract attribute "meth"
2924+
C1()
2925+
2926+
class P2(Protocol):
2927+
@classmethod
2928+
def meth(cls) -> int: ...
2929+
class B2(P2): ...
2930+
class C2(P2):
2931+
@classmethod
2932+
def meth(cls) -> int:
2933+
return 0
2934+
B2() # E: Cannot instantiate abstract class "B2" with abstract attribute "meth"
2935+
C2()
2936+
2937+
class P3(Protocol):
2938+
@overload
2939+
def meth(self, x: int) -> int: ...
2940+
@overload
2941+
def meth(self, x: str) -> str: ...
2942+
class B3(P3): ...
2943+
class C3(P3):
2944+
@overload
2945+
def meth(self, x: int) -> int: ...
2946+
@overload
2947+
def meth(self, x: str) -> str: ...
2948+
def meth(self, x: Union[int, str]) -> Union[int, str]:
2949+
return 0
2950+
B3() # E: Cannot instantiate abstract class "B3" with abstract attribute "meth"
2951+
C3()
2952+
[builtins fixtures/classmethod.pyi]
2953+
2954+
[case testEmptyBodyImplicitlyAbstractProtocolProperty]
2955+
from typing import Protocol
2956+
2957+
class P1(Protocol):
2958+
@property
2959+
def attr(self) -> int: ...
2960+
class B1(P1): ...
2961+
class C1(P1):
2962+
@property
2963+
def attr(self) -> int:
2964+
return 0
2965+
B1() # E: Cannot instantiate abstract class "B1" with abstract attribute "attr"
2966+
C1()
2967+
2968+
class P2(Protocol):
2969+
@property
2970+
def attr(self) -> int: ...
2971+
@attr.setter
2972+
def attr(self, value: int) -> None: ...
2973+
class B2(P2): ...
2974+
class C2(P2):
2975+
@property
2976+
def attr(self) -> int: return 0
2977+
@attr.setter
2978+
def attr(self, value: int) -> None: pass
2979+
B2() # E: Cannot instantiate abstract class "B2" with abstract attribute "attr"
2980+
C2()
2981+
[builtins fixtures/property.pyi]
2982+
2983+
[case testEmptyBodyImplicitlyAbstractProtocolStub]
2984+
from stub import P1, P2, P3, P4
2985+
2986+
class B1(P1): ...
2987+
class B2(P2): ...
2988+
class B3(P3): ...
2989+
class B4(P4): ...
2990+
2991+
B1()
2992+
B2()
2993+
B3()
2994+
B4() # E: Cannot instantiate abstract class "B4" with abstract attribute "meth"
2995+
2996+
[file stub.pyi]
2997+
from typing import Protocol, overload, Union
2998+
from abc import abstractmethod
2999+
3000+
class P1(Protocol):
3001+
def meth(self) -> int: ...
3002+
3003+
class P2(Protocol):
3004+
@classmethod
3005+
def meth(cls) -> int: ...
3006+
3007+
class P3(Protocol):
3008+
@overload
3009+
def meth(self, x: int) -> int: ...
3010+
@overload
3011+
def meth(self, x: str) -> str: ...
3012+
3013+
class P4(Protocol):
3014+
@abstractmethod
3015+
def meth(self) -> int: ...
3016+
[builtins fixtures/classmethod.pyi]
3017+
3018+
[case testEmptyBodyVariationsImplicitlyAbstractProtocol]
3019+
from typing import Protocol
3020+
3021+
class WithPass(Protocol):
3022+
def meth(self) -> int:
3023+
pass
3024+
class A(WithPass): ...
3025+
A() # E: Cannot instantiate abstract class "A" with abstract attribute "meth"
3026+
3027+
class WithEllipses(Protocol):
3028+
def meth(self) -> int: ...
3029+
class B(WithEllipses): ...
3030+
B() # E: Cannot instantiate abstract class "B" with abstract attribute "meth"
3031+
3032+
class WithDocstring(Protocol):
3033+
def meth(self) -> int:
3034+
"""Docstring for meth.
3035+
3036+
This is meth."""
3037+
class C(WithDocstring): ...
3038+
C() # E: Cannot instantiate abstract class "C" with abstract attribute "meth"
3039+
3040+
class WithRaise(Protocol):
3041+
def meth(self) -> int:
3042+
"""Docstring for meth."""
3043+
raise NotImplementedError
3044+
class D(WithRaise): ...
3045+
D() # E: Cannot instantiate abstract class "D" with abstract attribute "meth"
3046+
[builtins fixtures/exception.pyi]
3047+
3048+
[case testEmptyBodyNonAbstractProtocol]
3049+
from typing import Any, Optional, Protocol, Union
3050+
3051+
class NotAbstract(Protocol):
3052+
def f(self) -> None: ...
3053+
def g(self) -> Any: ...
3054+
def h(self, x: int): ...
3055+
def j(self): ...
3056+
def k(self, x): ...
3057+
def l(self) -> Optional[int]: ...
3058+
def m(self) -> Union[str, None]: ...
3059+
3060+
class A(NotAbstract): ...
3061+
A()

0 commit comments

Comments
 (0)