From 4344e501ef7239ced9972f33c497c04c7423c73d Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Mon, 15 Sep 2025 18:16:21 +0200 Subject: [PATCH 1/2] Generate __getattr__ wrapper --- mypyc/codegen/emitclass.py | 7 + mypyc/irbuild/builder.py | 3 +- mypyc/irbuild/function.py | 56 +++- mypyc/lib-rt/CPy.h | 4 + mypyc/primitives/generic_ops.py | 9 + mypyc/test-data/irbuild-classes.test | 99 ++++++ mypyc/test-data/run-classes.test | 432 +++++++++++++++++++++++++++ 7 files changed, 608 insertions(+), 2 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 94f32b3224a9..9e8f9c74bc6d 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -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. @@ -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 = { diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index 12b5bc7f8f82..f4ee4371b9bf 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -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. @@ -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 diff --git a/mypyc/irbuild/function.py b/mypyc/irbuild/function.py index f0fc424aea54..6bc77c7e01a7 100644 --- a/mypyc/irbuild/function.py +++ b/mypyc/irbuild/function.py @@ -42,6 +42,7 @@ ) from mypyc.ir.ops import ( BasicBlock, + ComparisonOp, GetAttr, Integer, LoadAddress, @@ -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 @@ -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. + + __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 @@ -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 diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 5dec7509ac7b..b9cecb9280f3 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -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 diff --git a/mypyc/primitives/generic_ops.py b/mypyc/primitives/generic_ops.py index 8a4ddc370280..428a2fab56d5 100644 --- a/mypyc/primitives/generic_ops.py +++ b/mypyc/primitives/generic_ops.py @@ -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, +) diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 92857f525cca..696eceb88682 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -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" + + 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 diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 3d0250cd24ee..1ce75ec7a2ee 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -4088,3 +4088,435 @@ def test_inheritance_2() -> None: x = None y = None d = None + +[case testDunderGetAttr] +from mypy_extensions import mypyc_attr +from typing import Dict, Optional + +class GetAttr: + class_var = "x" + + def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + self.extra_attrs = extra_attrs + self.regular_attr = regular_attr + + def __getattr__(self, attr: str) -> Optional[object]: + return self.extra_attrs.get(attr) + +class GetAttrDefault: + class_var = "x" + + def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + self.extra_attrs = extra_attrs + self.regular_attr = regular_attr + + def __getattr__(self, attr: str, default: int = 8, mult: int = 1) -> Optional[object]: + return self.extra_attrs.get(attr, default * mult) + +class GetAttrInherited(GetAttr): + subclass_var = "y" + + def __init__(self, extra_attrs: Dict[str, object], regular_attr: int, sub_attr: int): + super().__init__(extra_attrs, regular_attr) + self.sub_attr = sub_attr + +class GetAttrOverridden(GetAttr): + subclass_var = "y" + + def __init__(self, extra_attrs: Dict[str, object], regular_attr: int, sub_attr: int): + super().__init__(extra_attrs, regular_attr) + self.sub_attr = sub_attr + + def __getattr__(self, attr: str) -> Optional[object]: + return attr + +@mypyc_attr(native_class=False) +class GetAttrNonNative: + class_var = "x" + + def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + self.extra_attrs = extra_attrs + self.regular_attr = regular_attr + + def __getattr__(self, attr: str) -> Optional[object]: + return self.extra_attrs.get(attr) + +def test_getattr() -> None: + i = GetAttr({"one": 1, "two": "two", "three": 3.14}, 42) + assert i.__getattr__("one") == 1 + assert i.__getattr__("regular_attr") == None + assert i.__getattr__("class_var") == None + assert i.__getattr__("four") == None + + assert getattr(i, "two") == "two" + assert getattr(i, "regular_attr") == 42 + assert getattr(i, "class_var") == "x" + assert getattr(i, "four") == None + + assert i.three == 3.14 + assert i.regular_attr == 42 + assert i.class_var == "x" + assert i.four == None + + assert i.__class__ == GetAttr + + i.extra_attrs["regular_attr"] = (4, 4, 4) + assert i.__getattr__("regular_attr") == (4, 4, 4) + assert getattr(i, "regular_attr") == 42 + assert i.regular_attr == 42 + +def test_getattr_default() -> None: + i = GetAttrDefault({"one": 1, "two": "two", "three": 3.14}, 42) + assert i.__getattr__("one") == 1 + assert i.__getattr__("regular_attr") == 8 + assert i.__getattr__("class_var") == 8 + assert i.__getattr__("four", 4, 3) == 12 + + assert getattr(i, "two") == "two" + assert getattr(i, "regular_attr") == 42 + assert getattr(i, "class_var") == "x" + assert getattr(i, "four") == 8 + + assert i.three == 3.14 + assert i.regular_attr == 42 + assert i.class_var == "x" + assert i.four == 8 + + assert i.__class__ == GetAttrDefault + + i.extra_attrs["class_var"] = (4, 4, 4) + assert i.__getattr__("class_var") == (4, 4, 4) + assert getattr(i, "class_var") == "x" + assert i.class_var == "x" + +def test_getattr_inherited() -> None: + i = GetAttrInherited({"one": 1, "two": "two", "three": 3.14}, 42, 24) + assert i.__getattr__("one") == 1 + assert i.__getattr__("regular_attr") == None + assert i.__getattr__("sub_attr") == None + assert i.__getattr__("class_var") == None + assert i.__getattr__("subclass_var") == None + assert i.__getattr__("four") == None + + assert getattr(i, "two") == "two" + assert getattr(i, "regular_attr") == 42 + assert getattr(i, "sub_attr") == 24 + assert getattr(i, "class_var") == "x" + assert getattr(i, "subclass_var") == "y" + assert getattr(i, "four") == None + + assert i.three == 3.14 + assert i.regular_attr == 42 + assert i.sub_attr == 24 + assert i.class_var == "x" + assert i.subclass_var == "y" + assert i.four == None + + assert i.__class__ == GetAttrInherited + + i.extra_attrs["sub_attr"] = (4, 4, 4) + assert i.__getattr__("sub_attr") == (4, 4, 4) + assert getattr(i, "sub_attr") == 24 + assert i.sub_attr == 24 + + base_ref: GetAttr = i + assert getattr(base_ref, "sub_attr") == 24 + assert base_ref.sub_attr == 24 + + assert getattr(base_ref, "subclass_var") == "y" + assert base_ref.subclass_var == "y" + + assert getattr(base_ref, "new") == None + assert base_ref.new == None + + assert base_ref.__class__ == GetAttrInherited + + +def test_getattr_overridden() -> None: + i = GetAttrOverridden({"one": 1, "two": "two", "three": 3.14}, 42, 24) + assert i.__getattr__("one") == "one" + assert i.__getattr__("regular_attr") == "regular_attr" + assert i.__getattr__("sub_attr") == "sub_attr" + assert i.__getattr__("class_var") == "class_var" + assert i.__getattr__("subclass_var") == "subclass_var" + assert i.__getattr__("four") == "four" + + assert getattr(i, "two") == "two" + assert getattr(i, "regular_attr") == 42 + assert getattr(i, "sub_attr") == 24 + assert getattr(i, "class_var") == "x" + assert getattr(i, "subclass_var") == "y" + assert getattr(i, "four") == "four" + + assert i.three == "three" + assert i.regular_attr == 42 + assert i.sub_attr == 24 + assert i.class_var == "x" + assert i.subclass_var == "y" + assert i.four == "four" + + assert i.__class__ == GetAttrOverridden + + i.extra_attrs["subclass_var"] = (4, 4, 4) + assert i.__getattr__("subclass_var") == "subclass_var" + assert getattr(i, "subclass_var") == "y" + assert i.subclass_var == "y" + + base_ref: GetAttr = i + assert getattr(base_ref, "sub_attr") == 24 + assert base_ref.sub_attr == 24 + + assert getattr(base_ref, "subclass_var") == "y" + assert base_ref.subclass_var == "y" + + assert getattr(base_ref, "new") == "new" + assert base_ref.new == "new" + + assert base_ref.__class__ == GetAttrOverridden + +def test_getattr_nonnative() -> None: + i = GetAttr({"one": 1, "two": "two", "three": 3.14}, 42) + assert i.__getattr__("one") == 1 + assert i.__getattr__("regular_attr") == None + assert i.__getattr__("class_var") == None + assert i.__getattr__("four") == None + + assert getattr(i, "two") == "two" + assert getattr(i, "regular_attr") == 42 + assert getattr(i, "class_var") == "x" + assert getattr(i, "four") == None + + assert i.three == 3.14 + assert i.regular_attr == 42 + assert i.class_var == "x" + assert i.four == None + + assert i.__class__ == GetAttr + + i.extra_attrs["regular_attr"] = (4, 4, 4) + assert i.__getattr__("regular_attr") == (4, 4, 4) + assert getattr(i, "regular_attr") == 42 + assert i.regular_attr == 42 + +[typing fixtures/typing-full.pyi] + +[case testDunderGetAttrInterpreted] +from mypy_extensions import mypyc_attr +from typing import Dict, Optional + +class GetAttr: + class_var = "x" + + def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + self.extra_attrs = extra_attrs + self.regular_attr = regular_attr + + def __getattr__(self, attr: str) -> Optional[object]: + return self.extra_attrs.get(attr) + +class GetAttrDefault: + class_var = "x" + + def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + self.extra_attrs = extra_attrs + self.regular_attr = regular_attr + + def __getattr__(self, attr: str, default: int = 8, mult: int = 1) -> Optional[object]: + return self.extra_attrs.get(attr, default * mult) + +class GetAttrInherited(GetAttr): + subclass_var = "y" + + def __init__(self, extra_attrs: Dict[str, object], regular_attr: int, sub_attr: int): + super().__init__(extra_attrs, regular_attr) + self.sub_attr = sub_attr + +class GetAttrOverridden(GetAttr): + subclass_var = "y" + + def __init__(self, extra_attrs: Dict[str, object], regular_attr: int, sub_attr: int): + super().__init__(extra_attrs, regular_attr) + self.sub_attr = sub_attr + + def __getattr__(self, attr: str) -> Optional[object]: + return attr + +@mypyc_attr(native_class=False) +class GetAttrNonNative: + class_var = "x" + + def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + self.extra_attrs = extra_attrs + self.regular_attr = regular_attr + + def __getattr__(self, attr: str) -> Optional[object]: + return self.extra_attrs.get(attr) + +[file driver.py] +from native import GetAttr, GetAttrDefault, GetAttrInherited, GetAttrOverridden, GetAttrNonNative + +def test_getattr() -> None: + i = GetAttr({"one": 1, "two": "two", "three": 3.14}, 42) + assert i.__getattr__("one") == 1 + assert i.__getattr__("regular_attr") == None + assert i.__getattr__("class_var") == None + assert i.__getattr__("four") == None + + assert getattr(i, "two") == "two" + assert getattr(i, "regular_attr") == 42 + assert getattr(i, "class_var") == "x" + assert getattr(i, "four") == None + + assert i.three == 3.14 + assert i.regular_attr == 42 + assert i.class_var == "x" + assert i.four == None + + assert i.__class__ == GetAttr + + i.extra_attrs["regular_attr"] = (4, 4, 4) + assert i.__getattr__("regular_attr") == (4, 4, 4) + assert getattr(i, "regular_attr") == 42 + assert i.regular_attr == 42 + +def test_getattr_default() -> None: + i = GetAttrDefault({"one": 1, "two": "two", "three": 3.14}, 42) + assert i.__getattr__("one") == 1 + assert i.__getattr__("regular_attr") == 8 + assert i.__getattr__("class_var") == 8 + assert i.__getattr__("four", 4, 3) == 12 + + assert getattr(i, "two") == "two" + assert getattr(i, "regular_attr") == 42 + assert getattr(i, "class_var") == "x" + assert getattr(i, "four") == 8 + + assert i.three == 3.14 + assert i.regular_attr == 42 + assert i.class_var == "x" + assert i.four == 8 + + assert i.__class__ == GetAttrDefault + + i.extra_attrs["class_var"] = (4, 4, 4) + assert i.__getattr__("class_var") == (4, 4, 4) + assert getattr(i, "class_var") == "x" + assert i.class_var == "x" + +def test_getattr_inherited() -> None: + i = GetAttrInherited({"one": 1, "two": "two", "three": 3.14}, 42, 24) + assert i.__getattr__("one") == 1 + assert i.__getattr__("regular_attr") == None + assert i.__getattr__("sub_attr") == None + assert i.__getattr__("class_var") == None + assert i.__getattr__("subclass_var") == None + assert i.__getattr__("four") == None + + assert getattr(i, "two") == "two" + assert getattr(i, "regular_attr") == 42 + assert getattr(i, "sub_attr") == 24 + assert getattr(i, "class_var") == "x" + assert getattr(i, "subclass_var") == "y" + assert getattr(i, "four") == None + + assert i.three == 3.14 + assert i.regular_attr == 42 + assert i.sub_attr == 24 + assert i.class_var == "x" + assert i.subclass_var == "y" + assert i.four == None + + assert i.__class__ == GetAttrInherited + + i.extra_attrs["sub_attr"] = (4, 4, 4) + assert i.__getattr__("sub_attr") == (4, 4, 4) + assert getattr(i, "sub_attr") == 24 + assert i.sub_attr == 24 + + base_ref: GetAttr = i + assert getattr(base_ref, "sub_attr") == 24 + assert base_ref.sub_attr == 24 + + assert getattr(base_ref, "subclass_var") == "y" + assert base_ref.subclass_var == "y" + + assert getattr(base_ref, "new") == None + assert base_ref.new == None + + assert base_ref.__class__ == GetAttrInherited + + +def test_getattr_overridden() -> None: + i = GetAttrOverridden({"one": 1, "two": "two", "three": 3.14}, 42, 24) + assert i.__getattr__("one") == "one" + assert i.__getattr__("regular_attr") == "regular_attr" + assert i.__getattr__("sub_attr") == "sub_attr" + assert i.__getattr__("class_var") == "class_var" + assert i.__getattr__("subclass_var") == "subclass_var" + assert i.__getattr__("four") == "four" + + assert getattr(i, "two") == "two" + assert getattr(i, "regular_attr") == 42 + assert getattr(i, "sub_attr") == 24 + assert getattr(i, "class_var") == "x" + assert getattr(i, "subclass_var") == "y" + assert getattr(i, "four") == "four" + + assert i.three == "three" + assert i.regular_attr == 42 + assert i.sub_attr == 24 + assert i.class_var == "x" + assert i.subclass_var == "y" + assert i.four == "four" + + assert i.__class__ == GetAttrOverridden + + i.extra_attrs["subclass_var"] = (4, 4, 4) + assert i.__getattr__("subclass_var") == "subclass_var" + assert getattr(i, "subclass_var") == "y" + assert i.subclass_var == "y" + + base_ref: GetAttr = i + assert getattr(base_ref, "sub_attr") == 24 + assert base_ref.sub_attr == 24 + + assert getattr(base_ref, "subclass_var") == "y" + assert base_ref.subclass_var == "y" + + assert getattr(base_ref, "new") == "new" + assert base_ref.new == "new" + + assert base_ref.__class__ == GetAttrOverridden + +def test_getattr_nonnative() -> None: + i = GetAttr({"one": 1, "two": "two", "three": 3.14}, 42) + assert i.__getattr__("one") == 1 + assert i.__getattr__("regular_attr") == None + assert i.__getattr__("class_var") == None + assert i.__getattr__("four") == None + + assert getattr(i, "two") == "two" + assert getattr(i, "regular_attr") == 42 + assert getattr(i, "class_var") == "x" + assert getattr(i, "four") == None + + assert i.three == 3.14 + assert i.regular_attr == 42 + assert i.class_var == "x" + assert i.four == None + + assert i.__class__ == GetAttr + + i.extra_attrs["regular_attr"] = (4, 4, 4) + assert i.__getattr__("regular_attr") == (4, 4, 4) + assert getattr(i, "regular_attr") == 42 + assert i.regular_attr == 42 + +test_getattr() +test_getattr_default() +test_getattr_inherited() +test_getattr_overridden() +test_getattr_nonnative() + + +[typing fixtures/typing-full.pyi] From c07f74fc9cd211e9c2a52cbacfa17c53b661ee4a Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Thu, 25 Sep 2025 17:31:18 +0200 Subject: [PATCH 2/2] Address review comments --- mypyc/ir/ops.py | 6 ++- mypyc/irbuild/function.py | 8 +-- mypyc/irbuild/ll_builder.py | 2 + mypyc/primitives/generic_ops.py | 2 +- mypyc/primitives/registry.py | 3 ++ mypyc/test-data/irbuild-classes.test | 74 +++++++++++++++++++--------- mypyc/test-data/run-classes.test | 48 +++++++++--------- mypyc/transform/refcount.py | 5 +- 8 files changed, 93 insertions(+), 55 deletions(-) diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 4b3b5eb3c8ca..76c1e07a79d5 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -1221,6 +1221,7 @@ def __init__( var_arg_idx: int = -1, *, is_pure: bool = False, + returns_null: bool = False, ) -> None: self.error_kind = error_kind super().__init__(line) @@ -1235,7 +1236,10 @@ def __init__( # and all the arguments are immutable. Pure functions support # additional optimizations. Pure functions never fail. self.is_pure = is_pure - if is_pure: + # The function might return a null value that does not indicate + # an error. + self.returns_null = returns_null + if is_pure or returns_null: assert error_kind == ERR_NEVER def sources(self) -> list[Value]: diff --git a/mypyc/irbuild/function.py b/mypyc/irbuild/function.py index 6bc77c7e01a7..a9a098d25dde 100644 --- a/mypyc/irbuild/function.py +++ b/mypyc/irbuild/function.py @@ -375,10 +375,10 @@ def generate_getattr_wrapper(builder: IRBuilder, cdef: ClassDef, getattr: FuncDe __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. + have to check type of self and if it's not the compiled class, resolve "__getattr__" against + the type at runtime and call the returned method, like _Py_slot_tp_getattr_hook in cpython. - __getattr__ is not supported in classes which inherit from python classes because those + __getattr__ is not supported in classes which inherit from non-native 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. @@ -391,7 +391,7 @@ def generate_getattr_wrapper(builder: IRBuilder, cdef: ClassDef, getattr: FuncDe 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) + builder.error(error_base + "it inherits from a non-native class", line) with builder.enter_method(ir, name, object_rprimitive, internal=True): attr_arg = builder.add_argument("attr", object_rprimitive) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 4b85c13892c1..37f2add4abbd 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -2048,6 +2048,7 @@ def call_c( line, var_arg_idx, is_pure=desc.is_pure, + returns_null=desc.returns_null, ) ) if desc.is_borrowed: @@ -2131,6 +2132,7 @@ def primitive_op( desc.extra_int_constants, desc.priority, is_pure=desc.is_pure, + returns_null=False, ) return self.call_c(c_desc, args, line, result_type=result_type) diff --git a/mypyc/primitives/generic_ops.py b/mypyc/primitives/generic_ops.py index 428a2fab56d5..ff978b7c8c3b 100644 --- a/mypyc/primitives/generic_ops.py +++ b/mypyc/primitives/generic_ops.py @@ -408,5 +408,5 @@ return_type=object_rprimitive, c_function_name="CPyObject_GenericGetAttr", error_kind=ERR_NEVER, - is_borrowed=True, + returns_null=True, ) diff --git a/mypyc/primitives/registry.py b/mypyc/primitives/registry.py index 07546663d08e..3188bc322809 100644 --- a/mypyc/primitives/registry.py +++ b/mypyc/primitives/registry.py @@ -61,6 +61,7 @@ class CFunctionDescription(NamedTuple): extra_int_constants: list[tuple[int, RType]] priority: int is_pure: bool + returns_null: bool # A description for C load operations including LoadGlobal and LoadAddress @@ -253,6 +254,7 @@ def custom_op( is_borrowed: bool = False, *, is_pure: bool = False, + returns_null: bool = False, ) -> CFunctionDescription: """Create a one-off CallC op that can't be automatically generated from the AST. @@ -274,6 +276,7 @@ def custom_op( extra_int_constants, 0, is_pure=is_pure, + returns_null=returns_null, ) diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 696eceb88682..76e28711c5e3 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -2091,35 +2091,48 @@ class InterpSub: [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 + def __getattr__(self, attr: str) -> 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 + def __getattr__(self, attr: str) -> object: # E: "__getattr__" not supported in class "InheritsInterpreted" because it inherits from a non-native class + return 0 + +@mypyc_attr(native_class=False) +class NonNative: + pass + +class InheritsNonNative(NonNative): + def __getattr__(self, attr: str) -> object: # E: "__getattr__" not supported in class "InheritsNonNative" because it inherits from a non-native class return 0 [case testGetAttr] -from typing import Optional, Tuple +from typing import ClassVar class GetAttr: class_var = "x" + class_var_annotated: ClassVar[int] = 99 def __init__(self, regular_attr: int): self.regular_attr = regular_attr - def __getattr__(self, attr: str) -> Optional[object]: + def __getattr__(self, attr: str) -> object: return attr -def test_getattr() -> Tuple[object, object, object]: + def method(self) -> int: + return 0 + +def test_getattr() -> list[object]: i = GetAttr(42) one = i.one two = i.regular_attr three = i.class_var - return (one, two, three) + four = i.class_var_annotated + five = i.method() + return [one, two, three, four, five] [typing fixtures/typing-full.pyi] [out] @@ -2139,7 +2152,7 @@ def GetAttr.__getattr____wrapper(__mypyc_self__, attr): attr, r0 :: object r1 :: bit r2 :: str - r3 :: union[object, None] + r3 :: object L0: r0 = CPyObject_GenericGetAttr(__mypyc_self__, attr) r1 = r0 != 0 @@ -2150,6 +2163,10 @@ L2: r2 = cast(str, attr) r3 = __mypyc_self__.__getattr__(r2) return r3 +def GetAttr.method(self): + self :: __main__.GetAttr +L0: + return 0 def GetAttr.__mypyc_defaults_setup(__mypyc_self__): __mypyc_self__ :: __main__.GetAttr r0 :: str @@ -2160,16 +2177,14 @@ L0: def test_getattr(): r0, i :: __main__.GetAttr r1 :: str - r2 :: object - one :: union[object, None] + r2, one :: object 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] + r4, three, r5 :: str + r6 :: object + r7, four, r8, five :: int + r9 :: list + r10, r11, r12 :: object + r13 :: ptr L0: r0 = GetAttr(84) i = r0 @@ -2180,10 +2195,21 @@ L0: 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 + r5 = 'class_var_annotated' + r6 = CPyObject_GetAttr(i, r5) + r7 = unbox(int, r6) + four = r7 + r8 = i.method() + five = r8 + r9 = PyList_New(5) + r10 = box(int, two) + r11 = box(int, four) + r12 = box(int, five) + r13 = list_items r9 + buf_init_item r13, 0, one + buf_init_item r13, 1, r10 + buf_init_item r13, 2, three + buf_init_item r13, 3, r11 + buf_init_item r13, 4, r12 + keep_alive r9 + return r9 diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 1ce75ec7a2ee..b2f1a088585d 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -4091,54 +4091,54 @@ def test_inheritance_2() -> None: [case testDunderGetAttr] from mypy_extensions import mypyc_attr -from typing import Dict, Optional +from typing import ClassVar class GetAttr: class_var = "x" - def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + def __init__(self, extra_attrs: dict[str, object], regular_attr: int): self.extra_attrs = extra_attrs self.regular_attr = regular_attr - def __getattr__(self, attr: str) -> Optional[object]: + def __getattr__(self, attr: str) -> object: return self.extra_attrs.get(attr) class GetAttrDefault: - class_var = "x" + class_var: ClassVar[str] = "x" - def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + def __init__(self, extra_attrs: dict[str, object], regular_attr: int): self.extra_attrs = extra_attrs self.regular_attr = regular_attr - def __getattr__(self, attr: str, default: int = 8, mult: int = 1) -> Optional[object]: + def __getattr__(self, attr: str, default: int = 8, mult: int = 1) -> object: return self.extra_attrs.get(attr, default * mult) class GetAttrInherited(GetAttr): subclass_var = "y" - def __init__(self, extra_attrs: Dict[str, object], regular_attr: int, sub_attr: int): + def __init__(self, extra_attrs: dict[str, object], regular_attr: int, sub_attr: int): super().__init__(extra_attrs, regular_attr) self.sub_attr = sub_attr class GetAttrOverridden(GetAttr): - subclass_var = "y" + subclass_var: ClassVar[str] = "y" - def __init__(self, extra_attrs: Dict[str, object], regular_attr: int, sub_attr: int): + def __init__(self, extra_attrs: dict[str, object], regular_attr: int, sub_attr: int): super().__init__(extra_attrs, regular_attr) self.sub_attr = sub_attr - def __getattr__(self, attr: str) -> Optional[object]: + def __getattr__(self, attr: str) -> str: return attr @mypyc_attr(native_class=False) class GetAttrNonNative: class_var = "x" - def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + def __init__(self, extra_attrs: dict[str, object], regular_attr: int): self.extra_attrs = extra_attrs self.regular_attr = regular_attr - def __getattr__(self, attr: str) -> Optional[object]: + def __getattr__(self, attr: str) -> object: return self.extra_attrs.get(attr) def test_getattr() -> None: @@ -4302,54 +4302,54 @@ def test_getattr_nonnative() -> None: [case testDunderGetAttrInterpreted] from mypy_extensions import mypyc_attr -from typing import Dict, Optional +from typing import ClassVar class GetAttr: class_var = "x" - def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + def __init__(self, extra_attrs: dict[str, object], regular_attr: int): self.extra_attrs = extra_attrs self.regular_attr = regular_attr - def __getattr__(self, attr: str) -> Optional[object]: + def __getattr__(self, attr: str) -> object: return self.extra_attrs.get(attr) class GetAttrDefault: - class_var = "x" + class_var: ClassVar[str] = "x" - def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + def __init__(self, extra_attrs: dict[str, object], regular_attr: int): self.extra_attrs = extra_attrs self.regular_attr = regular_attr - def __getattr__(self, attr: str, default: int = 8, mult: int = 1) -> Optional[object]: + def __getattr__(self, attr: str, default: int = 8, mult: int = 1) -> object: return self.extra_attrs.get(attr, default * mult) class GetAttrInherited(GetAttr): subclass_var = "y" - def __init__(self, extra_attrs: Dict[str, object], regular_attr: int, sub_attr: int): + def __init__(self, extra_attrs: dict[str, object], regular_attr: int, sub_attr: int): super().__init__(extra_attrs, regular_attr) self.sub_attr = sub_attr class GetAttrOverridden(GetAttr): - subclass_var = "y" + subclass_var: ClassVar[str] = "y" - def __init__(self, extra_attrs: Dict[str, object], regular_attr: int, sub_attr: int): + def __init__(self, extra_attrs: dict[str, object], regular_attr: int, sub_attr: int): super().__init__(extra_attrs, regular_attr) self.sub_attr = sub_attr - def __getattr__(self, attr: str) -> Optional[object]: + def __getattr__(self, attr: str) -> str: return attr @mypyc_attr(native_class=False) class GetAttrNonNative: class_var = "x" - def __init__(self, extra_attrs: Dict[str, object], regular_attr: int): + def __init__(self, extra_attrs: dict[str, object], regular_attr: int): self.extra_attrs = extra_attrs self.regular_attr = regular_attr - def __getattr__(self, attr: str) -> Optional[object]: + def __getattr__(self, attr: str) -> object: return self.extra_attrs.get(attr) [file driver.py] diff --git a/mypyc/transform/refcount.py b/mypyc/transform/refcount.py index 60daebc415fd..beacb409edfb 100644 --- a/mypyc/transform/refcount.py +++ b/mypyc/transform/refcount.py @@ -33,6 +33,7 @@ Assign, BasicBlock, Branch, + CallC, ControlOp, DecRef, Goto, @@ -89,7 +90,9 @@ def insert_ref_count_opcodes(ir: FuncIR) -> None: def is_maybe_undefined(post_must_defined: set[Value], src: Value) -> bool: - return isinstance(src, Register) and src not in post_must_defined + return (isinstance(src, Register) and src not in post_must_defined) or ( + isinstance(src, CallC) and src.returns_null + ) def maybe_append_dec_ref(