diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 1756905e6f0e..5ada85f154a1 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -299,7 +299,8 @@ def generate_class(cl: ClassIR, module: str, emitter: Emitter) -> None: fields["tp_traverse"] = f"(traverseproc){name_prefix}_traverse" fields["tp_clear"] = f"(inquiry){clear_name}" # Populate .tp_finalize and generate a finalize method only if __del__ is defined for this class. - del_method = next((e.method for e in cl.vtable_entries if e.name == "__del__"), None) + # A decorated __del__ has no vtable entry, so it's looked up in the methods. + del_method = cl.get_method("__del__") if del_method: fields["tp_finalize"] = f"(destructor){finalize_name}" if needs_getseters: diff --git a/mypyc/irbuild/vtable.py b/mypyc/irbuild/vtable.py index 4b0a7dd55942..bce9762f6248 100644 --- a/mypyc/irbuild/vtable.py +++ b/mypyc/irbuild/vtable.py @@ -4,7 +4,6 @@ import itertools -from mypyc.common import UNDECORATED_PREFIX from mypyc.ir.class_ir import ClassIR, VTableEntries, VTableMethod from mypyc.sametype import is_same_method_signature @@ -40,9 +39,11 @@ def compute_vtable(cls: ClassIR) -> None: for t in [cls] + [t for t in all_traits if t is not cls]: for fn in itertools.chain(t.methods.values()): - if fn.name.startswith(UNDECORATED_PREFIX): - # The undecorated function of a decorated override is only called by the - # decorator at runtime, never natively + if fn.name not in t.method_decls: + # Decorated methods aren't declared as native methods, since they are only + # called through the Python API, so they don't need an entry. This includes + # the undecorated function of a decorated override, which only the + # decorators call. continue # TODO: don't generate a new entry when we overload without changing the type if fn == cls.get_method(fn.name, prefer_method=True): diff --git a/mypyc/test-data/irbuild-glue-methods.test b/mypyc/test-data/irbuild-glue-methods.test index bdbdb7e2afa6..91bc9d4db491 100644 --- a/mypyc/test-data/irbuild-glue-methods.test +++ b/mypyc/test-data/irbuild-glue-methods.test @@ -509,3 +509,60 @@ L0: r0 = '' __mypyc_self__._x = r0 return 1 + +[case testNoGlueForOverrideOfDecoratedMethod_64bit] +from typing import TypeVar + +T = TypeVar("T") + +def dec(f: T) -> T: + return f + +class Base: + @dec + def f(self) -> object: + return None + +class Sub(Base): + def f(self) -> int: + return 1 + +def call_base(b: Base) -> object: + return b.f() + +def call_sub(s: Sub) -> int: + return s.f() +[out] +def dec(f): + f :: object +L0: + return f +def Base.f(self): + self :: __main__.Base + r0 :: object +L0: + r0 = box(None, 1) + return r0 +def Sub.f(self): + self :: __main__.Sub +L0: + return 2 +def call_base(b): + b :: __main__.Base + r0 :: str + r1 :: object[1] + r2 :: object_ptr + r3 :: object +L0: + r0 = 'f' + r1 = [b] + r2 = load_address r1 + r3 = PyObject_VectorcallMethod(r0, r2, 9223372036854775809, 0) + keep_alive b + return r3 +def call_sub(s): + s :: __main__.Sub + r0 :: int +L0: + r0 = s.f() + return r0 diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 2eb4dc261e99..2a5593754966 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -7835,6 +7835,329 @@ Sub = type("Sub", (Base,), {}) s = Sub() assert s.method() == "a1" +[case testNativeMethodOverridesDecoratedMethod] +from typing import Any, Callable, Iterator, TypeVar +from mypy_extensions import mypyc_attr, trait + +F = TypeVar("F", bound=Callable[..., Any]) + +def same(f: F) -> F: + return f + +def to_str(f: Callable[..., int]) -> Callable[..., str]: + def wrapper(*args: Any) -> str: + return "s" + str(f(*args)) + return wrapper + +def drop_arg(f: Callable[[Any, int], int]) -> Callable[[Any], int]: + def wrapper(self: Any) -> int: + return f(self, 10) + return wrapper + +events: list[str] = [] + +def logged(f: F) -> F: + def wrapper(self: Any) -> None: + events.append("decorated") + f(self) + return wrapper # type: ignore[return-value] + +class Base: + @same + def o(self) -> object: + return "base" + + @to_str + def s(self) -> int: + return 5 + + @drop_arg + def n(self, x: int) -> int: + return x + 1 + + @same + def gen(self) -> Iterator[int]: + yield 1 + + @same + async def co(self) -> int: + return 1 + + @classmethod + @to_str + def c(cls) -> int: + return 6 + + @staticmethod + @to_str + def st() -> int: + return 7 + +class Native(Base): + def o(self) -> int: + return 3 + + def s(self) -> str: + return "native" + + def n(self) -> int: + return 2 + + def gen(self) -> Iterator[int]: + yield 2 + + async def co(self) -> int: + return 2 + + @classmethod + def c(cls) -> str: + return "c" + + @staticmethod + def st() -> str: + return "st" + +class Leaf(Native): + def s(self) -> str: + return "leaf" + +class Inherits(Native): + pass + +class Decorated(Base): + @same + def s(self) -> str: + return "decorated" + +@trait +class Trait: + @to_str + def s(self) -> int: + return 8 + +class Impl(Trait): + def s(self) -> str: + return "impl" + +class Uses(Trait): + pass + +class Del: + @logged + def __del__(self) -> None: + events.append("Del") + +class NativeDel(Del): + def __del__(self) -> None: + events.append("NativeDel") + +@mypyc_attr(allow_interpreted_subclasses=True) +class Open: + @to_str + def s(self) -> int: + return 9 + +@mypyc_attr(allow_interpreted_subclasses=True) +class OpenNative(Open): + def s(self) -> str: + return "open" + +def o_base(b: Base) -> object: + return b.o() + +def o_native(b: Native) -> int: + return b.o() + +def s_base(b: Base) -> str: + return b.s() + +def s_native(b: Native) -> str: + return b.s() + +def n_base(b: Base) -> int: + return b.n() + +def n_native(b: Native) -> int: + return b.n() + +def gen_base(b: Base) -> list[int]: + return list(b.gen()) + +def gen_native(b: Native) -> list[int]: + return list(b.gen()) + +async def co_base(b: Base) -> int: + return await b.co() + +async def co_native(b: Native) -> int: + return await b.co() + +def s_trait(t: Trait) -> str: + return t.s() + +def s_impl(i: Impl) -> str: + return i.s() + +def s_open(o: Open) -> str: + return o.s() + +def s_open_native(o: OpenNative) -> str: + return o.s() + +def test_native_method_overrides_decorated_method() -> None: + assert o_base(Base()) == "base" + assert o_base(Native()) == 3 + assert o_native(Native()) == 3 + assert o_native(Leaf()) == 3 + + assert s_base(Base()) == "s5" + assert s_base(Native()) == "native" + assert s_base(Leaf()) == "leaf" + assert s_base(Inherits()) == "native" + assert s_base(Decorated()) == "decorated" + assert s_native(Native()) == "native" + assert s_native(Leaf()) == "leaf" + assert s_native(Inherits()) == "native" + + assert n_base(Base()) == 11 + assert n_base(Native()) == 2 + assert n_native(Native()) == 2 + + assert gen_base(Base()) == [1] + assert gen_base(Native()) == [2] + assert gen_native(Native()) == [2] + + b: Base = Native() + assert Base.c() == "s6" + assert Native.c() == "c" + assert b.c() == "c" + assert Base.st() == "s7" + assert Native.st() == "st" + assert b.st() == "st" + + assert s_trait(Impl()) == "impl" + assert s_trait(Uses()) == "s8" + assert s_impl(Impl()) == "impl" + + assert s_open(Open()) == "s9" + assert s_open(OpenNative()) == "open" + assert s_open_native(OpenNative()) == "open" + +[typing fixtures/typing-full.pyi] +[file driver.py] +import asyncio +from native import ( + Base, Native, Leaf, Del, NativeDel, Open, OpenNative, co_base, co_native, events, s_open, + s_open_native, test_native_method_overrides_decorated_method, +) + +test_native_method_overrides_decorated_method() + +assert Base().o() == "base" +assert Native().o() == 3 +assert Base().s() == "s5" +assert Native().s() == "native" +assert Leaf().s() == "leaf" +assert Base().n() == 11 +assert Native().n() == 2 + +assert asyncio.run(co_base(Base())) == 1 +assert asyncio.run(co_base(Native())) == 2 +assert asyncio.run(co_native(Native())) == 2 + +Del() +assert events == ["decorated", "Del"], events +events.clear() +NativeDel() +assert events == ["NativeDel"], events + +class Interp(OpenNative): + def s(self) -> str: + return "interp" + +class InterpInherits(OpenNative): + pass + +class InterpOpen(Open): + def s(self) -> str: + return "interp open" + +assert s_open(Interp()) == "interp" +assert s_open_native(Interp()) == "interp" +assert s_open(InterpInherits()) == "open" +assert s_open_native(InterpInherits()) == "open" +assert s_open(InterpOpen()) == "interp open" + +[case testNativePropertyOverridesDecoratedProperty] +from typing import Any, Callable +from mypy_extensions import trait + +def to_str(f: Callable[[Any], int]) -> Callable[[Any], str]: + def wrapper(self: Any) -> str: + return "s" + str(f(self)) + return wrapper + +class Base: + @property + @to_str + def p(self) -> int: + return 5 + +class Native(Base): + @property + def p(self) -> str: + return "native" + +class Leaf(Native): + @property + def p(self) -> str: + return "leaf" + +@trait +class Trait: + @property + @to_str + def p(self) -> int: + return 7 + +class Impl(Trait): + @property + def p(self) -> str: + return "impl" + +class Uses(Trait): + pass + +def p_base(b: Base) -> str: + return b.p + +def p_native(b: Native) -> str: + return b.p + +def p_trait(t: Trait) -> str: + return t.p + +def p_impl(i: Impl) -> str: + return i.p + +def test_native_property_overrides_decorated_property() -> None: + assert p_base(Base()) == "s5" + assert p_base(Native()) == "native" + assert p_base(Leaf()) == "leaf" + assert p_native(Native()) == "native" + assert p_native(Leaf()) == "leaf" + assert p_trait(Impl()) == "impl" + assert p_trait(Uses()) == "s7" + assert p_impl(Impl()) == "impl" + +[file driver.py] +from native import Base, Native, Leaf, test_native_property_overrides_decorated_property + +test_native_property_overrides_decorated_property() +assert Base().p == "s5" +assert Native().p == "native" +assert Leaf().p == "leaf" + [case testClassVarSelfReference] # ClassVar initializers that reference other ClassVars from the same class. # In CPython, the class body executes as a function where earlier assignments