Skip to content

Commit

Permalink
Treat methods with empty bodies in Protocols as abstract
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tmke8 committed Jan 24, 2022
1 parent af366c0 commit 22ca011
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 4 deletions.
27 changes: 26 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
ClassDef, Var, GDEF, FuncItem, Import, Expression, Lvalue,
ImportFrom, ImportAll, Block, LDEF, NameExpr, MemberExpr,
IndexExpr, TupleExpr, ListExpr, ExpressionStmt, ReturnStmt,
RaiseStmt, AssertStmt, OperatorAssignmentStmt, WhileStmt,
RaiseStmt, AssertStmt, OperatorAssignmentStmt, WhileStmt, PassStmt,
ForStmt, BreakStmt, ContinueStmt, IfStmt, TryStmt, WithStmt, DelStmt,
GlobalDecl, SuperExpr, DictExpr, CallExpr, RefExpr, OpExpr, UnaryExpr,
SliceExpr, CastExpr, RevealExpr, TypeApplication, Context, SymbolTable,
Expand Down Expand Up @@ -667,6 +667,16 @@ def analyze_func_def(self, defn: FuncDef) -> None:
assert isinstance(defn.type, CallableType)
defn.type = set_callable_name(defn.type, defn)

if (self.is_class_scope() and self.type is not None and
defn.type is not None and isinstance(defn.type, CallableType)):
# Treat empty functions in Protocol as abstract
# Conditions:
# not an overload, not a stub file, with non-None return type annatation
if (not self.is_stub_file and self.type.is_protocol and not defn.is_decorated and
not isinstance(get_proper_type(defn.type.ret_type), (NoneType, AnyType)) and
is_empty_function_body(defn.body.body)):
defn.is_abstract = True

self.analyze_arg_initializers(defn)
self.analyze_function_body(defn)
if (defn.is_coroutine and
Expand Down Expand Up @@ -5477,3 +5487,18 @@ def is_same_symbol(a: Optional[SymbolNode], b: Optional[SymbolNode]) -> bool:
or (isinstance(a, PlaceholderNode)
and isinstance(b, PlaceholderNode))
or is_same_var_from_getattr(a, b))


def is_empty_function_body(body: List[Statement]) -> bool:
"""Is the function body empty?
We consider it empty if it comprises a single statement which is one of
1. ellipsis
2. pass
3. docstring
"""
if len(body) != 1:
return False
if isinstance(body[0], ExpressionStmt):
return isinstance(body[0].expr, (EllipsisExpr, StrExpr))
return isinstance(body[0], PassStmt)
6 changes: 3 additions & 3 deletions mypy/semanal_classprop.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing_extensions import Final

from mypy.nodes import (
Node, TypeInfo, Var, Decorator, OverloadedFuncDef, SymbolTable, CallExpr, PromoteExpr,
Node, TypeInfo, Var, Decorator, OverloadedFuncDef, SymbolTable, CallExpr, PromoteExpr, FuncDef
)
from mypy.types import Instance, Type
from mypy.errors import Errors
Expand Down Expand Up @@ -78,8 +78,8 @@ def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: E
func = None
else:
func = node
if isinstance(func, Decorator):
fdef = func.func
if isinstance(func, Decorator) or (isinstance(func, FuncDef) and not typ.is_protocol):
fdef = func.func if isinstance(func, Decorator) else func
if fdef.is_abstract and name not in concrete:
typ.is_abstract = True
abstract.append(name)
Expand Down
38 changes: 38 additions & 0 deletions test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,44 @@ class C2(P):
C()
C2()

[case testCannotInstantiateEmptyMethodExplicitProtocolSubtypes]
from typing import Protocol

class P(Protocol):
def meth(self) -> int:
pass

class A(P):
pass

A() # E: Cannot instantiate abstract class "A" with abstract attribute "meth"

class B(P):
def meth(self) -> int:
return 0

B()

class P2(Protocol):
def meth(self) -> int:
...

class A2(P):
pass

A2() # E: Cannot instantiate abstract class "A2" with abstract attribute "meth"

class P3(Protocol):
def meth(self) -> int:
"""Docstring for meth.

This is meth."""

class A3(P):
pass

A3() # E: Cannot instantiate abstract class "A3" with abstract attribute "meth"

[case testCannotInstantiateAbstractVariableExplicitProtocolSubtypes]
from typing import Protocol

Expand Down

0 comments on commit 22ca011

Please sign in to comment.