Skip to content

Commit 2475643

Browse files
authored
[mypyc] Support attributes that override properties (#14377)
Code like this is now supported by mypyc: ``` class B: @Property def x(self) -> int: return 0 class C(B): x: int = 0 # Attribute overrides a property ``` The implementation generates implicit getter/setter methods for attributes as needed and puts them in the vtable. I had to change both the irbuild "prepare" pass (where we generate declarations), irbuild main pass (where we generate implicit accessor IRs), and codegen (where implicit properties aren't visible externally to CPython). Also fix a minor mypy bug related to overriding properties and multiple inheritance that I encountered. This doesn't handle glue methods yet.
1 parent e1117c3 commit 2475643

File tree

11 files changed

+487
-70
lines changed

11 files changed

+487
-70
lines changed

mypy/checker.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -2498,8 +2498,13 @@ class C(B, A[int]): ... # this is unsafe because...
24982498
ok = is_equivalent(first_type, second_type)
24992499
if not ok:
25002500
second_node = base2[name].node
2501-
if isinstance(second_node, Decorator) and second_node.func.is_property:
2502-
ok = is_subtype(first_type, cast(CallableType, second_type).ret_type)
2501+
if (
2502+
isinstance(second_type, FunctionLike)
2503+
and second_node is not None
2504+
and is_property(second_node)
2505+
):
2506+
second_type = get_property_type(second_type)
2507+
ok = is_subtype(first_type, second_type)
25032508
else:
25042509
if first_type is None:
25052510
self.msg.cannot_determine_type_in_base(name, base1.name, ctx)

mypyc/codegen/emitclass.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -824,7 +824,10 @@ def generate_getseter_declarations(cl: ClassIR, emitter: Emitter) -> None:
824824
)
825825
)
826826

827-
for prop in cl.properties:
827+
for prop, (getter, setter) in cl.properties.items():
828+
if getter.decl.implicit:
829+
continue
830+
828831
# Generate getter declaration
829832
emitter.emit_line("static PyObject *")
830833
emitter.emit_line(
@@ -834,7 +837,7 @@ def generate_getseter_declarations(cl: ClassIR, emitter: Emitter) -> None:
834837
)
835838

836839
# Generate property setter declaration if a setter exists
837-
if cl.properties[prop][1]:
840+
if setter:
838841
emitter.emit_line("static int")
839842
emitter.emit_line(
840843
"{}({} *self, PyObject *value, void *closure);".format(
@@ -854,11 +857,13 @@ def generate_getseters_table(cl: ClassIR, name: str, emitter: Emitter) -> None:
854857
)
855858
)
856859
emitter.emit_line(" NULL, NULL},")
857-
for prop in cl.properties:
860+
for prop, (getter, setter) in cl.properties.items():
861+
if getter.decl.implicit:
862+
continue
863+
858864
emitter.emit_line(f'{{"{prop}",')
859865
emitter.emit_line(f" (getter){getter_name(cl, prop, emitter.names)},")
860866

861-
setter = cl.properties[prop][1]
862867
if setter:
863868
emitter.emit_line(f" (setter){setter_name(cl, prop, emitter.names)},")
864869
emitter.emit_line("NULL, NULL},")
@@ -878,6 +883,9 @@ def generate_getseters(cl: ClassIR, emitter: Emitter) -> None:
878883
if i < len(cl.attributes) - 1:
879884
emitter.emit_line("")
880885
for prop, (getter, setter) in cl.properties.items():
886+
if getter.decl.implicit:
887+
continue
888+
881889
rtype = getter.sig.ret_type
882890
emitter.emit_line("")
883891
generate_readonly_getter(cl, prop, rtype, getter, emitter)

mypyc/ir/class_ir.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,18 @@ def name_prefix(self, names: NameGenerator) -> str:
278278
def struct_name(self, names: NameGenerator) -> str:
279279
return f"{exported_name(self.fullname)}Object"
280280

281-
def get_method_and_class(self, name: str) -> tuple[FuncIR, ClassIR] | None:
281+
def get_method_and_class(
282+
self, name: str, *, prefer_method: bool = False
283+
) -> tuple[FuncIR, ClassIR] | None:
282284
for ir in self.mro:
283285
if name in ir.methods:
284-
return ir.methods[name], ir
286+
func_ir = ir.methods[name]
287+
if not prefer_method and func_ir.decl.implicit:
288+
# This is an implicit accessor, so there is also an attribute definition
289+
# which the caller prefers. This happens if an attribute overrides a
290+
# property.
291+
return None
292+
return func_ir, ir
285293

286294
return None
287295

mypyc/ir/func_ir.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def __init__(
139139
kind: int = FUNC_NORMAL,
140140
is_prop_setter: bool = False,
141141
is_prop_getter: bool = False,
142+
implicit: bool = False,
142143
) -> None:
143144
self.name = name
144145
self.class_name = class_name
@@ -155,7 +156,11 @@ def __init__(
155156
else:
156157
self.bound_sig = sig.bound_sig()
157158

158-
# this is optional because this will be set to the line number when the corresponding
159+
# If True, not present in the mypy AST and must be synthesized during irbuild
160+
# Currently only supported for property getters/setters
161+
self.implicit = implicit
162+
163+
# This is optional because this will be set to the line number when the corresponding
159164
# FuncIR is created
160165
self._line: int | None = None
161166

@@ -198,6 +203,7 @@ def serialize(self) -> JsonDict:
198203
"kind": self.kind,
199204
"is_prop_setter": self.is_prop_setter,
200205
"is_prop_getter": self.is_prop_getter,
206+
"implicit": self.implicit,
201207
}
202208

203209
# TODO: move this to FuncIR?
@@ -219,6 +225,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> FuncDecl:
219225
data["kind"],
220226
data["is_prop_setter"],
221227
data["is_prop_getter"],
228+
data["implicit"],
222229
)
223230

224231

mypyc/irbuild/classdef.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
is_class_var,
2626
)
2727
from mypy.types import ENUM_REMOVED_PROPS, Instance, UnboundType, get_proper_type
28+
from mypyc.common import PROPSET_PREFIX
2829
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
2930
from mypyc.ir.func_ir import FuncDecl, FuncSignature
3031
from mypyc.ir.ops import (
@@ -53,7 +54,13 @@
5354
object_rprimitive,
5455
)
5556
from mypyc.irbuild.builder import IRBuilder
56-
from mypyc.irbuild.function import handle_ext_method, handle_non_ext_method, load_type
57+
from mypyc.irbuild.function import (
58+
gen_property_getter_ir,
59+
gen_property_setter_ir,
60+
handle_ext_method,
61+
handle_non_ext_method,
62+
load_type,
63+
)
5764
from mypyc.irbuild.util import dataclass_type, get_func_def, is_constant, is_dataclass_decorator
5865
from mypyc.primitives.dict_ops import dict_new_op, dict_set_item_op
5966
from mypyc.primitives.generic_ops import py_hasattr_op, py_setattr_op
@@ -151,6 +158,24 @@ def transform_class_def(builder: IRBuilder, cdef: ClassDef) -> None:
151158
else:
152159
builder.error("Unsupported statement in class body", stmt.line)
153160

161+
# Generate implicit property setters/getters
162+
for name, decl in ir.method_decls.items():
163+
if decl.implicit and decl.is_prop_getter:
164+
getter_ir = gen_property_getter_ir(builder, decl, cdef)
165+
builder.functions.append(getter_ir)
166+
ir.methods[getter_ir.decl.name] = getter_ir
167+
168+
setter_ir = None
169+
setter_name = PROPSET_PREFIX + name
170+
if setter_name in ir.method_decls:
171+
setter_ir = gen_property_setter_ir(builder, ir.method_decls[setter_name], cdef)
172+
builder.functions.append(setter_ir)
173+
ir.methods[setter_name] = setter_ir
174+
175+
ir.properties[name] = (getter_ir, setter_ir)
176+
# TODO: Generate glue method if needed?
177+
# TODO: Do we need interpreted glue methods? Maybe not?
178+
154179
cls_builder.finalize(ir)
155180

156181

mypyc/irbuild/function.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
Var,
2929
)
3030
from mypy.types import CallableType, get_proper_type
31-
from mypyc.common import LAMBDA_NAME, SELF_NAME
31+
from mypyc.common import LAMBDA_NAME, PROPSET_PREFIX, SELF_NAME
3232
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
3333
from mypyc.ir.func_ir import (
3434
FUNC_CLASSMETHOD,
@@ -1026,3 +1026,34 @@ def get_native_impl_ids(builder: IRBuilder, singledispatch_func: FuncDef) -> dic
10261026
"""
10271027
impls = builder.singledispatch_impls[singledispatch_func]
10281028
return {impl: i for i, (typ, impl) in enumerate(impls) if not is_decorated(builder, impl)}
1029+
1030+
1031+
def gen_property_getter_ir(builder: IRBuilder, func_decl: FuncDecl, cdef: ClassDef) -> FuncIR:
1032+
"""Generate an implicit trivial property getter for an attribute.
1033+
1034+
These are used if an attribute can also be accessed as a property.
1035+
"""
1036+
name = func_decl.name
1037+
builder.enter(name)
1038+
self_reg = builder.add_argument("self", func_decl.sig.args[0].type)
1039+
value = builder.builder.get_attr(self_reg, name, func_decl.sig.ret_type, -1)
1040+
builder.add(Return(value))
1041+
args, _, blocks, ret_type, fn_info = builder.leave()
1042+
return FuncIR(func_decl, args, blocks)
1043+
1044+
1045+
def gen_property_setter_ir(builder: IRBuilder, func_decl: FuncDecl, cdef: ClassDef) -> FuncIR:
1046+
"""Generate an implicit trivial property setter for an attribute.
1047+
1048+
These are used if an attribute can also be accessed as a property.
1049+
"""
1050+
name = func_decl.name
1051+
builder.enter(name)
1052+
self_reg = builder.add_argument("self", func_decl.sig.args[0].type)
1053+
value_reg = builder.add_argument("value", func_decl.sig.args[1].type)
1054+
assert name.startswith(PROPSET_PREFIX)
1055+
attr_name = name[len(PROPSET_PREFIX) :]
1056+
builder.add(SetAttr(self_reg, attr_name, value_reg, -1))
1057+
builder.add(Return(builder.none()))
1058+
args, _, blocks, ret_type, fn_info = builder.leave()
1059+
return FuncIR(func_decl, args, blocks)

0 commit comments

Comments
 (0)