Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mypyc] Support attributes that override properties #14377

Merged
merged 25 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
554e393
Add failing test cases
JukkaL Dec 18, 2022
96b2767
Fix accessing attribute that overrides a property
JukkaL Dec 18, 2022
f8f65cd
WIP partially enable test case
JukkaL Dec 18, 2022
c9f9ae8
Refactor mypyc.irbuild.prepare a little
JukkaL Dec 18, 2022
147ccde
More refactoring
JukkaL Dec 18, 2022
e507645
Prefer attributes over methods, if both are defined
JukkaL Dec 18, 2022
0243a1e
Generate method decl for implicit property getters
JukkaL Dec 18, 2022
15b7b30
Fix serialization of the "implicit" attribute
JukkaL Dec 18, 2022
8e638b0
Support simple use cases where an attribute overrides a property
JukkaL Dec 18, 2022
44075a9
Fix case where both attribute and property are inherited
JukkaL Dec 18, 2022
c756c36
Support settable properties
JukkaL Dec 30, 2022
de7e436
Black + isort
JukkaL Dec 30, 2022
d92905d
Merge test cases
JukkaL Dec 30, 2022
1fd4bc5
Merge more test cases
JukkaL Dec 30, 2022
d80842a
Fix edge case
JukkaL Dec 30, 2022
0274969
Test case fixes
JukkaL Dec 30, 2022
810c4e1
Add native int test cases
JukkaL Dec 30, 2022
b0ee424
Fix test case
JukkaL Jan 2, 2023
479ffcd
Minor refactoring
JukkaL Jan 2, 2023
039fe10
Fix self check
JukkaL Jan 2, 2023
1996b8a
Improve comments and minor cleanup
JukkaL Jan 2, 2023
f5d4560
Minor refactoring
JukkaL Jan 2, 2023
8fc012e
Make test cases compatible with CPython
JukkaL Jan 10, 2023
d948ce8
Merge branch 'master' into mypyc-property-inheritance
JukkaL Jan 10, 2023
158ee54
Merge remote-tracking branch 'origin/master' into mypyc-property-inhe…
JukkaL Jan 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2498,8 +2498,13 @@ class C(B, A[int]): ... # this is unsafe because...
ok = is_equivalent(first_type, second_type)
if not ok:
second_node = base2[name].node
if isinstance(second_node, Decorator) and second_node.func.is_property:
ok = is_subtype(first_type, cast(CallableType, second_type).ret_type)
if (
isinstance(second_type, FunctionLike)
and second_node is not None
and is_property(second_node)
):
second_type = get_property_type(second_type)
ok = is_subtype(first_type, second_type)
else:
if first_type is None:
self.msg.cannot_determine_type_in_base(name, base1.name, ctx)
Expand Down
16 changes: 12 additions & 4 deletions mypyc/codegen/emitclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,10 @@ def generate_getseter_declarations(cl: ClassIR, emitter: Emitter) -> None:
)
)

for prop in cl.properties:
for prop, (getter, setter) in cl.properties.items():
if getter.decl.implicit:
continue

# Generate getter declaration
emitter.emit_line("static PyObject *")
emitter.emit_line(
Expand All @@ -834,7 +837,7 @@ def generate_getseter_declarations(cl: ClassIR, emitter: Emitter) -> None:
)

# Generate property setter declaration if a setter exists
if cl.properties[prop][1]:
if setter:
emitter.emit_line("static int")
emitter.emit_line(
"{}({} *self, PyObject *value, void *closure);".format(
Expand All @@ -854,11 +857,13 @@ def generate_getseters_table(cl: ClassIR, name: str, emitter: Emitter) -> None:
)
)
emitter.emit_line(" NULL, NULL},")
for prop in cl.properties:
for prop, (getter, setter) in cl.properties.items():
if getter.decl.implicit:
continue

emitter.emit_line(f'{{"{prop}",')
emitter.emit_line(f" (getter){getter_name(cl, prop, emitter.names)},")

setter = cl.properties[prop][1]
if setter:
emitter.emit_line(f" (setter){setter_name(cl, prop, emitter.names)},")
emitter.emit_line("NULL, NULL},")
Expand All @@ -878,6 +883,9 @@ def generate_getseters(cl: ClassIR, emitter: Emitter) -> None:
if i < len(cl.attributes) - 1:
emitter.emit_line("")
for prop, (getter, setter) in cl.properties.items():
if getter.decl.implicit:
continue

rtype = getter.sig.ret_type
emitter.emit_line("")
generate_readonly_getter(cl, prop, rtype, getter, emitter)
Expand Down
12 changes: 10 additions & 2 deletions mypyc/ir/class_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,18 @@ def name_prefix(self, names: NameGenerator) -> str:
def struct_name(self, names: NameGenerator) -> str:
return f"{exported_name(self.fullname)}Object"

def get_method_and_class(self, name: str) -> tuple[FuncIR, ClassIR] | None:
def get_method_and_class(
self, name: str, *, prefer_method: bool = False
) -> tuple[FuncIR, ClassIR] | None:
for ir in self.mro:
if name in ir.methods:
return ir.methods[name], ir
func_ir = ir.methods[name]
if not prefer_method and func_ir.decl.implicit:
# This is an implicit accessor, so there is also an attribute definition
# which the caller prefers. This happens if an attribute overrides a
# property.
return None
return func_ir, ir

return None

Expand Down
9 changes: 8 additions & 1 deletion mypyc/ir/func_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def __init__(
kind: int = FUNC_NORMAL,
is_prop_setter: bool = False,
is_prop_getter: bool = False,
implicit: bool = False,
) -> None:
self.name = name
self.class_name = class_name
Expand All @@ -155,7 +156,11 @@ def __init__(
else:
self.bound_sig = sig.bound_sig()

# this is optional because this will be set to the line number when the corresponding
# If True, not present in the mypy AST and must be synthesized during irbuild
# Currently only supported for property getters/setters
self.implicit = implicit

# This is optional because this will be set to the line number when the corresponding
# FuncIR is created
self._line: int | None = None

Expand Down Expand Up @@ -198,6 +203,7 @@ def serialize(self) -> JsonDict:
"kind": self.kind,
"is_prop_setter": self.is_prop_setter,
"is_prop_getter": self.is_prop_getter,
"implicit": self.implicit,
}

# TODO: move this to FuncIR?
Expand All @@ -219,6 +225,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> FuncDecl:
data["kind"],
data["is_prop_setter"],
data["is_prop_getter"],
data["implicit"],
)


Expand Down
27 changes: 26 additions & 1 deletion mypyc/irbuild/classdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
is_class_var,
)
from mypy.types import ENUM_REMOVED_PROPS, Instance, UnboundType, get_proper_type
from mypyc.common import PROPSET_PREFIX
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
from mypyc.ir.func_ir import FuncDecl, FuncSignature
from mypyc.ir.ops import (
Expand Down Expand Up @@ -53,7 +54,13 @@
object_rprimitive,
)
from mypyc.irbuild.builder import IRBuilder
from mypyc.irbuild.function import handle_ext_method, handle_non_ext_method, load_type
from mypyc.irbuild.function import (
gen_property_getter_ir,
gen_property_setter_ir,
handle_ext_method,
handle_non_ext_method,
load_type,
)
from mypyc.irbuild.util import dataclass_type, get_func_def, is_constant, is_dataclass_decorator
from mypyc.primitives.dict_ops import dict_new_op, dict_set_item_op
from mypyc.primitives.generic_ops import py_hasattr_op, py_setattr_op
Expand Down Expand Up @@ -151,6 +158,24 @@ def transform_class_def(builder: IRBuilder, cdef: ClassDef) -> None:
else:
builder.error("Unsupported statement in class body", stmt.line)

# Generate implicit property setters/getters
for name, decl in ir.method_decls.items():
if decl.implicit and decl.is_prop_getter:
getter_ir = gen_property_getter_ir(builder, decl, cdef)
builder.functions.append(getter_ir)
ir.methods[getter_ir.decl.name] = getter_ir

setter_ir = None
setter_name = PROPSET_PREFIX + name
if setter_name in ir.method_decls:
setter_ir = gen_property_setter_ir(builder, ir.method_decls[setter_name], cdef)
builder.functions.append(setter_ir)
ir.methods[setter_name] = setter_ir

ir.properties[name] = (getter_ir, setter_ir)
# TODO: Generate glue method if needed?
# TODO: Do we need interpreted glue methods? Maybe not?

cls_builder.finalize(ir)


Expand Down
33 changes: 32 additions & 1 deletion mypyc/irbuild/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
Var,
)
from mypy.types import CallableType, get_proper_type
from mypyc.common import LAMBDA_NAME, SELF_NAME
from mypyc.common import LAMBDA_NAME, PROPSET_PREFIX, SELF_NAME
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
from mypyc.ir.func_ir import (
FUNC_CLASSMETHOD,
Expand Down Expand Up @@ -1026,3 +1026,34 @@ def get_native_impl_ids(builder: IRBuilder, singledispatch_func: FuncDef) -> dic
"""
impls = builder.singledispatch_impls[singledispatch_func]
return {impl: i for i, (typ, impl) in enumerate(impls) if not is_decorated(builder, impl)}


def gen_property_getter_ir(builder: IRBuilder, func_decl: FuncDecl, cdef: ClassDef) -> FuncIR:
"""Generate an implicit trivial property getter for an attribute.

These are used if an attribute can also be accessed as a property.
"""
name = func_decl.name
builder.enter(name)
self_reg = builder.add_argument("self", func_decl.sig.args[0].type)
value = builder.builder.get_attr(self_reg, name, func_decl.sig.ret_type, -1)
builder.add(Return(value))
args, _, blocks, ret_type, fn_info = builder.leave()
return FuncIR(func_decl, args, blocks)


def gen_property_setter_ir(builder: IRBuilder, func_decl: FuncDecl, cdef: ClassDef) -> FuncIR:
"""Generate an implicit trivial property setter for an attribute.

These are used if an attribute can also be accessed as a property.
"""
name = func_decl.name
builder.enter(name)
self_reg = builder.add_argument("self", func_decl.sig.args[0].type)
value_reg = builder.add_argument("value", func_decl.sig.args[1].type)
assert name.startswith(PROPSET_PREFIX)
attr_name = name[len(PROPSET_PREFIX) :]
builder.add(SetAttr(self_reg, attr_name, value_reg, -1))
builder.add(Return(builder.none()))
args, _, blocks, ret_type, fn_info = builder.leave()
return FuncIR(func_decl, args, blocks)
Loading