Skip to content

Commit 2f9d921

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 c0e49ab commit 2f9d921

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
@@ -5489,3 +5515,59 @@ def is_same_symbol(a: Optional[SymbolNode], b: Optional[SymbolNode]) -> bool:
54895515
or (isinstance(a, PlaceholderNode)
54905516
and isinstance(b, PlaceholderNode))
54915517
or is_same_var_from_getattr(a, b))
5518+
5519+
5520+
def is_trivial_body(block: Block) -> bool:
5521+
"""Returns 'true' if the given body is "trivial" -- if it contains just a "pass",
5522+
"..." (ellipsis), or "raise NotImplementedError()". A trivial body may also
5523+
start with a statement containing just a string (e.g. a docstring).
5524+
5525+
Note: functions that raise other kinds of exceptions do not count as
5526+
"trivial". We use this function to help us determine when it's ok to
5527+
relax certain checks on body, but functions that raise arbitrary exceptions
5528+
are more likely to do non-trivial work. For example:
5529+
5530+
def halt(self, reason: str = ...) -> NoReturn:
5531+
raise MyCustomError("Fatal error: " + reason, self.line, self.context)
5532+
5533+
A function that raises just NotImplementedError is much less likely to be
5534+
this complex.
5535+
"""
5536+
body = block.body
5537+
5538+
# Skip a docstring
5539+
if (body and isinstance(body[0], ExpressionStmt) and
5540+
isinstance(body[0].expr, (StrExpr, UnicodeExpr))):
5541+
body = block.body[1:]
5542+
5543+
if len(body) == 0:
5544+
# There's only a docstring (or no body at all).
5545+
return True
5546+
elif len(body) > 1:
5547+
return False
5548+
5549+
stmt = body[0]
5550+
5551+
if isinstance(stmt, RaiseStmt):
5552+
expr = stmt.expr
5553+
if expr is None:
5554+
return False
5555+
if isinstance(expr, CallExpr):
5556+
expr = expr.callee
5557+
5558+
return (isinstance(expr, NameExpr)
5559+
and expr.fullname == 'builtins.NotImplementedError')
5560+
5561+
return (isinstance(stmt, PassStmt) or
5562+
(isinstance(stmt, ExpressionStmt) and
5563+
isinstance(stmt.expr, EllipsisExpr)))
5564+
5565+
5566+
def can_be_none(t: Type) -> bool:
5567+
"""Can a variable of the given type be None?"""
5568+
t = get_proper_type(t)
5569+
return (
5570+
isinstance(t, NoneType) or
5571+
isinstance(t, AnyType) or
5572+
(isinstance(t, UnionType) and any(can_be_none(ut) for ut in t.items))
5573+
)

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
@@ -2795,3 +2795,152 @@ class MyClass:
27952795
assert isinstance(self, MyProtocol)
27962796
[builtins fixtures/isinstance.pyi]
27972797
[typing fixtures/typing-full.pyi]
2798+
2799+
[case testEmptyBodyImplicitlyAbstractProtocol]
2800+
from typing import Protocol, overload, Union
2801+
2802+
class P1(Protocol):
2803+
def meth(self) -> int: ...
2804+
class B1(P1): ...
2805+
class C1(P1):
2806+
def meth(self) -> int:
2807+
return 0
2808+
B1() # E: Cannot instantiate abstract class "B1" with abstract attribute "meth"
2809+
C1()
2810+
2811+
class P2(Protocol):
2812+
@classmethod
2813+
def meth(cls) -> int: ...
2814+
class B2(P2): ...
2815+
class C2(P2):
2816+
@classmethod
2817+
def meth(cls) -> int:
2818+
return 0
2819+
B2() # E: Cannot instantiate abstract class "B2" with abstract attribute "meth"
2820+
C2()
2821+
2822+
class P3(Protocol):
2823+
@overload
2824+
def meth(self, x: int) -> int: ...
2825+
@overload
2826+
def meth(self, x: str) -> str: ...
2827+
class B3(P3): ...
2828+
class C3(P3):
2829+
@overload
2830+
def meth(self, x: int) -> int: ...
2831+
@overload
2832+
def meth(self, x: str) -> str: ...
2833+
def meth(self, x: Union[int, str]) -> Union[int, str]:
2834+
return 0
2835+
B3() # E: Cannot instantiate abstract class "B3" with abstract attribute "meth"
2836+
C3()
2837+
[builtins fixtures/classmethod.pyi]
2838+
2839+
[case testEmptyBodyImplicitlyAbstractProtocolProperty]
2840+
from typing import Protocol
2841+
2842+
class P1(Protocol):
2843+
@property
2844+
def attr(self) -> int: ...
2845+
class B1(P1): ...
2846+
class C1(P1):
2847+
@property
2848+
def attr(self) -> int:
2849+
return 0
2850+
B1() # E: Cannot instantiate abstract class "B1" with abstract attribute "attr"
2851+
C1()
2852+
2853+
class P2(Protocol):
2854+
@property
2855+
def attr(self) -> int: ...
2856+
@attr.setter
2857+
def attr(self, value: int) -> None: ...
2858+
class B2(P2): ...
2859+
class C2(P2):
2860+
@property
2861+
def attr(self) -> int: return 0
2862+
@attr.setter
2863+
def attr(self, value: int) -> None: pass
2864+
B2() # E: Cannot instantiate abstract class "B2" with abstract attribute "attr"
2865+
C2()
2866+
[builtins fixtures/property.pyi]
2867+
2868+
[case testEmptyBodyImplicitlyAbstractProtocolStub]
2869+
from stub import P1, P2, P3, P4
2870+
2871+
class B1(P1): ...
2872+
class B2(P2): ...
2873+
class B3(P3): ...
2874+
class B4(P4): ...
2875+
2876+
B1()
2877+
B2()
2878+
B3()
2879+
B4() # E: Cannot instantiate abstract class "B4" with abstract attribute "meth"
2880+
2881+
[file stub.pyi]
2882+
from typing import Protocol, overload, Union
2883+
from abc import abstractmethod
2884+
2885+
class P1(Protocol):
2886+
def meth(self) -> int: ...
2887+
2888+
class P2(Protocol):
2889+
@classmethod
2890+
def meth(cls) -> int: ...
2891+
2892+
class P3(Protocol):
2893+
@overload
2894+
def meth(self, x: int) -> int: ...
2895+
@overload
2896+
def meth(self, x: str) -> str: ...
2897+
2898+
class P4(Protocol):
2899+
@abstractmethod
2900+
def meth(self) -> int: ...
2901+
[builtins fixtures/classmethod.pyi]
2902+
2903+
[case testEmptyBodyVariationsImplicitlyAbstractProtocol]
2904+
from typing import Protocol
2905+
2906+
class WithPass(Protocol):
2907+
def meth(self) -> int:
2908+
pass
2909+
class A(WithPass): ...
2910+
A() # E: Cannot instantiate abstract class "A" with abstract attribute "meth"
2911+
2912+
class WithEllipses(Protocol):
2913+
def meth(self) -> int: ...
2914+
class B(WithEllipses): ...
2915+
B() # E: Cannot instantiate abstract class "B" with abstract attribute "meth"
2916+
2917+
class WithDocstring(Protocol):
2918+
def meth(self) -> int:
2919+
"""Docstring for meth.
2920+
2921+
This is meth."""
2922+
class C(WithDocstring): ...
2923+
C() # E: Cannot instantiate abstract class "C" with abstract attribute "meth"
2924+
2925+
class WithRaise(Protocol):
2926+
def meth(self) -> int:
2927+
"""Docstring for meth."""
2928+
raise NotImplementedError
2929+
class D(WithRaise): ...
2930+
D() # E: Cannot instantiate abstract class "D" with abstract attribute "meth"
2931+
[builtins fixtures/exception.pyi]
2932+
2933+
[case testEmptyBodyNonAbstractProtocol]
2934+
from typing import Any, Optional, Protocol, Union
2935+
2936+
class NotAbstract(Protocol):
2937+
def f(self) -> None: ...
2938+
def g(self) -> Any: ...
2939+
def h(self, x: int): ...
2940+
def j(self): ...
2941+
def k(self, x): ...
2942+
def l(self) -> Optional[int]: ...
2943+
def m(self) -> Union[str, None]: ...
2944+
2945+
class A(NotAbstract): ...
2946+
A()

0 commit comments

Comments
 (0)