Skip to content

Commit 22ca011

Browse files
committed
Treat methods with empty bodies in Protocols as abstract
See python#8005 for the motivation. There are some subtleties to this; for one, we can't apply the rule to type stubs, because they never have a function body. And of course, if the return type is `None` or `Any`, then an empty function is completely valid.
1 parent af366c0 commit 22ca011

File tree

3 files changed

+67
-4
lines changed

3 files changed

+67
-4
lines changed

mypy/semanal.py

Lines changed: 26 additions & 1 deletion
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,
@@ -667,6 +667,16 @@ def analyze_func_def(self, defn: FuncDef) -> None:
667667
assert isinstance(defn.type, CallableType)
668668
defn.type = set_callable_name(defn.type, defn)
669669

670+
if (self.is_class_scope() and self.type is not None and
671+
defn.type is not None and isinstance(defn.type, CallableType)):
672+
# Treat empty functions in Protocol as abstract
673+
# Conditions:
674+
# not an overload, not a stub file, with non-None return type annatation
675+
if (not self.is_stub_file and self.type.is_protocol and not defn.is_decorated and
676+
not isinstance(get_proper_type(defn.type.ret_type), (NoneType, AnyType)) and
677+
is_empty_function_body(defn.body.body)):
678+
defn.is_abstract = True
679+
670680
self.analyze_arg_initializers(defn)
671681
self.analyze_function_body(defn)
672682
if (defn.is_coroutine and
@@ -5477,3 +5487,18 @@ def is_same_symbol(a: Optional[SymbolNode], b: Optional[SymbolNode]) -> bool:
54775487
or (isinstance(a, PlaceholderNode)
54785488
and isinstance(b, PlaceholderNode))
54795489
or is_same_var_from_getattr(a, b))
5490+
5491+
5492+
def is_empty_function_body(body: List[Statement]) -> bool:
5493+
"""Is the function body empty?
5494+
5495+
We consider it empty if it comprises a single statement which is one of
5496+
1. ellipsis
5497+
2. pass
5498+
3. docstring
5499+
"""
5500+
if len(body) != 1:
5501+
return False
5502+
if isinstance(body[0], ExpressionStmt):
5503+
return isinstance(body[0].expr, (EllipsisExpr, StrExpr))
5504+
return isinstance(body[0], PassStmt)

mypy/semanal_classprop.py

Lines changed: 3 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
@@ -78,8 +78,8 @@ def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: E
7878
func = None
7979
else:
8080
func = node
81-
if isinstance(func, Decorator):
82-
fdef = func.func
81+
if isinstance(func, Decorator) or (isinstance(func, FuncDef) and not typ.is_protocol):
82+
fdef = func.func if isinstance(func, Decorator) else func
8383
if fdef.is_abstract and name not in concrete:
8484
typ.is_abstract = True
8585
abstract.append(name)

test-data/unit/check-protocols.test

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,44 @@ class C2(P):
10791079
C()
10801080
C2()
10811081

1082+
[case testCannotInstantiateEmptyMethodExplicitProtocolSubtypes]
1083+
from typing import Protocol
1084+
1085+
class P(Protocol):
1086+
def meth(self) -> int:
1087+
pass
1088+
1089+
class A(P):
1090+
pass
1091+
1092+
A() # E: Cannot instantiate abstract class "A" with abstract attribute "meth"
1093+
1094+
class B(P):
1095+
def meth(self) -> int:
1096+
return 0
1097+
1098+
B()
1099+
1100+
class P2(Protocol):
1101+
def meth(self) -> int:
1102+
...
1103+
1104+
class A2(P):
1105+
pass
1106+
1107+
A2() # E: Cannot instantiate abstract class "A2" with abstract attribute "meth"
1108+
1109+
class P3(Protocol):
1110+
def meth(self) -> int:
1111+
"""Docstring for meth.
1112+
1113+
This is meth."""
1114+
1115+
class A3(P):
1116+
pass
1117+
1118+
A3() # E: Cannot instantiate abstract class "A3" with abstract attribute "meth"
1119+
10821120
[case testCannotInstantiateAbstractVariableExplicitProtocolSubtypes]
10831121
from typing import Protocol
10841122

0 commit comments

Comments
 (0)