Skip to content

Commit af4e8fd

Browse files
committed
Treat methods with empty bodies in Protocols as abstract
Closes python#8005 Closes python#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 6648199 commit af4e8fd

File tree

4 files changed

+237
-48
lines changed

4 files changed

+237
-48
lines changed

mypy/checker.py

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@
115115
ReturnStmt,
116116
StarExpr,
117117
Statement,
118-
StrExpr,
119118
SymbolTable,
120119
SymbolTableNode,
121120
TempNode,
@@ -134,7 +133,7 @@
134133
from mypy.plugin import CheckerPluginInterface, Plugin
135134
from mypy.sametypes import is_same_type
136135
from mypy.scope import Scope
137-
from mypy.semanal import refers_to_fullname, set_callable_name
136+
from mypy.semanal import is_trivial_body, refers_to_fullname, set_callable_name
138137
from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS
139138
from mypy.sharedparse import BINARY_MAGIC_METHODS
140139
from mypy.state import state
@@ -1171,7 +1170,7 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
11711170
item.arguments[i].variable.type = arg_type
11721171

11731172
# Type check initialization expressions.
1174-
body_is_trivial = self.is_trivial_body(defn.body)
1173+
body_is_trivial = is_trivial_body(defn.body)
11751174
self.check_default_args(item, body_is_trivial)
11761175

11771176
# Type check body in a new scope.
@@ -1339,48 +1338,6 @@ def check___new___signature(self, fdef: FuncDef, typ: CallableType) -> None:
13391338
"but must return a subtype of",
13401339
)
13411340

1342-
def is_trivial_body(self, block: Block) -> bool:
1343-
"""Returns 'true' if the given body is "trivial" -- if it contains just a "pass",
1344-
"..." (ellipsis), or "raise NotImplementedError()". A trivial body may also
1345-
start with a statement containing just a string (e.g. a docstring).
1346-
1347-
Note: functions that raise other kinds of exceptions do not count as
1348-
"trivial". We use this function to help us determine when it's ok to
1349-
relax certain checks on body, but functions that raise arbitrary exceptions
1350-
are more likely to do non-trivial work. For example:
1351-
1352-
def halt(self, reason: str = ...) -> NoReturn:
1353-
raise MyCustomError("Fatal error: " + reason, self.line, self.context)
1354-
1355-
A function that raises just NotImplementedError is much less likely to be
1356-
this complex.
1357-
"""
1358-
body = block.body
1359-
1360-
# Skip a docstring
1361-
if body and isinstance(body[0], ExpressionStmt) and isinstance(body[0].expr, StrExpr):
1362-
body = block.body[1:]
1363-
1364-
if len(body) == 0:
1365-
# There's only a docstring (or no body at all).
1366-
return True
1367-
elif len(body) > 1:
1368-
return False
1369-
1370-
stmt = body[0]
1371-
1372-
if isinstance(stmt, RaiseStmt):
1373-
expr = stmt.expr
1374-
if expr is None:
1375-
return False
1376-
if isinstance(expr, CallExpr):
1377-
expr = expr.callee
1378-
1379-
return isinstance(expr, NameExpr) and expr.fullname == "builtins.NotImplementedError"
1380-
1381-
return isinstance(stmt, PassStmt) or (
1382-
isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, EllipsisExpr)
1383-
)
13841341

13851342
def check_reverse_op_method(
13861343
self, defn: FuncItem, reverse_type: CallableType, reverse_name: str, context: Context

mypy/semanal.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
OverloadedFuncDef,
146146
OverloadPart,
147147
ParamSpecExpr,
148+
PassStmt,
148149
PlaceholderNode,
149150
PromoteExpr,
150151
RaiseStmt,
@@ -837,6 +838,16 @@ def analyze_func_def(self, defn: FuncDef) -> None:
837838

838839
self.analyze_arg_initializers(defn)
839840
self.analyze_function_body(defn)
841+
842+
# Mark protocol methods with empty bodies and None-incompatible return types as abstract.
843+
if self.is_class_scope() and defn.type is not None:
844+
assert self.type is not None and isinstance(defn.type, CallableType)
845+
if (not self.is_stub_file and self.type.is_protocol and
846+
(not isinstance(self.scope.function, OverloadedFuncDef)
847+
or defn.is_property) and
848+
not can_be_none(defn.type.ret_type) and is_trivial_body(defn.body)):
849+
defn.is_abstract = True
850+
840851
if (
841852
defn.is_coroutine
842853
and isinstance(defn.type, CallableType)
@@ -975,6 +986,21 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
975986
# We know this is an overload def. Infer properties and perform some checks.
976987
self.process_final_in_overload(defn)
977988
self.process_static_or_class_method_in_overload(defn)
989+
if defn.impl:
990+
self.process_overload_impl(defn)
991+
992+
def process_overload_impl(self, defn: OverloadedFuncDef) -> None:
993+
"""Set flags for an overload implementation.
994+
995+
Currently, this checks for a trivial body in protocols classes,
996+
where it makes the method implicitly abstract.
997+
"""
998+
assert defn.impl is not None
999+
impl = defn.impl if isinstance(defn.impl, FuncDef) else defn.impl.func
1000+
if is_trivial_body(impl.body) and self.is_class_scope() and not self.is_stub_file:
1001+
assert self.type is not None
1002+
if self.type.is_protocol:
1003+
impl.is_abstract = True
9781004

9791005
def analyze_overload_sigs_and_impl(
9801006
self, defn: OverloadedFuncDef
@@ -1052,7 +1078,8 @@ def handle_missing_overload_implementation(self, defn: OverloadedFuncDef) -> Non
10521078
"""Generate error about missing overload implementation (only if needed)."""
10531079
if not self.is_stub_file:
10541080
if self.type and self.type.is_protocol and not self.is_func_scope():
1055-
# An overloaded protocol method doesn't need an implementation.
1081+
# An overloaded protocol method doesn't need an implementation,
1082+
# but if it doesn't have one, then it is considered implicitly abstract.
10561083
for item in defn.items:
10571084
if isinstance(item, Decorator):
10581085
item.func.is_abstract = True
@@ -6033,3 +6060,57 @@ def is_same_symbol(a: Optional[SymbolNode], b: Optional[SymbolNode]) -> bool:
60336060
or (isinstance(a, PlaceholderNode) and isinstance(b, PlaceholderNode))
60346061
or is_same_var_from_getattr(a, b)
60356062
)
6063+
6064+
6065+
def is_trivial_body(block: Block) -> bool:
6066+
"""Returns 'true' if the given body is "trivial" -- if it contains just a "pass",
6067+
"..." (ellipsis), or "raise NotImplementedError()". A trivial body may also
6068+
start with a statement containing just a string (e.g. a docstring).
6069+
6070+
Note: functions that raise other kinds of exceptions do not count as
6071+
"trivial". We use this function to help us determine when it's ok to
6072+
relax certain checks on body, but functions that raise arbitrary exceptions
6073+
are more likely to do non-trivial work. For example:
6074+
6075+
def halt(self, reason: str = ...) -> NoReturn:
6076+
raise MyCustomError("Fatal error: " + reason, self.line, self.context)
6077+
6078+
A function that raises just NotImplementedError is much less likely to be
6079+
this complex.
6080+
"""
6081+
body = block.body
6082+
6083+
# Skip a docstring
6084+
if body and isinstance(body[0], ExpressionStmt) and isinstance(body[0].expr, StrExpr):
6085+
body = block.body[1:]
6086+
6087+
if len(body) == 0:
6088+
# There's only a docstring (or no body at all).
6089+
return True
6090+
elif len(body) > 1:
6091+
return False
6092+
6093+
stmt = body[0]
6094+
6095+
if isinstance(stmt, RaiseStmt):
6096+
expr = stmt.expr
6097+
if expr is None:
6098+
return False
6099+
if isinstance(expr, CallExpr):
6100+
expr = expr.callee
6101+
6102+
return isinstance(expr, NameExpr) and expr.fullname == "builtins.NotImplementedError"
6103+
6104+
return isinstance(stmt, PassStmt) or (
6105+
isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, EllipsisExpr)
6106+
)
6107+
6108+
6109+
def can_be_none(t: Type) -> bool:
6110+
"""Can a variable of the given type be None?"""
6111+
t = get_proper_type(t)
6112+
return (
6113+
isinstance(t, NoneType) or
6114+
isinstance(t, AnyType) or
6115+
(isinstance(t, UnionType) and any(can_be_none(ut) for ut in t.items))
6116+
)

mypy/semanal_classprop.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from mypy.nodes import (
1212
CallExpr,
1313
Decorator,
14+
FuncDef,
1415
Node,
1516
OverloadedFuncDef,
1617
PromoteExpr,
@@ -69,8 +70,9 @@ def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: E
6970
else:
7071
func = node
7172
if isinstance(func, Decorator):
72-
fdef = func.func
73-
if fdef.is_abstract and name not in concrete:
73+
func = func.func
74+
if isinstance(func, FuncDef):
75+
if func.is_abstract and name not in concrete:
7476
typ.is_abstract = True
7577
abstract.append(name)
7678
if base is typ:

test-data/unit/check-protocols.test

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

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

0 commit comments

Comments
 (0)