diff --git a/mypyc/irbuild/function.py b/mypyc/irbuild/function.py index 3afca5c6aef8..03c5ba1f6f1b 100644 --- a/mypyc/irbuild/function.py +++ b/mypyc/irbuild/function.py @@ -514,6 +514,12 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None elif func_ir.decl.kind == FUNC_STATICMETHOD: stat_meth = builder.load_module_attr_by_fullname("builtins.staticmethod", fdef.line) decorated_func = builder.py_call(stat_meth, [decorated_func], fdef.line) + elif fdef.is_property: + # Mypy also removes @property, and only accepts other decorators below it. + # The decorated getter can't be called natively, so this becomes a regular + # Python property instead of a native one. + prop = builder.load_module_attr_by_fullname("builtins.property", fdef.line) + decorated_func = builder.py_call(prop, [decorated_func], fdef.line) # Set the callable object representing the decorated method as an attribute of the # extension class. @@ -521,7 +527,7 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None py_setattr_op, [typ, builder.load_str(name), decorated_func], fdef.line ) - if fdef.is_property: + elif fdef.is_property: # If there is a property setter, it will be processed after the getter, # We populate the optional setter field with none for now. assert name not in class_ir.properties @@ -529,9 +535,14 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None elif fdef in builder.prop_setters: # The respective property getter must have been processed already - assert name in class_ir.properties - getter_ir, _ = class_ir.properties[name] - class_ir.properties[name] = (getter_ir, func_ir) + if name in class_ir.properties: + getter_ir, _ = class_ir.properties[name] + class_ir.properties[name] = (getter_ir, func_ir) + else: + # A getter with other decorators isn't a native property (see above). + builder.error( + "Property setter not supported when the getter has other decorators", fdef.line + ) class_ir.methods[func_ir.decl.name] = func_ir diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py index f2e8141b3f41..f964a7b5887c 100644 --- a/mypyc/irbuild/prepare.py +++ b/mypyc/irbuild/prepare.py @@ -325,7 +325,9 @@ def prepare_method_def( decl.sig.args[1].pos_only = True ir.method_decls[PROPSET_PREFIX + node.name] = decl - if node.func.is_property: + # A getter with other decorators isn't a native property. It's set up at runtime + # like other decorated methods, and accessed using generic attribute operations. + if node.func.is_property and not node.decorators: assert node.func.type, f"Expected return type annotation for property '{node.name}'" decl.is_prop_getter = True ir.property_types[node.name] = decl.sig.ret_type diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 59ab6ced0391..942ac9ddb4dc 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -1352,6 +1352,60 @@ L0: r2 = r1.x return r2 +[case testPropertyWithOtherDecorator] +from typing import TypeVar + +T = TypeVar("T") + +def dec(f: T) -> T: + return f + +class C: + @property + @dec + def p(self) -> int: + return 1 + +def get(c: C) -> int: + return c.p +[out] +def dec(f): + f :: object +L0: + return f +def C.p(self): + self :: __main__.C +L0: + return 2 +def get(c): + c :: __main__.C + r0 :: str + r1 :: object + r2 :: int +L0: + r0 = 'p' + r1 = CPyObject_GetAttr(c, r0) + r2 = unbox(int, r1) + return r2 + +[case testPropertySetterWithDecoratedGetter] +from typing import TypeVar + +T = TypeVar("T") + +def dec(f: T) -> T: + return f + +class C: + @property + @dec + def p(self) -> int: + return 1 + + @p.setter + def p(self, value: int) -> None: # E: Property setter not supported when the getter has other decorators + pass + [case testPropertyReturningCallable] from typing import Callable diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index d8965568af3d..710416559c52 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2398,6 +2398,182 @@ def test_property_returning_callable_inherited() -> None: assert AttrOverride().f(-6) == 6 assert call_trait_f(UsesTrait(), 1) == 0 +[case testPropertyWithOtherDecorator] +from functools import cache, cached_property +from typing import Any, Callable + +from mypy_extensions import mypyc_attr, trait +from testutil import assertRaises + +def double(f: Callable[[Any], int]) -> Callable[[Any], int]: + def wrapper(self: Any) -> int: + return f(self) * 2 + + return wrapper + +def to_str(f: Callable[[Any], int]) -> Callable[[Any], str]: + def wrapper(self: Any) -> str: + return f"<{f(self)}>" + + return wrapper + +class C: + def __init__(self) -> None: + self.calls = 0 + + @property + @double + def p(self) -> int: + self.calls += 1 + return 21 + + @property + @to_str + def s(self) -> int: + return 5 + + @property + @to_str + @double + def two(self) -> int: + return 3 + + @property + @cache + def cached(self) -> int: + self.calls += 1 + return 7 + + @cached_property + @double + def cp(self) -> int: + return 4 + + def via_self(self) -> int: + return self.p + 1 + +class Base: + @property + @double + def x(self) -> int: + return 1 + +class NativeOverride(Base): + @property + def x(self) -> int: + return 5 + +class DecoratedOverride(Base): + @property + @double + def x(self) -> int: + return 10 + +class AttrOverride(Base): + x: int = 7 + +@trait +class T: + @property + @double + def t(self) -> int: + return 4 + +class UsesTrait(T): + pass + +@mypyc_attr(allow_interpreted_subclasses=True) +class Open: + @property + @double + def o(self) -> int: + return 8 + +@mypyc_attr(native_class=False) +class NonNative: + def __init__(self) -> None: + self.calls = 0 + + @property + @double + def p(self) -> int: + self.calls += 1 + return 3 + + @property + @to_str + def s(self) -> int: + return 5 + + @property + @cache + def cached(self) -> int: + self.calls += 1 + return 7 + +def get_x(b: Base) -> int: + return b.x + +def get_t(t: T) -> int: + return t.t + +def get_o(o: Open) -> int: + return o.o + +def test_property_with_other_decorator() -> None: + c = C() + assert c.p == 42 + assert c.p + 1 == 43 + assert c.calls == 2 + assert c.via_self() == 43 + assert c.s == "<5>" + assert c.two == "<6>" + a: Any = c + assert a.p == 42 + assert a.s == "<5>" + assert isinstance(getattr(C, "p"), property) + with assertRaises(AttributeError): + a.p = 1 + +def test_property_with_caching_decorator() -> None: + c = C() + assert c.cached == 7 + assert c.cached == 7 + assert c.calls == 1 + assert C().cached == 7 + assert c.cp == 8 + +def test_property_with_other_decorator_inherited() -> None: + assert get_x(Base()) == 2 + assert get_x(NativeOverride()) == 5 + assert NativeOverride().x == 5 + assert get_x(DecoratedOverride()) == 20 + assert DecoratedOverride().x == 20 + assert get_x(AttrOverride()) == 7 + assert AttrOverride().x == 7 + assert get_t(UsesTrait()) == 8 + assert UsesTrait().t == 8 + +def test_property_with_other_decorator_interpreted_subclass() -> None: + assert get_o(Open()) == 16 + make_type: Any = type + sub = make_type("Sub", (Open,), {"o": 3}) + assert get_o(sub()) == 3 + +def test_property_with_other_decorator_non_native() -> None: + n = NonNative() + assert n.p == 6 + assert n.calls == 1 + assert n.s == "<5>" + assert n.cached == 7 + assert n.cached == 7 + assert n.calls == 2 + a: Any = n + assert a.p == 6 + assert isinstance(getattr(NonNative, "p"), property) + with assertRaises(AttributeError): + a.p = 1 + [case testPropertySetters] from mypy_extensions import trait