Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 15 additions & 4 deletions mypyc/irbuild/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,24 +514,35 @@ 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.
builder.primitive_op(
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
class_ir.properties[name] = (func_ir, 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

Expand Down
4 changes: 3 additions & 1 deletion mypyc/irbuild/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions mypyc/test-data/irbuild-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
176 changes: 176 additions & 0 deletions mypyc/test-data/run-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading