Skip to content

Commit 1bb970a

Browse files
authored
Treat methods with empty bodies in Protocols as abstract (#12118)
Also give a note if return type is compatible with `None` (and strict optional is on).
1 parent d468b85 commit 1bb970a

File tree

11 files changed

+406
-87
lines changed

11 files changed

+406
-87
lines changed

mypy/checker.py

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@
6767
CONTRAVARIANT,
6868
COVARIANT,
6969
GDEF,
70+
IMPLICITLY_ABSTRACT,
7071
INVARIANT,
72+
IS_ABSTRACT,
7173
LDEF,
7274
LITERAL_TYPE,
7375
MDEF,
@@ -115,7 +117,6 @@
115117
ReturnStmt,
116118
StarExpr,
117119
Statement,
118-
StrExpr,
119120
SymbolTable,
120121
SymbolTableNode,
121122
TempNode,
@@ -134,7 +135,7 @@
134135
from mypy.plugin import CheckerPluginInterface, Plugin
135136
from mypy.sametypes import is_same_type
136137
from mypy.scope import Scope
137-
from mypy.semanal import refers_to_fullname, set_callable_name
138+
from mypy.semanal import is_trivial_body, refers_to_fullname, set_callable_name
138139
from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS
139140
from mypy.sharedparse import BINARY_MAGIC_METHODS
140141
from mypy.state import state
@@ -618,7 +619,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
618619
for fdef in defn.items:
619620
assert isinstance(fdef, Decorator)
620621
self.check_func_item(fdef.func, name=fdef.func.name)
621-
if fdef.func.is_abstract:
622+
if fdef.func.abstract_status in (IS_ABSTRACT, IMPLICITLY_ABSTRACT):
622623
num_abstract += 1
623624
if num_abstract not in (0, len(defn.items)):
624625
self.fail(message_registry.INCONSISTENT_ABSTRACT_OVERLOAD, defn)
@@ -1171,7 +1172,7 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
11711172
item.arguments[i].variable.type = arg_type
11721173

11731174
# Type check initialization expressions.
1174-
body_is_trivial = self.is_trivial_body(defn.body)
1175+
body_is_trivial = is_trivial_body(defn.body)
11751176
self.check_default_args(item, body_is_trivial)
11761177

11771178
# Type check body in a new scope.
@@ -1339,49 +1340,6 @@ def check___new___signature(self, fdef: FuncDef, typ: CallableType) -> None:
13391340
"but must return a subtype of",
13401341
)
13411342

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-
)
1384-
13851343
def check_reverse_op_method(
13861344
self, defn: FuncItem, reverse_type: CallableType, reverse_name: str, context: Context
13871345
) -> None:

mypy/checkexpr.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
ARG_POS,
2828
ARG_STAR,
2929
ARG_STAR2,
30+
IMPLICITLY_ABSTRACT,
3031
LITERAL_TYPE,
3132
REVEAL_TYPE,
3233
ArgKind,
@@ -96,6 +97,7 @@
9697
)
9798
from mypy.sametypes import is_same_type
9899
from mypy.semanal_enum import ENUM_BASES
100+
from mypy.state import state
99101
from mypy.subtypes import is_equivalent, is_subtype, non_method_protocol_members
100102
from mypy.traverser import has_await_expression
101103
from mypy.typeanal import (
@@ -1236,24 +1238,32 @@ def check_callable_call(
12361238

12371239
if (
12381240
callee.is_type_obj()
1239-
and callee.type_object().is_abstract
1241+
and callee.type_object().is_protocol
12401242
# Exception for Type[...]
12411243
and not callee.from_type_type
1242-
and not callee.type_object().fallback_to_any
12431244
):
1244-
type = callee.type_object()
1245-
self.msg.cannot_instantiate_abstract_class(
1246-
callee.type_object().name, type.abstract_attributes, context
1245+
self.chk.fail(
1246+
message_registry.CANNOT_INSTANTIATE_PROTOCOL.format(callee.type_object().name),
1247+
context,
12471248
)
12481249
elif (
12491250
callee.is_type_obj()
1250-
and callee.type_object().is_protocol
1251+
and callee.type_object().is_abstract
12511252
# Exception for Type[...]
12521253
and not callee.from_type_type
1254+
and not callee.type_object().fallback_to_any
12531255
):
1254-
self.chk.fail(
1255-
message_registry.CANNOT_INSTANTIATE_PROTOCOL.format(callee.type_object().name),
1256-
context,
1256+
type = callee.type_object()
1257+
# Determine whether the implicitly abstract attributes are functions with
1258+
# None-compatible return types.
1259+
abstract_attributes: Dict[str, bool] = {}
1260+
for attr_name, abstract_status in type.abstract_attributes:
1261+
if abstract_status == IMPLICITLY_ABSTRACT:
1262+
abstract_attributes[attr_name] = self.can_return_none(type, attr_name)
1263+
else:
1264+
abstract_attributes[attr_name] = False
1265+
self.msg.cannot_instantiate_abstract_class(
1266+
callee.type_object().name, abstract_attributes, context
12571267
)
12581268

12591269
formal_to_actual = map_actuals_to_formals(
@@ -1335,6 +1345,30 @@ def check_callable_call(
13351345
callee = callee.copy_modified(ret_type=new_ret_type)
13361346
return callee.ret_type, callee
13371347

1348+
def can_return_none(self, type: TypeInfo, attr_name: str) -> bool:
1349+
"""Is the given attribute a method with a None-compatible return type?
1350+
1351+
Overloads are only checked if there is an implementation.
1352+
"""
1353+
if not state.strict_optional:
1354+
# If strict-optional is not set, is_subtype(NoneType(), T) is always True.
1355+
# So, we cannot do anything useful here in that case.
1356+
return False
1357+
for base in type.mro:
1358+
symnode = base.names.get(attr_name)
1359+
if symnode is None:
1360+
continue
1361+
node = symnode.node
1362+
if isinstance(node, OverloadedFuncDef):
1363+
node = node.impl
1364+
if isinstance(node, Decorator):
1365+
node = node.func
1366+
if isinstance(node, FuncDef):
1367+
if node.type is not None:
1368+
assert isinstance(node.type, CallableType)
1369+
return is_subtype(NoneType(), node.type.ret_type)
1370+
return False
1371+
13381372
def analyze_type_type_callee(self, item: ProperType, context: Context) -> Type:
13391373
"""Analyze the callee X in X(...) where X is Type[item].
13401374

mypy/messages.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1321,7 +1321,7 @@ def incompatible_conditional_function_def(self, defn: FuncDef) -> None:
13211321
self.fail("All conditional function variants must have identical " "signatures", defn)
13221322

13231323
def cannot_instantiate_abstract_class(
1324-
self, class_name: str, abstract_attributes: List[str], context: Context
1324+
self, class_name: str, abstract_attributes: Dict[str, bool], context: Context
13251325
) -> None:
13261326
attrs = format_string_list([f'"{a}"' for a in abstract_attributes])
13271327
self.fail(
@@ -1330,6 +1330,24 @@ def cannot_instantiate_abstract_class(
13301330
context,
13311331
code=codes.ABSTRACT,
13321332
)
1333+
attrs_with_none = [
1334+
f'"{a}"'
1335+
for a, implicit_and_can_return_none in abstract_attributes.items()
1336+
if implicit_and_can_return_none
1337+
]
1338+
if not attrs_with_none:
1339+
return
1340+
if len(attrs_with_none) == 1:
1341+
note = (
1342+
"The following method was marked implicitly abstract because it has an empty "
1343+
"function body: {}. If it is not meant to be abstract, explicitly return None."
1344+
)
1345+
else:
1346+
note = (
1347+
"The following methods were marked implicitly abstract because they have empty "
1348+
"function bodies: {}. If they are not meant to be abstract, explicitly return None."
1349+
)
1350+
self.note(note.format(format_string_list(attrs_with_none)), context, code=codes.ABSTRACT)
13331351

13341352
def base_class_definitions_incompatible(
13351353
self, name: str, base1: TypeInfo, base2: TypeInfo, context: Context

mypy/nodes.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,14 @@ def is_dynamic(self) -> bool:
758758
return self.type is None
759759

760760

761-
FUNCDEF_FLAGS: Final = FUNCITEM_FLAGS + ["is_decorated", "is_conditional", "is_abstract"]
761+
FUNCDEF_FLAGS: Final = FUNCITEM_FLAGS + ["is_decorated", "is_conditional"]
762+
763+
# Abstract status of a function
764+
NOT_ABSTRACT: Final = 0
765+
# Explicitly abstract (with @abstractmethod or overload without implementation)
766+
IS_ABSTRACT: Final = 1
767+
# Implicitly abstract: used for functions with trivial bodies defined in Protocols
768+
IMPLICITLY_ABSTRACT: Final = 2
762769

763770

764771
class FuncDef(FuncItem, SymbolNode, Statement):
@@ -771,7 +778,7 @@ class FuncDef(FuncItem, SymbolNode, Statement):
771778
"_name",
772779
"is_decorated",
773780
"is_conditional",
774-
"is_abstract",
781+
"abstract_status",
775782
"original_def",
776783
"deco_line",
777784
)
@@ -788,7 +795,7 @@ def __init__(
788795
self._name = name
789796
self.is_decorated = False
790797
self.is_conditional = False # Defined conditionally (within block)?
791-
self.is_abstract = False
798+
self.abstract_status = NOT_ABSTRACT
792799
self.is_final = False
793800
# Original conditional definition
794801
self.original_def: Union[None, FuncDef, Var, Decorator] = None
@@ -817,6 +824,7 @@ def serialize(self) -> JsonDict:
817824
"arg_kinds": [int(x.value) for x in self.arg_kinds],
818825
"type": None if self.type is None else self.type.serialize(),
819826
"flags": get_flags(self, FUNCDEF_FLAGS),
827+
"abstract_status": self.abstract_status,
820828
# TODO: Do we need expanded, original_def?
821829
}
822830

@@ -839,6 +847,7 @@ def deserialize(cls, data: JsonDict) -> "FuncDef":
839847
# NOTE: ret.info is set in the fixup phase.
840848
ret.arg_names = data["arg_names"]
841849
ret.arg_kinds = [ArgKind(x) for x in data["arg_kinds"]]
850+
ret.abstract_status = data["abstract_status"]
842851
# Leave these uninitialized so that future uses will trigger an error
843852
del ret.arguments
844853
del ret.max_pos
@@ -2674,7 +2683,9 @@ class is generic then it will be a type constructor of higher kind.
26742683
is_abstract: bool # Does the class have any abstract attributes?
26752684
is_protocol: bool # Is this a protocol class?
26762685
runtime_protocol: bool # Does this protocol support isinstance checks?
2677-
abstract_attributes: List[str]
2686+
# List of names of abstract attributes together with their abstract status.
2687+
# The abstract status must be one of `NOT_ABSTRACT`, `IS_ABSTRACT`, `IMPLICITLY_ABSTRACT`.
2688+
abstract_attributes: List[Tuple[str, int]]
26782689
deletable_attributes: List[str] # Used by mypyc only
26792690
# Does this type have concrete `__slots__` defined?
26802691
# If class does not have `__slots__` defined then it is `None`,
@@ -3034,7 +3045,7 @@ def deserialize(cls, data: JsonDict) -> "TypeInfo":
30343045
ti = TypeInfo(names, defn, module_name)
30353046
ti._fullname = data["fullname"]
30363047
# TODO: Is there a reason to reconstruct ti.subtypes?
3037-
ti.abstract_attributes = data["abstract_attributes"]
3048+
ti.abstract_attributes = [(attr[0], attr[1]) for attr in data["abstract_attributes"]]
30383049
ti.type_vars = data["type_vars"]
30393050
ti.has_param_spec_type = data["has_param_spec_type"]
30403051
ti.bases = [mypy.types.Instance.deserialize(b) for b in data["bases"]]

0 commit comments

Comments
 (0)