Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
7 changes: 7 additions & 0 deletions mypyc/codegen/emitclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ def native_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
return f"{NATIVE_PREFIX}{fn.cname(emitter.names)}"


def dunder_attr_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
wrapper_fn = cl.get_method(fn.name + "__wrapper")
assert wrapper_fn
return f"{NATIVE_PREFIX}{wrapper_fn.cname(emitter.names)}"


# We maintain a table from dunder function names to struct slots they
# correspond to and functions that generate a wrapper (if necessary)
# and return the function name to stick in the slot.
Expand All @@ -55,6 +61,7 @@ def native_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
"__iter__": ("tp_iter", native_slot),
"__hash__": ("tp_hash", generate_hash_wrapper),
"__get__": ("tp_descr_get", generate_get_wrapper),
"__getattr__": ("tp_getattro", dunder_attr_slot),
}

AS_MAPPING_SLOT_DEFS: SlotTable = {
Expand Down
3 changes: 2 additions & 1 deletion mypyc/irbuild/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,7 @@ def enter_method(
ret_type: RType,
fn_info: FuncInfo | str = "",
self_type: RType | None = None,
internal: bool = False,
) -> Iterator[None]:
"""Generate IR for a method.

Expand Down Expand Up @@ -1268,7 +1269,7 @@ def enter_method(
sig = FuncSignature(args, ret_type)
name = self.function_name_stack.pop()
class_ir = self.class_ir_stack.pop()
decl = FuncDecl(name, class_ir.name, self.module_name, sig)
decl = FuncDecl(name, class_ir.name, self.module_name, sig, internal=internal)
ir = FuncIR(decl, arg_regs, blocks)
class_ir.methods[name] = ir
class_ir.method_decls[name] = ir.decl
Expand Down
56 changes: 55 additions & 1 deletion mypyc/irbuild/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
)
from mypyc.ir.ops import (
BasicBlock,
ComparisonOp,
GetAttr,
Integer,
LoadAddress,
Expand Down Expand Up @@ -81,7 +82,7 @@
dict_new_op,
exact_dict_set_item_op,
)
from mypyc.primitives.generic_ops import py_setattr_op
from mypyc.primitives.generic_ops import generic_getattr, py_setattr_op
from mypyc.primitives.misc_ops import register_function
from mypyc.primitives.registry import builtin_names
from mypyc.sametype import is_same_method_signature, is_same_type
Expand Down Expand Up @@ -364,6 +365,56 @@ def gen_func_ir(
return (func_ir, func_reg)


def generate_getattr_wrapper(builder: IRBuilder, cdef: ClassDef, getattr: FuncDef) -> None:
"""
Generate a wrapper function for __getattr__ that can be put into the tp_getattro slot.
The wrapper takes one argument besides self which is the attribute name.
It first checks if the name matches any of the attributes of this class.
If it does, it returns that attribute. If none match, it calls __getattr__.

__getattr__ is not supported in classes that allow interpreted subclasses because the
tp_getattro slot is inherited by subclasses and if the subclass overrides __getattr__,
the override would be ignored in our wrapper. TODO: To support this, the wrapper would
have to resolve "__getattr__" against the type at runtime and call the returned method,
like _Py_slot_tp_getattr_hook in cpython.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the wrapper check the type of self, and only use the slow path if the runtime type is unexpected?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that was my thinking as well. updated the comment to mention this.


__getattr__ is not supported in classes which inherit from python classes because those
have __dict__ which currently has some strange interactions when class attributes and
variables are assigned through __dict__ vs. through regular attribute access. Allowing
__getattr__ on top of that could be problematic.
"""
name = getattr.name + "__wrapper"
ir = builder.mapper.type_to_ir[cdef.info]
line = getattr.line

error_base = f'"__getattr__" not supported in class "{cdef.name}" because '
if ir.allow_interpreted_subclasses:
builder.error(error_base + "it allows interpreted subclasses", line)
if ir.inherits_python:
builder.error(error_base + "it inherits from an interpreted class", line)

with builder.enter_method(ir, name, object_rprimitive, internal=True):
attr_arg = builder.add_argument("attr", object_rprimitive)
generic_getattr_result = builder.call_c(generic_getattr, [builder.self(), attr_arg], line)

return_generic, call_getattr = BasicBlock(), BasicBlock()
null = Integer(0, object_rprimitive, line)
got_generic = builder.add(
ComparisonOp(generic_getattr_result, null, ComparisonOp.NEQ, line)
)
builder.add_bool_branch(got_generic, return_generic, call_getattr)

builder.activate_block(return_generic)
builder.add(Return(generic_getattr_result, line))

builder.activate_block(call_getattr)
# No attribute matched so call user-provided __getattr__.
getattr_result = builder.gen_method_call(
builder.self(), getattr.name, [attr_arg], object_rprimitive, line
)
builder.add(Return(getattr_result, line))


def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None:
# Perform the function of visit_method for methods inside extension classes.
name = fdef.name
Expand Down Expand Up @@ -430,6 +481,9 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None
class_ir.glue_methods[(class_ir, name)] = f
builder.functions.append(f)

if fdef.name == "__getattr__":
generate_getattr_wrapper(builder, cdef, fdef)


def handle_non_ext_method(
builder: IRBuilder, non_ext: NonExtClassInfo, cdef: ClassDef, fdef: FuncDef
Expand Down
4 changes: 4 additions & 0 deletions mypyc/lib-rt/CPy.h
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,10 @@ PyObject *CPy_GetANext(PyObject *aiter);
void CPy_SetTypeAliasTypeComputeFunction(PyObject *alias, PyObject *compute_value);
void CPyTrace_LogEvent(const char *location, const char *line, const char *op, const char *details);

static inline PyObject *CPyObject_GenericGetAttr(PyObject *self, PyObject *name) {
return _PyObject_GenericGetAttrWithDict(self, name, NULL, 1);
}

#if CPY_3_11_FEATURES
PyObject *CPy_GetName(PyObject *obj);
#endif
Expand Down
9 changes: 9 additions & 0 deletions mypyc/primitives/generic_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,12 @@
c_function_name="CPy_GetName",
error_kind=ERR_MAGIC,
)

# look-up name in tp_dict but don't raise AttributeError on failure
generic_getattr = custom_op(
arg_types=[object_rprimitive, object_rprimitive],
return_type=object_rprimitive,
c_function_name="CPyObject_GenericGetAttr",
error_kind=ERR_NEVER,
is_borrowed=True,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the function returns a strong new reference, not a borrowed reference. Can you double check this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, you're right. it calls PyDict_GetItemRef internally which returns a strong reference. i've changed the function description but without borrowing, mypyc would always insert a dec_ref that would crash if CPyObject_GenericGetAttr returned null. i've changed the refcount logic to insert xdec_ref in this case since a null returned here is not an error.

)
99 changes: 99 additions & 0 deletions mypyc/test-data/irbuild-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -2088,3 +2088,102 @@ class NonNative:
@mypyc_attr(free_list_len=1, allow_interpreted_subclasses=True) # E: "free_list_len" can't be used in a class that allows interpreted subclasses
class InterpSub:
pass

[case testUnsupportedGetAttr]
from mypy_extensions import mypyc_attr
from typing import Optional

@mypyc_attr(allow_interpreted_subclasses=True)
class AllowsInterpreted:
def __getattr__(self, attr: str) -> Optional[object]: # E: "__getattr__" not supported in class "AllowsInterpreted" because it allows interpreted subclasses
return 0

class InheritsInterpreted(dict):
def __getattr__(self, attr: str) -> Optional[object]: # E: "__getattr__" not supported in class "InheritsInterpreted" because it inherits from an interpreted class
return 0

[case testGetAttr]
from typing import Optional, Tuple

class GetAttr:
class_var = "x"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test also a "real" class variable that is annotated using ClassVar[t].

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

obtaining the value through the wrapper works (since i've also added this to the run tests) but when the variable is annotated like this, mypyc seems to always call CPyObject_GetAttr instead of accessing it directly. not a regression since it works like this without __getattr__ too but maybe it could be optimized in the future.


def __init__(self, regular_attr: int):
self.regular_attr = regular_attr

def __getattr__(self, attr: str) -> Optional[object]:
return attr

def test_getattr() -> Tuple[object, object, object]:
i = GetAttr(42)
one = i.one
two = i.regular_attr
three = i.class_var
return (one, two, three)

[typing fixtures/typing-full.pyi]
[out]
def GetAttr.__init__(self, regular_attr):
self :: __main__.GetAttr
regular_attr :: int
L0:
self.regular_attr = regular_attr
return 1
def GetAttr.__getattr__(self, attr):
self :: __main__.GetAttr
attr :: str
L0:
return attr
def GetAttr.__getattr____wrapper(__mypyc_self__, attr):
__mypyc_self__ :: __main__.GetAttr
attr, r0 :: object
r1 :: bit
r2 :: str
r3 :: union[object, None]
L0:
r0 = CPyObject_GenericGetAttr(__mypyc_self__, attr)
r1 = r0 != 0
if r1 goto L1 else goto L2 :: bool
L1:
return r0
L2:
r2 = cast(str, attr)
r3 = __mypyc_self__.__getattr__(r2)
return r3
def GetAttr.__mypyc_defaults_setup(__mypyc_self__):
__mypyc_self__ :: __main__.GetAttr
r0 :: str
L0:
r0 = 'x'
__mypyc_self__.class_var = r0
return 1
def test_getattr():
r0, i :: __main__.GetAttr
r1 :: str
r2 :: object
one :: union[object, None]
r3, two :: int
r4, three :: str
r5 :: tuple[union[object, None], int, str]
r6 :: union[object, None]
r7 :: int
r8 :: str
r9 :: object
r10 :: tuple[union[object, None], object, str]
L0:
r0 = GetAttr(84)
i = r0
r1 = 'one'
r2 = CPyObject_GetAttr(i, r1)
one = r2
r3 = i.regular_attr
two = r3
r4 = i.class_var
three = r4
r5 = (one, two, three)
r6 = r5[0]
r7 = r5[1]
r8 = r5[2]
r9 = box(int, r7)
r10 = (r6, r9, r8)
return r10
Loading
Loading