Skip to content

Commit 1bcfc04

Browse files
[mypyc] Introduce ClassBuilder and add support for attrs classes (#11328)
Create a class structure for building different types of classes, extension vs non-extension, plus various types of dataclasses. Specilize member expressions. Prior to this change dataclasses.field would only be specialized if called without the module, as field() Add support for attrs classes and build proper __annotations__ for dataclasses. Co-authored-by: Jingchen Ye <[email protected]>
1 parent f79e7af commit 1bcfc04

File tree

9 files changed

+671
-161
lines changed

9 files changed

+671
-161
lines changed

mypyc/ir/class_ir.py

+8
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ def __init__(self, name: str, module_name: str, is_trait: bool = False,
152152
# None if separate compilation prevents this from working
153153
self.children: Optional[List[ClassIR]] = []
154154

155+
def __repr__(self) -> str:
156+
return (
157+
"ClassIR("
158+
"name={self.name}, module_name={self.module_name}, "
159+
"is_trait={self.is_trait}, is_generated={self.is_generated}, "
160+
"is_abstract={self.is_abstract}, is_ext_class={self.is_ext_class}"
161+
")".format(self=self))
162+
155163
@property
156164
def fullname(self) -> str:
157165
return "{}.{}".format(self.module_name, self.name)

mypyc/irbuild/classdef.py

+260-120
Large diffs are not rendered by default.

mypyc/irbuild/expression.py

+8-20
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from mypyc.primitives.set_ops import set_add_op, set_update_op
3939
from mypyc.primitives.str_ops import str_slice_op
4040
from mypyc.primitives.int_ops import int_comparison_op_mapping
41-
from mypyc.irbuild.specialize import specializers
41+
from mypyc.irbuild.specialize import apply_function_specialization, apply_method_specialization
4242
from mypyc.irbuild.builder import IRBuilder
4343
from mypyc.irbuild.for_helpers import (
4444
translate_list_comprehension, translate_set_comprehension,
@@ -209,7 +209,8 @@ def transform_call_expr(builder: IRBuilder, expr: CallExpr) -> Value:
209209
callee = callee.analyzed.expr # Unwrap type application
210210

211211
if isinstance(callee, MemberExpr):
212-
return translate_method_call(builder, expr, callee)
212+
return apply_method_specialization(builder, expr, callee) or \
213+
translate_method_call(builder, expr, callee)
213214
elif isinstance(callee, SuperExpr):
214215
return translate_super_method_call(builder, expr, callee)
215216
else:
@@ -219,7 +220,8 @@ def transform_call_expr(builder: IRBuilder, expr: CallExpr) -> Value:
219220
def translate_call(builder: IRBuilder, expr: CallExpr, callee: Expression) -> Value:
220221
# The common case of calls is refexprs
221222
if isinstance(callee, RefExpr):
222-
return translate_refexpr_call(builder, expr, callee)
223+
return apply_function_specialization(builder, expr, callee) or \
224+
translate_refexpr_call(builder, expr, callee)
223225

224226
function = builder.accept(callee)
225227
args = [builder.accept(arg) for arg in expr.args]
@@ -229,18 +231,6 @@ def translate_call(builder: IRBuilder, expr: CallExpr, callee: Expression) -> Va
229231

230232
def translate_refexpr_call(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value:
231233
"""Translate a non-method call."""
232-
233-
# TODO: Allow special cases to have default args or named args. Currently they don't since
234-
# they check that everything in arg_kinds is ARG_POS.
235-
236-
# If there is a specializer for this function, try calling it.
237-
# We would return the first successful one.
238-
if callee.fullname and (callee.fullname, None) in specializers:
239-
for specializer in specializers[callee.fullname, None]:
240-
val = specializer(builder, expr, callee)
241-
if val is not None:
242-
return val
243-
244234
# Gen the argument values
245235
arg_values = [builder.accept(arg) for arg in expr.args]
246236

@@ -297,11 +287,9 @@ def translate_method_call(builder: IRBuilder, expr: CallExpr, callee: MemberExpr
297287

298288
# If there is a specializer for this method name/type, try calling it.
299289
# We would return the first successful one.
300-
if (callee.name, receiver_typ) in specializers:
301-
for specializer in specializers[callee.name, receiver_typ]:
302-
val = specializer(builder, expr, callee)
303-
if val is not None:
304-
return val
290+
val = apply_method_specialization(builder, expr, callee, receiver_typ)
291+
if val is not None:
292+
return val
305293

306294
obj = builder.accept(callee.expr)
307295
args = [builder.accept(arg) for arg in expr.args]

mypyc/irbuild/function.py

-10
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,6 @@ def transform_decorator(builder: IRBuilder, dec: Decorator) -> None:
117117
builder.functions.append(func_ir)
118118

119119

120-
def transform_method(builder: IRBuilder,
121-
cdef: ClassDef,
122-
non_ext: Optional[NonExtClassInfo],
123-
fdef: FuncDef) -> None:
124-
if non_ext:
125-
handle_non_ext_method(builder, non_ext, cdef, fdef)
126-
else:
127-
handle_ext_method(builder, cdef, fdef)
128-
129-
130120
def transform_lambda_expr(builder: IRBuilder, expr: LambdaExpr) -> Value:
131121
typ = get_proper_type(builder.types[expr])
132122
assert isinstance(typ, CallableType)

mypyc/irbuild/specialize.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,34 @@
5757
specializers: Dict[Tuple[str, Optional[RType]], List[Specializer]] = {}
5858

5959

60+
def _apply_specialization(builder: 'IRBuilder', expr: CallExpr, callee: RefExpr,
61+
name: Optional[str], typ: Optional[RType] = None) -> Optional[Value]:
62+
# TODO: Allow special cases to have default args or named args. Currently they don't since
63+
# they check that everything in arg_kinds is ARG_POS.
64+
65+
# If there is a specializer for this function, try calling it.
66+
# Return the first successful one.
67+
if name and (name, typ) in specializers:
68+
for specializer in specializers[name, typ]:
69+
val = specializer(builder, expr, callee)
70+
if val is not None:
71+
return val
72+
return None
73+
74+
75+
def apply_function_specialization(builder: 'IRBuilder', expr: CallExpr,
76+
callee: RefExpr) -> Optional[Value]:
77+
"""Invoke the Specializer callback for a function if one has been registered"""
78+
return _apply_specialization(builder, expr, callee, callee.fullname)
79+
80+
81+
def apply_method_specialization(builder: 'IRBuilder', expr: CallExpr, callee: MemberExpr,
82+
typ: Optional[RType] = None) -> Optional[Value]:
83+
"""Invoke the Specializer callback for a method if one has been registered"""
84+
name = callee.fullname if typ is None else callee.name
85+
return _apply_specialization(builder, expr, callee, name, typ)
86+
87+
6088
def specialize_function(
6189
name: str, typ: Optional[RType] = None) -> Callable[[Specializer], Specializer]:
6290
"""Decorator to register a function as being a specializer.
@@ -329,10 +357,12 @@ def gen_inner_stmts() -> None:
329357

330358

331359
@specialize_function('dataclasses.field')
360+
@specialize_function('attr.ib')
361+
@specialize_function('attr.attrib')
332362
@specialize_function('attr.Factory')
333363
def translate_dataclasses_field_call(
334364
builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[Value]:
335-
"""Special case for 'dataclasses.field' and 'attr.Factory'
365+
"""Special case for 'dataclasses.field', 'attr.attrib', and 'attr.Factory'
336366
function calls because the results of such calls are type-checked
337367
by mypy using the types of the arguments to their respective
338368
functions, resulting in attempted coercions by mypyc that throw a

mypyc/irbuild/util.py

+35-9
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@
44

55
from mypy.nodes import (
66
ClassDef, FuncDef, Decorator, OverloadedFuncDef, StrExpr, CallExpr, RefExpr, Expression,
7-
IntExpr, FloatExpr, Var, TupleExpr, UnaryExpr, BytesExpr,
7+
IntExpr, FloatExpr, Var, NameExpr, TupleExpr, UnaryExpr, BytesExpr,
88
ArgKind, ARG_NAMED, ARG_NAMED_OPT, ARG_POS, ARG_OPT, GDEF,
99
)
1010

1111

12+
DATACLASS_DECORATORS = {
13+
'dataclasses.dataclass',
14+
'attr.s',
15+
'attr.attrs',
16+
}
17+
18+
1219
def is_trait_decorator(d: Expression) -> bool:
1320
return isinstance(d, RefExpr) and d.fullname == 'mypy_extensions.trait'
1421

@@ -17,21 +24,40 @@ def is_trait(cdef: ClassDef) -> bool:
1724
return any(is_trait_decorator(d) for d in cdef.decorators) or cdef.info.is_protocol
1825

1926

20-
def is_dataclass_decorator(d: Expression) -> bool:
21-
return (
22-
(isinstance(d, RefExpr) and d.fullname == 'dataclasses.dataclass')
23-
or (
24-
isinstance(d, CallExpr)
27+
def dataclass_decorator_type(d: Expression) -> Optional[str]:
28+
if isinstance(d, RefExpr) and d.fullname in DATACLASS_DECORATORS:
29+
return d.fullname.split('.')[0]
30+
elif (isinstance(d, CallExpr)
2531
and isinstance(d.callee, RefExpr)
26-
and d.callee.fullname == 'dataclasses.dataclass'
27-
)
28-
)
32+
and d.callee.fullname in DATACLASS_DECORATORS):
33+
name = d.callee.fullname.split('.')[0]
34+
if name == 'attr' and 'auto_attribs' in d.arg_names:
35+
# Note: the mypy attrs plugin checks that the value of auto_attribs is
36+
# not computed at runtime, so we don't need to perform that check here
37+
auto = d.args[d.arg_names.index('auto_attribs')]
38+
if isinstance(auto, NameExpr) and auto.name == 'True':
39+
return 'attr-auto'
40+
return name
41+
else:
42+
return None
43+
44+
45+
def is_dataclass_decorator(d: Expression) -> bool:
46+
return dataclass_decorator_type(d) is not None
2947

3048

3149
def is_dataclass(cdef: ClassDef) -> bool:
3250
return any(is_dataclass_decorator(d) for d in cdef.decorators)
3351

3452

53+
def dataclass_type(cdef: ClassDef) -> Optional[str]:
54+
for d in cdef.decorators:
55+
typ = dataclass_decorator_type(d)
56+
if typ is not None:
57+
return typ
58+
return None
59+
60+
3561
def get_mypyc_attr_literal(e: Expression) -> Any:
3662
"""Convert an expression from a mypyc_attr decorator to a value.
3763

0 commit comments

Comments
 (0)