Skip to content

Commit 4344e50

Browse files
committed
Generate __getattr__ wrapper
1 parent 354bea6 commit 4344e50

File tree

7 files changed

+608
-2
lines changed

7 files changed

+608
-2
lines changed

mypyc/codegen/emitclass.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ def native_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
3939
return f"{NATIVE_PREFIX}{fn.cname(emitter.names)}"
4040

4141

42+
def dunder_attr_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
43+
wrapper_fn = cl.get_method(fn.name + "__wrapper")
44+
assert wrapper_fn
45+
return f"{NATIVE_PREFIX}{wrapper_fn.cname(emitter.names)}"
46+
47+
4248
# We maintain a table from dunder function names to struct slots they
4349
# correspond to and functions that generate a wrapper (if necessary)
4450
# and return the function name to stick in the slot.
@@ -55,6 +61,7 @@ def native_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
5561
"__iter__": ("tp_iter", native_slot),
5662
"__hash__": ("tp_hash", generate_hash_wrapper),
5763
"__get__": ("tp_descr_get", generate_get_wrapper),
64+
"__getattr__": ("tp_getattro", dunder_attr_slot),
5865
}
5966

6067
AS_MAPPING_SLOT_DEFS: SlotTable = {

mypyc/irbuild/builder.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,7 @@ def enter_method(
12411241
ret_type: RType,
12421242
fn_info: FuncInfo | str = "",
12431243
self_type: RType | None = None,
1244+
internal: bool = False,
12441245
) -> Iterator[None]:
12451246
"""Generate IR for a method.
12461247
@@ -1268,7 +1269,7 @@ def enter_method(
12681269
sig = FuncSignature(args, ret_type)
12691270
name = self.function_name_stack.pop()
12701271
class_ir = self.class_ir_stack.pop()
1271-
decl = FuncDecl(name, class_ir.name, self.module_name, sig)
1272+
decl = FuncDecl(name, class_ir.name, self.module_name, sig, internal=internal)
12721273
ir = FuncIR(decl, arg_regs, blocks)
12731274
class_ir.methods[name] = ir
12741275
class_ir.method_decls[name] = ir.decl

mypyc/irbuild/function.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
)
4343
from mypyc.ir.ops import (
4444
BasicBlock,
45+
ComparisonOp,
4546
GetAttr,
4647
Integer,
4748
LoadAddress,
@@ -81,7 +82,7 @@
8182
dict_new_op,
8283
exact_dict_set_item_op,
8384
)
84-
from mypyc.primitives.generic_ops import py_setattr_op
85+
from mypyc.primitives.generic_ops import generic_getattr, py_setattr_op
8586
from mypyc.primitives.misc_ops import register_function
8687
from mypyc.primitives.registry import builtin_names
8788
from mypyc.sametype import is_same_method_signature, is_same_type
@@ -364,6 +365,56 @@ def gen_func_ir(
364365
return (func_ir, func_reg)
365366

366367

368+
def generate_getattr_wrapper(builder: IRBuilder, cdef: ClassDef, getattr: FuncDef) -> None:
369+
"""
370+
Generate a wrapper function for __getattr__ that can be put into the tp_getattro slot.
371+
The wrapper takes one argument besides self which is the attribute name.
372+
It first checks if the name matches any of the attributes of this class.
373+
If it does, it returns that attribute. If none match, it calls __getattr__.
374+
375+
__getattr__ is not supported in classes that allow interpreted subclasses because the
376+
tp_getattro slot is inherited by subclasses and if the subclass overrides __getattr__,
377+
the override would be ignored in our wrapper. TODO: To support this, the wrapper would
378+
have to resolve "__getattr__" against the type at runtime and call the returned method,
379+
like _Py_slot_tp_getattr_hook in cpython.
380+
381+
__getattr__ is not supported in classes which inherit from python classes because those
382+
have __dict__ which currently has some strange interactions when class attributes and
383+
variables are assigned through __dict__ vs. through regular attribute access. Allowing
384+
__getattr__ on top of that could be problematic.
385+
"""
386+
name = getattr.name + "__wrapper"
387+
ir = builder.mapper.type_to_ir[cdef.info]
388+
line = getattr.line
389+
390+
error_base = f'"__getattr__" not supported in class "{cdef.name}" because '
391+
if ir.allow_interpreted_subclasses:
392+
builder.error(error_base + "it allows interpreted subclasses", line)
393+
if ir.inherits_python:
394+
builder.error(error_base + "it inherits from an interpreted class", line)
395+
396+
with builder.enter_method(ir, name, object_rprimitive, internal=True):
397+
attr_arg = builder.add_argument("attr", object_rprimitive)
398+
generic_getattr_result = builder.call_c(generic_getattr, [builder.self(), attr_arg], line)
399+
400+
return_generic, call_getattr = BasicBlock(), BasicBlock()
401+
null = Integer(0, object_rprimitive, line)
402+
got_generic = builder.add(
403+
ComparisonOp(generic_getattr_result, null, ComparisonOp.NEQ, line)
404+
)
405+
builder.add_bool_branch(got_generic, return_generic, call_getattr)
406+
407+
builder.activate_block(return_generic)
408+
builder.add(Return(generic_getattr_result, line))
409+
410+
builder.activate_block(call_getattr)
411+
# No attribute matched so call user-provided __getattr__.
412+
getattr_result = builder.gen_method_call(
413+
builder.self(), getattr.name, [attr_arg], object_rprimitive, line
414+
)
415+
builder.add(Return(getattr_result, line))
416+
417+
367418
def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None:
368419
# Perform the function of visit_method for methods inside extension classes.
369420
name = fdef.name
@@ -430,6 +481,9 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None
430481
class_ir.glue_methods[(class_ir, name)] = f
431482
builder.functions.append(f)
432483

484+
if fdef.name == "__getattr__":
485+
generate_getattr_wrapper(builder, cdef, fdef)
486+
433487

434488
def handle_non_ext_method(
435489
builder: IRBuilder, non_ext: NonExtClassInfo, cdef: ClassDef, fdef: FuncDef

mypyc/lib-rt/CPy.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,10 @@ PyObject *CPy_GetANext(PyObject *aiter);
949949
void CPy_SetTypeAliasTypeComputeFunction(PyObject *alias, PyObject *compute_value);
950950
void CPyTrace_LogEvent(const char *location, const char *line, const char *op, const char *details);
951951

952+
static inline PyObject *CPyObject_GenericGetAttr(PyObject *self, PyObject *name) {
953+
return _PyObject_GenericGetAttrWithDict(self, name, NULL, 1);
954+
}
955+
952956
#if CPY_3_11_FEATURES
953957
PyObject *CPy_GetName(PyObject *obj);
954958
#endif

mypyc/primitives/generic_ops.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,12 @@
401401
c_function_name="CPy_GetName",
402402
error_kind=ERR_MAGIC,
403403
)
404+
405+
# look-up name in tp_dict but don't raise AttributeError on failure
406+
generic_getattr = custom_op(
407+
arg_types=[object_rprimitive, object_rprimitive],
408+
return_type=object_rprimitive,
409+
c_function_name="CPyObject_GenericGetAttr",
410+
error_kind=ERR_NEVER,
411+
is_borrowed=True,
412+
)

mypyc/test-data/irbuild-classes.test

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2088,3 +2088,102 @@ class NonNative:
20882088
@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
20892089
class InterpSub:
20902090
pass
2091+
2092+
[case testUnsupportedGetAttr]
2093+
from mypy_extensions import mypyc_attr
2094+
from typing import Optional
2095+
2096+
@mypyc_attr(allow_interpreted_subclasses=True)
2097+
class AllowsInterpreted:
2098+
def __getattr__(self, attr: str) -> Optional[object]: # E: "__getattr__" not supported in class "AllowsInterpreted" because it allows interpreted subclasses
2099+
return 0
2100+
2101+
class InheritsInterpreted(dict):
2102+
def __getattr__(self, attr: str) -> Optional[object]: # E: "__getattr__" not supported in class "InheritsInterpreted" because it inherits from an interpreted class
2103+
return 0
2104+
2105+
[case testGetAttr]
2106+
from typing import Optional, Tuple
2107+
2108+
class GetAttr:
2109+
class_var = "x"
2110+
2111+
def __init__(self, regular_attr: int):
2112+
self.regular_attr = regular_attr
2113+
2114+
def __getattr__(self, attr: str) -> Optional[object]:
2115+
return attr
2116+
2117+
def test_getattr() -> Tuple[object, object, object]:
2118+
i = GetAttr(42)
2119+
one = i.one
2120+
two = i.regular_attr
2121+
three = i.class_var
2122+
return (one, two, three)
2123+
2124+
[typing fixtures/typing-full.pyi]
2125+
[out]
2126+
def GetAttr.__init__(self, regular_attr):
2127+
self :: __main__.GetAttr
2128+
regular_attr :: int
2129+
L0:
2130+
self.regular_attr = regular_attr
2131+
return 1
2132+
def GetAttr.__getattr__(self, attr):
2133+
self :: __main__.GetAttr
2134+
attr :: str
2135+
L0:
2136+
return attr
2137+
def GetAttr.__getattr____wrapper(__mypyc_self__, attr):
2138+
__mypyc_self__ :: __main__.GetAttr
2139+
attr, r0 :: object
2140+
r1 :: bit
2141+
r2 :: str
2142+
r3 :: union[object, None]
2143+
L0:
2144+
r0 = CPyObject_GenericGetAttr(__mypyc_self__, attr)
2145+
r1 = r0 != 0
2146+
if r1 goto L1 else goto L2 :: bool
2147+
L1:
2148+
return r0
2149+
L2:
2150+
r2 = cast(str, attr)
2151+
r3 = __mypyc_self__.__getattr__(r2)
2152+
return r3
2153+
def GetAttr.__mypyc_defaults_setup(__mypyc_self__):
2154+
__mypyc_self__ :: __main__.GetAttr
2155+
r0 :: str
2156+
L0:
2157+
r0 = 'x'
2158+
__mypyc_self__.class_var = r0
2159+
return 1
2160+
def test_getattr():
2161+
r0, i :: __main__.GetAttr
2162+
r1 :: str
2163+
r2 :: object
2164+
one :: union[object, None]
2165+
r3, two :: int
2166+
r4, three :: str
2167+
r5 :: tuple[union[object, None], int, str]
2168+
r6 :: union[object, None]
2169+
r7 :: int
2170+
r8 :: str
2171+
r9 :: object
2172+
r10 :: tuple[union[object, None], object, str]
2173+
L0:
2174+
r0 = GetAttr(84)
2175+
i = r0
2176+
r1 = 'one'
2177+
r2 = CPyObject_GetAttr(i, r1)
2178+
one = r2
2179+
r3 = i.regular_attr
2180+
two = r3
2181+
r4 = i.class_var
2182+
three = r4
2183+
r5 = (one, two, three)
2184+
r6 = r5[0]
2185+
r7 = r5[1]
2186+
r8 = r5[2]
2187+
r9 = box(int, r7)
2188+
r10 = (r6, r9, r8)
2189+
return r10

0 commit comments

Comments
 (0)