diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index 8bc4568e0d47..437b0de3d4e8 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -617,6 +617,10 @@ def translate_super_method_call(builder: IRBuilder, expr: CallExpr, callee: Supe return translate_call(builder, expr, callee) decl = base.method_decl(callee.name) + if decl.is_prop_getter: + # super().prop(...) calls the property's value, so get it through super() + return translate_call(builder, expr, callee) + arg_values = [builder.accept(arg) for arg in expr.args] arg_kinds, arg_names = expr.arg_kinds.copy(), expr.arg_names.copy() diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index aafdb73fed84..16c26dec09e9 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1489,7 +1489,12 @@ def gen_method_call( and not base.type.class_ir.builtin_base ): name = name if base.type.class_ir.is_ext_class else fast_name - if base.type.class_ir.has_method(name): + # Property getters are also stored as methods, but obj.prop(...) calls the + # property's value, so handle properties like attributes below. + if ( + base.type.class_ir.has_method(name) + and not base.type.class_ir.method_decl(name).is_prop_getter + ): decl = base.type.class_ir.method_decl(name) if arg_kinds is None: assert arg_names is None, "arg_kinds not present but arg_names is" diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index a08bfb61646e..e18cb28ba28d 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -1352,6 +1352,42 @@ L0: r2 = r1.x return r2 +[case testPropertyReturningCallable] +from typing import Callable + +class C: + _f: Callable[[int], int] + + @property + def f(self) -> Callable[[int], int]: + return self._f + +def g(c: C) -> int: + return c.f(1) +[out] +def C.f(self): + self :: __main__.C + r0 :: object +L0: + r0 = self._f + return r0 +def g(c): + c :: __main__.C + r0, r1 :: object + r2 :: object[1] + r3 :: object_ptr + r4 :: object + r5 :: int +L0: + r0 = c.f + r1 = object 1 + r2 = [r1] + r3 = load_address r2 + r4 = PyObject_Vectorcall(r0, r3, 1, 0) + keep_alive r1 + r5 = unbox(int, r4) + return r5 + [case testBorrowResultOfCustomGetItemInIfStatement_withgil] from typing import List diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 77f029b6b80b..58263855bbaf 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2129,6 +2129,89 @@ Represents a sequence of values. Updates itself by next, which is a new value. 3 3 +[case testPropertyReturningCallable] +from typing import Any, Callable +from mypy_extensions import trait + +class Config: + def __init__(self) -> None: + self._sorting_function: Callable[..., list[str]] | None = None + + @property + def sorting_function(self) -> Callable[..., list[str]]: + if self._sorting_function is not None: + return self._sorting_function + self._sorting_function = sorted + return self._sorting_function + + def sort(self, items: list[str]) -> list[str]: + return self.sorting_function(items) + +class Item: + def __init__(self, n: int) -> None: + self.n = n + +class C: + @property + def no_args(self) -> Callable[[], str]: + return lambda: "called" + + @property + def any_value(self) -> Any: + return lambda x: x * 2 + + @property + def item_type(self) -> type[Item]: + return Item + +class Base: + @property + def f(self) -> Callable[[int], int]: + return lambda x: x + 1 + +class Derived(Base): + def call_super(self, x: int) -> int: + return super().f(x) + +class AttrOverride(Base): + # A builtin, so that it isn't bound as a method when interpreted + f: Callable[[int], int] = abs + +@trait +class T: + @property + def f(self) -> Callable[[int], int]: + return lambda x: x - 1 + +class UsesTrait(T): + pass + +def call_f(b: Base, x: int) -> int: + return b.f(x) + +def call_trait_f(t: T, x: int) -> int: + return t.f(x) + +def test_property_returning_callable() -> None: + config = Config() + assert config.sorting_function(["b2", "b10", "b1"]) == ["b1", "b10", "b2"] + assert config.sorting_function(["a", "b"], reverse=True) == ["b", "a"] + assert config.sort(["b", "a"]) == ["a", "b"] + +def test_property_returning_other_callables() -> None: + c = C() + assert c.no_args() == "called" + assert c.any_value(21) == 42 + assert c.item_type(5).n == 5 + +def test_property_returning_callable_inherited() -> None: + assert call_f(Base(), 1) == 2 + assert call_f(Derived(), 1) == 2 + assert Derived().call_super(2) == 3 + assert call_f(AttrOverride(), -5) == 5 + assert AttrOverride().f(-6) == 6 + assert call_trait_f(UsesTrait(), 1) == 0 + [case testPropertySetters] from mypy_extensions import trait