Skip to content

Commit 1eb5fae

Browse files
authored
New semantic analyzer: abstract classes (#6423)
This fixes abstract classes by moving determination of abstract attributes to a new pass that happens between the main semantic analysis pass and type checking. The new pass is added mostly to simplify the main semantic analysis pass. This also adds simple type inference for decorated functions from the old semantic analysis pass 3. This fixes a test case where we need access to the signature of an abstract method when type checking the module top level. Since module top levels can't be deferred during type checking, the simple type inference is needed to remain compatible with the old semantic analyzer. Fixes #6322.
1 parent a2f44c6 commit 1eb5fae

File tree

9 files changed

+234
-151
lines changed

9 files changed

+234
-151
lines changed

mypy/build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2833,7 +2833,7 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
28332833
if not manager.options.new_semantic_analyzer:
28342834
manager.semantic_analyzer.add_builtin_aliases(typing_mod)
28352835
if manager.options.new_semantic_analyzer:
2836-
semantic_analysis_for_scc(graph, scc)
2836+
semantic_analysis_for_scc(graph, scc, manager.errors)
28372837
else:
28382838
for id in stale:
28392839
graph[id].semantic_analysis()

mypy/newsemanal/semanal.py

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,6 @@ def analyze_class_body_common(self, defn: ClassDef) -> None:
937937
"""Parts of class body analysis that are common to all kinds of class defs."""
938938
self.enter_class(defn.info)
939939
defn.defs.accept(self)
940-
self.calculate_abstract_status(defn.info)
941940
self.apply_class_plugin_hooks(defn)
942941
self.leave_class()
943942

@@ -1028,56 +1027,6 @@ def analyze_class_decorator(self, defn: ClassDef, decorator: Expression) -> None
10281027
'typing_extensions.final'):
10291028
defn.info.is_final = True
10301029

1031-
def calculate_abstract_status(self, typ: TypeInfo) -> None:
1032-
"""Calculate abstract status of a class.
1033-
1034-
Set is_abstract of the type to True if the type has an unimplemented
1035-
abstract attribute. Also compute a list of abstract attributes.
1036-
"""
1037-
concrete = set() # type: Set[str]
1038-
abstract = [] # type: List[str]
1039-
abstract_in_this_class = [] # type: List[str]
1040-
for base in typ.mro:
1041-
for name, symnode in base.names.items():
1042-
node = symnode.node
1043-
if isinstance(node, OverloadedFuncDef):
1044-
# Unwrap an overloaded function definition. We can just
1045-
# check arbitrarily the first overload item. If the
1046-
# different items have a different abstract status, there
1047-
# should be an error reported elsewhere.
1048-
func = node.items[0] # type: Optional[Node]
1049-
else:
1050-
func = node
1051-
if isinstance(func, Decorator):
1052-
fdef = func.func
1053-
if fdef.is_abstract and name not in concrete:
1054-
typ.is_abstract = True
1055-
abstract.append(name)
1056-
if base is typ:
1057-
abstract_in_this_class.append(name)
1058-
elif isinstance(node, Var):
1059-
if node.is_abstract_var and name not in concrete:
1060-
typ.is_abstract = True
1061-
abstract.append(name)
1062-
if base is typ:
1063-
abstract_in_this_class.append(name)
1064-
concrete.add(name)
1065-
# In stubs, abstract classes need to be explicitly marked because it is too
1066-
# easy to accidentally leave a concrete class abstract by forgetting to
1067-
# implement some methods.
1068-
typ.abstract_attributes = sorted(abstract)
1069-
if not self.is_stub_file:
1070-
return
1071-
if (typ.declared_metaclass and typ.declared_metaclass.type.fullname() == 'abc.ABCMeta'):
1072-
return
1073-
if typ.is_protocol:
1074-
return
1075-
if abstract and not abstract_in_this_class:
1076-
attrs = ", ".join('"{}"'.format(attr) for attr in sorted(abstract))
1077-
self.fail("Class {} has abstract attributes {}".format(typ.fullname(), attrs), typ)
1078-
self.note("If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass",
1079-
typ)
1080-
10811030
def setup_type_promotion(self, defn: ClassDef) -> None:
10821031
"""Setup extra, ad-hoc subtyping relationships between classes (promotion).
10831032

mypy/newsemanal/semanal_abstract.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Calculate the abstract status of classes.
2+
3+
This happens after semantic analysis and before type checking.
4+
"""
5+
6+
from typing import List, Set, Optional
7+
8+
from mypy.nodes import Node, MypyFile, SymbolTable, TypeInfo, Var, Decorator, OverloadedFuncDef
9+
from mypy.errors import Errors
10+
11+
12+
def calculate_abstract_status(file: MypyFile, errors: Errors) -> None:
13+
"""Calculate the abstract status of all classes in the symbol table in file.
14+
15+
Also check that ABCMeta is used correctly.
16+
"""
17+
process(file.names, file.is_stub, file.fullname(), errors)
18+
19+
20+
def process(names: SymbolTable, is_stub_file: bool, prefix: str, errors: Errors) -> None:
21+
for name, symnode in names.items():
22+
node = symnode.node
23+
if isinstance(node, TypeInfo) and node.fullname().startswith(prefix):
24+
calculate_class_abstract_status(node, is_stub_file, errors)
25+
new_prefix = prefix + '.' + node.name()
26+
process(node.names, is_stub_file, new_prefix, errors)
27+
28+
29+
def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: Errors) -> None:
30+
"""Calculate abstract status of a class.
31+
32+
Set is_abstract of the type to True if the type has an unimplemented
33+
abstract attribute. Also compute a list of abstract attributes.
34+
Report error is required ABCMeta metaclass is missing.
35+
"""
36+
concrete = set() # type: Set[str]
37+
abstract = [] # type: List[str]
38+
abstract_in_this_class = [] # type: List[str]
39+
for base in typ.mro:
40+
for name, symnode in base.names.items():
41+
node = symnode.node
42+
if isinstance(node, OverloadedFuncDef):
43+
# Unwrap an overloaded function definition. We can just
44+
# check arbitrarily the first overload item. If the
45+
# different items have a different abstract status, there
46+
# should be an error reported elsewhere.
47+
func = node.items[0] # type: Optional[Node]
48+
else:
49+
func = node
50+
if isinstance(func, Decorator):
51+
fdef = func.func
52+
if fdef.is_abstract and name not in concrete:
53+
typ.is_abstract = True
54+
abstract.append(name)
55+
if base is typ:
56+
abstract_in_this_class.append(name)
57+
elif isinstance(node, Var):
58+
if node.is_abstract_var and name not in concrete:
59+
typ.is_abstract = True
60+
abstract.append(name)
61+
if base is typ:
62+
abstract_in_this_class.append(name)
63+
concrete.add(name)
64+
# In stubs, abstract classes need to be explicitly marked because it is too
65+
# easy to accidentally leave a concrete class abstract by forgetting to
66+
# implement some methods.
67+
typ.abstract_attributes = sorted(abstract)
68+
if is_stub_file:
69+
if typ.declared_metaclass and typ.declared_metaclass.type.fullname() == 'abc.ABCMeta':
70+
return
71+
if typ.is_protocol:
72+
return
73+
if abstract and not abstract_in_this_class:
74+
def report(message: str, severity: str) -> None:
75+
errors.report(typ.line, typ.column, message, severity=severity)
76+
77+
attrs = ", ".join('"{}"'.format(attr) for attr in sorted(abstract))
78+
report("Class {} has abstract attributes {}".format(typ.fullname(), attrs), 'error')
79+
report("If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass",
80+
'note')

mypy/newsemanal/semanal_infer.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Simple type inference for decorated functions during semantic analysis."""
2+
3+
from typing import Optional
4+
5+
from mypy.nodes import Expression, Decorator, CallExpr, FuncDef, RefExpr, Var, ARG_POS
6+
from mypy.types import Type, CallableType, AnyType, TypeOfAny, TypeVarType, function_type
7+
from mypy.typevars import has_no_typevars
8+
from mypy.newsemanal.semanal_shared import SemanticAnalyzerInterface
9+
10+
11+
def infer_decorator_signature_if_simple(dec: Decorator,
12+
analyzer: SemanticAnalyzerInterface) -> None:
13+
"""Try to infer the type of the decorated function.
14+
15+
This lets us resolve additional references to decorated functions
16+
during type checking. Otherwise the type might not be available
17+
when we need it, since module top levels can't be deferred.
18+
19+
This basically uses a simple special-purpose type inference
20+
engine just for decorators.
21+
"""
22+
if dec.var.is_property:
23+
# Decorators are expected to have a callable type (it's a little odd).
24+
if dec.func.type is None:
25+
dec.var.type = CallableType(
26+
[AnyType(TypeOfAny.special_form)],
27+
[ARG_POS],
28+
[None],
29+
AnyType(TypeOfAny.special_form),
30+
analyzer.named_type('__builtins__.function'),
31+
name=dec.var.name())
32+
elif isinstance(dec.func.type, CallableType):
33+
dec.var.type = dec.func.type
34+
return
35+
decorator_preserves_type = True
36+
for expr in dec.decorators:
37+
preserve_type = False
38+
if isinstance(expr, RefExpr) and isinstance(expr.node, FuncDef):
39+
if expr.node.type and is_identity_signature(expr.node.type):
40+
preserve_type = True
41+
if not preserve_type:
42+
decorator_preserves_type = False
43+
break
44+
if decorator_preserves_type:
45+
# No non-identity decorators left. We can trivially infer the type
46+
# of the function here.
47+
dec.var.type = function_type(dec.func, analyzer.named_type('__builtins__.function'))
48+
if dec.decorators:
49+
return_type = calculate_return_type(dec.decorators[0])
50+
if return_type and isinstance(return_type, AnyType):
51+
# The outermost decorator will return Any so we know the type of the
52+
# decorated function.
53+
dec.var.type = AnyType(TypeOfAny.from_another_any, source_any=return_type)
54+
sig = find_fixed_callable_return(dec.decorators[0])
55+
if sig:
56+
# The outermost decorator always returns the same kind of function,
57+
# so we know that this is the type of the decorated function.
58+
orig_sig = function_type(dec.func, analyzer.named_type('__builtins__.function'))
59+
sig.name = orig_sig.items()[0].name
60+
dec.var.type = sig
61+
62+
63+
def is_identity_signature(sig: Type) -> bool:
64+
"""Is type a callable of form T -> T (where T is a type variable)?"""
65+
if isinstance(sig, CallableType) and sig.arg_kinds == [ARG_POS]:
66+
if isinstance(sig.arg_types[0], TypeVarType) and isinstance(sig.ret_type, TypeVarType):
67+
return sig.arg_types[0].id == sig.ret_type.id
68+
return False
69+
70+
71+
def calculate_return_type(expr: Expression) -> Optional[Type]:
72+
"""Return the return type if we can calculate it.
73+
74+
This only uses information available during semantic analysis so this
75+
will sometimes return None because of insufficient information (as
76+
type inference hasn't run yet).
77+
"""
78+
if isinstance(expr, RefExpr):
79+
if isinstance(expr.node, FuncDef):
80+
typ = expr.node.type
81+
if typ is None:
82+
# No signature -> default to Any.
83+
return AnyType(TypeOfAny.unannotated)
84+
# Explicit Any return?
85+
if isinstance(typ, CallableType):
86+
return typ.ret_type
87+
return None
88+
elif isinstance(expr.node, Var):
89+
return expr.node.type
90+
elif isinstance(expr, CallExpr):
91+
return calculate_return_type(expr.callee)
92+
return None
93+
94+
95+
def find_fixed_callable_return(expr: Expression) -> Optional[CallableType]:
96+
"""Return the return type, if expression refers to a callable that returns a callable.
97+
98+
But only do this if the return type has no type variables. Return None otherwise.
99+
This approximates things a lot as this is supposed to be called before type checking
100+
when full type information is not available yet.
101+
"""
102+
if isinstance(expr, RefExpr):
103+
if isinstance(expr.node, FuncDef):
104+
typ = expr.node.type
105+
if typ:
106+
if isinstance(typ, CallableType) and has_no_typevars(typ.ret_type):
107+
if isinstance(typ.ret_type, CallableType):
108+
return typ.ret_type
109+
elif isinstance(expr, CallExpr):
110+
t = find_fixed_callable_return(expr.callee)
111+
if t:
112+
if isinstance(t.ret_type, CallableType):
113+
return t.ret_type
114+
return None

mypy/newsemanal/semanal_main.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@
3131
)
3232
from mypy.newsemanal.semanal_typeargs import TypeArgumentAnalyzer
3333
from mypy.state import strict_optional_set
34+
from mypy.newsemanal.semanal import NewSemanticAnalyzer
35+
from mypy.newsemanal.semanal_abstract import calculate_abstract_status
36+
from mypy.errors import Errors
37+
from mypy.newsemanal.semanal_infer import infer_decorator_signature_if_simple
3438

3539
MYPY = False
3640
if MYPY:
3741
from mypy.build import Graph, State
38-
from mypy.newsemanal.semanal import NewSemanticAnalyzer
3942

4043

4144
# Perform up to this many semantic analysis iterations until giving up trying to bind all names.
@@ -46,7 +49,7 @@
4649
core_modules = ['typing', 'builtins', 'abc', 'collections']
4750

4851

49-
def semantic_analysis_for_scc(graph: 'Graph', scc: List[str]) -> None:
52+
def semantic_analysis_for_scc(graph: 'Graph', scc: List[str], errors: Errors) -> None:
5053
"""Perform semantic analysis for all modules in a SCC (import cycle).
5154
5255
Assume that reachability analysis has already been performed.
@@ -56,7 +59,8 @@ def semantic_analysis_for_scc(graph: 'Graph', scc: List[str]) -> None:
5659
# before functions. This limitation is unlikely to go away soon.
5760
process_top_levels(graph, scc)
5861
process_functions(graph, scc)
59-
check_type_arguments(graph, scc)
62+
check_type_arguments(graph, scc, errors)
63+
process_abstract_status(graph, scc, errors)
6064

6165

6266
def process_top_levels(graph: 'Graph', scc: List[str]) -> None:
@@ -183,22 +187,31 @@ def semantic_analyze_target(target: str,
183187
fnam=tree.path,
184188
options=state.options,
185189
active_type=active_type):
186-
if isinstance(node, Decorator):
190+
refresh_node = node
191+
if isinstance(refresh_node, Decorator):
187192
# Decorator expressions will be processed as part of the module top level.
188-
node = node.func
189-
analyzer.refresh_partial(node, [], final_iteration)
193+
refresh_node = refresh_node.func
194+
analyzer.refresh_partial(refresh_node, [], final_iteration)
195+
if isinstance(node, Decorator):
196+
infer_decorator_signature_if_simple(node, analyzer)
190197
if analyzer.deferred:
191198
return [target], analyzer.incomplete
192199
else:
193200
return [], analyzer.incomplete
194201

195202

196-
def check_type_arguments(graph: 'Graph', scc: List[str]) -> None:
203+
def check_type_arguments(graph: 'Graph', scc: List[str], errors: Errors) -> None:
197204
for module in scc:
198205
state = graph[module]
199-
errors = state.manager.errors
200206
assert state.tree
201207
analyzer = TypeArgumentAnalyzer(errors)
202208
with state.wrap_context():
203209
with strict_optional_set(state.options.strict_optional):
204210
state.tree.accept(analyzer)
211+
212+
213+
def process_abstract_status(graph: 'Graph', scc: List[str], errors: Errors) -> None:
214+
for module in scc:
215+
tree = graph[module].tree
216+
assert tree
217+
calculate_abstract_status(tree, errors)

0 commit comments

Comments
 (0)