Skip to content

Commit d79a9e6

Browse files
authored
[mypyc] Fix function wrapper descriptor for static/class methods (#21811)
Calling an `async` class method in interpreted context might fail on incorrect number of arguments because the class `T` on the lhs of the member expression `T.f(a,b,c)` is not bound to the `cls` argument. Instead the first regular argument is taken as `cls` and all other are shifted. Expand the `tp_descr_get` slot of the function wrapper type object to correctly handle class methods. Also modify the function lowering logic to take the method from the type dictionary instead of through `getattr` when calling a decorator on the method. `getattr` goes through the descriptor and binds the `cls` argument but the argument should be unbound when applying the decorator to match interpreted Python.
1 parent 82dd3af commit d79a9e6

4 files changed

Lines changed: 213 additions & 29 deletions

File tree

mypyc/codegen/emit.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@
2222
TYPE_VAR_PREFIX,
2323
)
2424
from mypyc.ir.class_ir import ClassIR, all_concrete_classes
25-
from mypyc.ir.func_ir import FUNC_STATICMETHOD, FuncDecl, FuncIR, get_text_signature
25+
from mypyc.ir.func_ir import (
26+
FUNC_CLASSMETHOD,
27+
FUNC_STATICMETHOD,
28+
FuncDecl,
29+
FuncIR,
30+
get_text_signature,
31+
)
2632
from mypyc.ir.ops import (
2733
NAMESPACE_MODULE,
2834
NAMESPACE_STATIC,
@@ -1419,13 +1425,17 @@ def emit_cpyfunction_instance(
14191425
cname = f"{PREFIX}{fn.cname(self.names)}"
14201426
wrapper_name = f"{cname}_wrapper"
14211427
cfunc = f"(PyCFunction){cname}"
1422-
func_flags = "METH_FASTCALL | METH_KEYWORDS"
1428+
func_flags = ["METH_FASTCALL", "METH_KEYWORDS"]
1429+
if fn.class_name and fn.decl.kind == FUNC_STATICMETHOD:
1430+
func_flags.append("METH_STATIC")
1431+
elif fn.class_name and fn.decl.kind == FUNC_CLASSMETHOD:
1432+
func_flags.append("METH_CLASS")
14231433
doc = f"PyDoc_STR({native_function_doc_initializer(fn)})"
14241434
has_self_arg = "true" if fn.class_name and fn.decl.kind != FUNC_STATICMETHOD else "false"
14251435

14261436
code_flags = "CO_COROUTINE"
14271437
self.emit_line(
1428-
f'PyObject* {wrapper_name} = CPyFunction_New({module}, "{filepath}", "{name}", {cfunc}, {func_flags}, {doc}, {fn.line}, {code_flags}, {has_self_arg});'
1438+
f'PyObject* {wrapper_name} = CPyFunction_New({module}, "{filepath}", "{name}", {cfunc}, {" | ".join(func_flags)}, {doc}, {fn.line}, {code_flags}, {has_self_arg});'
14291439
)
14301440
self.emit_line(f"if (unlikely(!{wrapper_name}))")
14311441
self.emit_line(error_stmt)

mypyc/irbuild/function.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,12 @@
8282
dict_new_op,
8383
exact_dict_set_item_op,
8484
)
85-
from mypyc.primitives.generic_ops import generic_getattr, generic_setattr, py_setattr_op
85+
from mypyc.primitives.generic_ops import (
86+
generic_getattr,
87+
generic_setattr,
88+
py_get_item_op,
89+
py_setattr_op,
90+
)
8691
from mypyc.primitives.misc_ops import register_function
8792
from mypyc.sametype import is_same_method_signature, is_same_type
8893

@@ -486,14 +491,30 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None
486491
if is_decorated(builder, fdef):
487492
# Obtain the function name in order to construct the name of the helper function.
488493
_, _, name = fdef.fullname.rpartition(".")
489-
# Read the PyTypeObject representing the class, get the callable object
490-
# representing the non-decorated method
494+
# Get the callable representing the non-decorated method directly from the type
495+
# dictionary. Attribute access would bind a class method before its decorators are
496+
# applied, but the decorators need to receive the unbound function.
491497
typ = builder.load_native_type_object(cdef.fullname)
492-
orig_func = builder.py_get_attr(typ, name, fdef.line)
498+
type_dict = builder.py_get_attr(typ, "__dict__", fdef.line)
499+
orig_func = builder.primitive_op(
500+
py_get_item_op, [type_dict, builder.load_str(name)], fdef.line
501+
)
493502

494503
# Decorate the non-decorated method
495504
decorated_func = load_decorated_func(builder, fdef, orig_func)
496505

506+
# @classmethod and @staticmethod aren't included in fdefs_to_decorators, since
507+
# mypy represents them using the function kind. Reapply the outer descriptor
508+
# after the other decorators, matching Python's decorator evaluation order.
509+
# TODO: Handle cases where @classmethod/@staticmethod are the inner decorator.
510+
# See mypyc#1208 for reference.
511+
if func_ir.decl.kind == FUNC_CLASSMETHOD:
512+
cls_meth = builder.load_module_attr_by_fullname("builtins.classmethod", fdef.line)
513+
decorated_func = builder.py_call(cls_meth, [decorated_func], fdef.line)
514+
elif func_ir.decl.kind == FUNC_STATICMETHOD:
515+
stat_meth = builder.load_module_attr_by_fullname("builtins.staticmethod", fdef.line)
516+
decorated_func = builder.py_call(stat_meth, [decorated_func], fdef.line)
517+
497518
# Set the callable object representing the decorated method as an attribute of the
498519
# extension class.
499520
builder.primitive_op(

mypyc/lib-rt/function_wrapper.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,19 @@ static PyGetSetDef CPyFunction_getsets[] = {
145145
{0, 0, 0, 0, 0}
146146
};
147147

148-
static PyObject* CPy_PyMethod_New(PyObject *func, PyObject *self, PyObject *typ) {
149-
(void)typ;
150-
if (!self) {
148+
static PyObject* CPyFunction_descr_get(PyObject *func, PyObject *self, PyObject *typ) {
149+
int flags = ((PyCFunctionObject *)func)->m_ml->ml_flags;
150+
if (flags & METH_CLASS) {
151+
if (typ == NULL) {
152+
if (self == NULL) {
153+
PyErr_SetString(PyExc_TypeError, "__get__(None, None) is invalid");
154+
return NULL;
155+
}
156+
typ = (PyObject *)Py_TYPE(self);
157+
}
158+
return PyMethod_New(func, typ);
159+
}
160+
if (!self || (flags & METH_STATIC)) {
151161
Py_INCREF(func);
152162
return func;
153163
}
@@ -162,7 +172,7 @@ static PyType_Slot CPyFunction_slots[] = {
162172
{Py_tp_clear, (void *)CPyFunction_clear},
163173
{Py_tp_members, (void *)CPyFunction_members},
164174
{Py_tp_getset, (void *)CPyFunction_getsets},
165-
{Py_tp_descr_get, (void *)CPy_PyMethod_New},
175+
{Py_tp_descr_get, (void *)CPyFunction_descr_get},
166176
{0, 0},
167177
};
168178

mypyc/test-data/run-async.test

Lines changed: 161 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,18 @@ def wrap(fn: F) -> F:
15541554

15551555
return cast(F, wrapper)
15561556

1557+
C = Callable[..., Any]
1558+
1559+
def get_decorator() -> C:
1560+
def decorator(endpoint: C) -> C:
1561+
@wraps(endpoint)
1562+
async def inner(self: Any, val: int, *args: Any) -> Any:
1563+
return await endpoint(self, val, *args)
1564+
1565+
return cast(C, inner)
1566+
1567+
return cast(C, decorator)
1568+
15571569
@wrap
15581570
def wrapped(val: int) -> int:
15591571
return val
@@ -1571,34 +1583,57 @@ async def wrapped2_async(val: int) -> int:
15711583
return val * 2
15721584

15731585
class T:
1574-
def returns_one(self) -> int:
1586+
def returns_one(self, val: int) -> int:
15751587
return 1
15761588

1577-
async def returns_one_async(self) -> int:
1589+
async def returns_one_async(self, val: int) -> int:
15781590
return 1
15791591

15801592
@wrap
1581-
def returns_two(self) -> int:
1593+
def returns_two(self, val: int) -> int:
15821594
return 1
15831595

15841596
@wrap
1585-
async def returns_two_async(self) -> int:
1597+
async def returns_two_async(self, val: int) -> int:
15861598
return 1
15871599

15881600
@staticmethod
1589-
def static() -> int:
1601+
def static(val: int) -> int:
1602+
return 2
1603+
1604+
@staticmethod
1605+
async def static_async(val: int) -> int:
1606+
return 2
1607+
1608+
@staticmethod
1609+
@wrap
1610+
def wrapped_static(val: int) -> int:
15901611
return 2
15911612

15921613
@staticmethod
1593-
async def static_async() -> int:
1614+
@wrap
1615+
async def wrapped_static_async(val: int) -> int:
15941616
return 2
15951617

15961618
@classmethod
1597-
def class_method(cls) -> int:
1619+
def class_method(cls, val: int) -> int:
15981620
return 3
15991621

16001622
@classmethod
1601-
async def class_method_async(cls) -> int:
1623+
async def class_method_async(cls, val: int) -> int:
1624+
assert cls is T
1625+
return 3
1626+
1627+
@classmethod
1628+
@wrap
1629+
def wrapped_class_method(cls, val: int) -> int:
1630+
assert cls is T
1631+
return 3
1632+
1633+
@classmethod
1634+
@get_decorator()
1635+
async def wrapped_class_method_async(cls, val: int) -> int:
1636+
assert cls is T
16021637
return 3
16031638

16041639
def is_coroutine(fn):
@@ -1653,38 +1688,50 @@ def test_method() -> None:
16531688
t = T()
16541689
# Call through variable to make sure the call is through vectorcall and not optimized to a native call.
16551690
f: Any = t.returns_one_async
1656-
assert asyncio.run(f()) == 1
1691+
assert asyncio.run(f(1)) == 1
16571692

16581693
assert not is_coroutine(T.returns_two)
16591694
assert is_coroutine(T.returns_two_async)
1660-
assert asyncio.run(t.returns_two_async()) == 2
1695+
assert asyncio.run(t.returns_two_async(2)) == 2
16611696

16621697
assert not is_coroutine(T.static)
16631698
assert is_coroutine(T.static_async)
1664-
assert asyncio.run(T.static_async()) == 2
1699+
assert asyncio.run(T.static_async(3)) == 2
1700+
assert not is_coroutine(T.wrapped_static)
1701+
assert is_coroutine(T.wrapped_static_async)
1702+
assert T.wrapped_static(3) == 4
1703+
assert t.wrapped_static(3) == 4
1704+
assert asyncio.run(T.wrapped_static_async(3)) == 4
1705+
assert asyncio.run(t.wrapped_static_async(3)) == 4
16651706

16661707
assert not is_coroutine(T.class_method)
16671708
assert is_coroutine(T.class_method_async)
1668-
assert asyncio.run(T.class_method_async()) == 3
1709+
assert asyncio.run(T.class_method_async(4)) == 3
1710+
assert not is_coroutine(T.wrapped_class_method)
1711+
assert is_coroutine(T.wrapped_class_method_async)
1712+
assert T.wrapped_class_method(4) == 6
1713+
assert t.wrapped_class_method(4) == 6
1714+
assert asyncio.run(T.wrapped_class_method_async(4)) == 3
1715+
assert asyncio.run(t.wrapped_class_method_async(4)) == 3
16691716

16701717
def test_nested() -> None:
1671-
def nested() -> int:
1718+
def nested(val: int) -> int:
16721719
return 1
16731720

1674-
async def nested_async() -> int:
1721+
async def nested_async(val: int) -> int:
16751722
return 1
16761723

16771724
@wrap
1678-
def nested_wrapped() -> int:
1725+
def nested_wrapped(val: int) -> int:
16791726
return 2
16801727

16811728
@wrap
1682-
async def nested_wrapped_async() -> int:
1729+
async def nested_wrapped_async(val: int) -> int:
16831730
return 2
16841731

16851732
assert not is_coroutine(nested)
16861733
assert is_coroutine(nested_async)
1687-
assert asyncio.run(nested_async()) == 1
1734+
assert asyncio.run(nested_async(1)) == 1
16881735

16891736
assert getattr(nested_async, "__name__") == "nested_async", getattr(nested_async, "__name__")
16901737
setattr(nested_async, "__name__", "some custom name")
@@ -1696,7 +1743,7 @@ def test_nested() -> None:
16961743

16971744
assert not is_coroutine(nested_wrapped)
16981745
assert is_coroutine(nested_wrapped_async)
1699-
assert asyncio.run(nested_wrapped_async()) == 4
1746+
assert asyncio.run(nested_wrapped_async(2)) == 4
17001747

17011748
def test_async_function_wrapper_code_refcount() -> None:
17021749
if is_gil_disabled():
@@ -1729,6 +1776,102 @@ def test_nested_async_function_wrapper_code_refcount() -> None:
17291776
assert before == after + 1, (before, after)
17301777
assert after == 1, after
17311778

1779+
[file driver.py]
1780+
import asyncio
1781+
import sys
1782+
import weakref
1783+
1784+
import native
1785+
1786+
def test_function() -> None:
1787+
native.identity_async.__name__ = "identity_async"
1788+
native.wrapped_async.__name__ = "wrapped_async"
1789+
1790+
assert not native.is_coroutine(native.identity)
1791+
assert native.is_coroutine(native.identity_async)
1792+
assert str(native.identity_async).startswith("<function identity_async"), str(native.identity_async)
1793+
assert asyncio.run(native.identity_async(42)) == 42
1794+
1795+
wr = weakref.ref(native.identity_async)
1796+
f = wr()
1797+
assert f
1798+
assert asyncio.run(f(43)) == 43
1799+
1800+
assert getattr(native.identity_async, "__name__") == "identity_async"
1801+
assert getattr(native.identity_async, "__code__") is not None
1802+
assert getattr(native.identity_async, "__defaults__") is None
1803+
assert getattr(native.identity_async, "__kwdefaults__") is None
1804+
assert getattr(native.identity_async, "__annotations__") is None
1805+
1806+
assert not native.is_coroutine(native.wrapped)
1807+
assert native.is_coroutine(native.wrapped_async)
1808+
assert asyncio.run(native.wrapped_async(22)) == 44
1809+
1810+
assert getattr(native.wrapped, "__name__") == "wrapped"
1811+
assert getattr(native.wrapped2, "__name__") == "wrapped2"
1812+
assert getattr(native.wrapped_async, "__name__") == "wrapped_async"
1813+
assert getattr(native.wrapped2_async, "__name__") == "wrapped2_async"
1814+
1815+
def test_method() -> None:
1816+
assert not native.is_coroutine(native.T.returns_one)
1817+
assert native.is_coroutine(native.T.returns_one_async)
1818+
assert str(native.T.returns_one_async).startswith("<function T.returns_one_async")
1819+
1820+
t = native.T()
1821+
f = t.returns_one_async
1822+
assert asyncio.run(f(1)) == 1
1823+
1824+
assert not native.is_coroutine(native.T.returns_two)
1825+
assert native.is_coroutine(native.T.returns_two_async)
1826+
assert asyncio.run(t.returns_two_async(2)) == 2
1827+
1828+
assert not native.is_coroutine(native.T.static)
1829+
assert native.is_coroutine(native.T.static_async)
1830+
assert asyncio.run(native.T.static_async(3)) == 2
1831+
assert asyncio.run(t.static_async(3)) == 2
1832+
assert not native.is_coroutine(native.T.wrapped_static)
1833+
assert native.is_coroutine(native.T.wrapped_static_async)
1834+
assert native.T.wrapped_static(3) == 4
1835+
assert t.wrapped_static(3) == 4
1836+
assert asyncio.run(native.T.wrapped_static_async(3)) == 4
1837+
assert asyncio.run(t.wrapped_static_async(3)) == 4
1838+
1839+
assert not native.is_coroutine(native.T.class_method)
1840+
assert native.is_coroutine(native.T.class_method_async)
1841+
assert asyncio.run(native.T.class_method_async(4)) == 3
1842+
assert asyncio.run(t.class_method_async(4)) == 3
1843+
assert not native.is_coroutine(native.T.wrapped_class_method)
1844+
assert native.is_coroutine(native.T.wrapped_class_method_async)
1845+
assert native.T.wrapped_class_method(4) == 6
1846+
assert t.wrapped_class_method(4) == 6
1847+
assert asyncio.run(native.T.wrapped_class_method_async(4)) == 3
1848+
assert asyncio.run(t.wrapped_class_method_async(4)) == 3
1849+
1850+
def test_nested() -> None:
1851+
def nested(val: int) -> int:
1852+
return 1
1853+
1854+
async def nested_async(val: int) -> int:
1855+
return 1
1856+
1857+
nested_wrapped = native.wrap(nested)
1858+
nested_wrapped_async = native.wrap(nested_async)
1859+
1860+
assert not native.is_coroutine(nested_wrapped)
1861+
assert native.is_coroutine(nested_wrapped_async)
1862+
assert nested_wrapped(1) == 2
1863+
assert asyncio.run(nested_wrapped_async(2)) == 2
1864+
1865+
native.test_function()
1866+
native.test_method()
1867+
native.test_nested()
1868+
native.test_async_function_wrapper_code_refcount()
1869+
native.test_nested_async_function_wrapper_code_refcount()
1870+
1871+
test_function()
1872+
test_nested()
1873+
test_method()
1874+
17321875
[file asyncio/__init__.pyi]
17331876
def run(x: object) -> object: ...
17341877

0 commit comments

Comments
 (0)