diff --git a/Cython/Utility/AsyncGen.c b/Cython/Utility/AsyncGen.c index 7e111dd6c25..cd33f3885f7 100644 --- a/Cython/Utility/AsyncGen.c +++ b/Cython/Utility/AsyncGen.c @@ -363,7 +363,6 @@ static PyMemberDef __Pyx_async_gen_memberlist[] = { //ADDED: "ag_await" {"ag_await", T_OBJECT, offsetof(__pyx_CoroutineObject, yieldfrom), READONLY, PyDoc_STR("object being awaited on, or None")}, - {"__module__", T_OBJECT, offsetof(__pyx_CoroutineObject, gi_modulename), 0, 0}, {"__weaklistoffset__", T_PYSSIZET, offsetof(__pyx_CoroutineObject, gi_weakreflist), READONLY, 0}, {0, 0, 0, 0, 0} /* Sentinel */ }; @@ -402,6 +401,8 @@ static PyType_Slot __pyx_AsyncGenType_slots[] = { {Py_tp_methods, (void *)__Pyx_async_gen_methods}, {Py_tp_members, (void *)__Pyx_async_gen_memberlist}, {Py_tp_getset, (void *)__Pyx_async_gen_getsetlist}, + {Py_tp_getattro, (void *)__pyx_Coroutine_getattro}, + {Py_tp_setattro, (void *)__pyx_Coroutine_setattro}, #if CYTHON_USE_TP_FINALIZE {Py_tp_finalize, (void *)__Pyx_Coroutine_del}, #endif diff --git a/Cython/Utility/Coroutine.c b/Cython/Utility/Coroutine.c index 883b7113389..01cd0d48563 100644 --- a/Cython/Utility/Coroutine.c +++ b/Cython/Utility/Coroutine.c @@ -464,6 +464,9 @@ static PyObject *__Pyx_Coroutine_Send(PyObject *self, PyObject *value); /*proto* static __Pyx_PySendResult __Pyx_Coroutine_Close(PyObject *self, PyObject **retval); /*proto*/ static PyObject *__Pyx_Coroutine_Throw(PyObject *gen, PyObject *args); /*proto*/ +static PyObject* __pyx_Coroutine_getattro(__pyx_CoroutineObject *self, PyObject *attr); /*proto*/ +static int __pyx_Coroutine_setattro(__pyx_CoroutineObject *self, PyObject *attr, PyObject *value); /*proto*/ + // macros for exception state swapping instead of inline functions to make use of the local thread state context #if CYTHON_USE_EXC_INFO_STACK #define __Pyx_Coroutine_SwapException(self) @@ -1328,6 +1331,30 @@ static PyObject *__Pyx_Coroutine_Throw(PyObject *self, PyObject *args) { return __Pyx__Coroutine_Throw(self, typ, val, tb, args, 1); } +// Getting and setting __module__ is particular tricky since if we make __module__ a descriptor, +// then it gets in the way of (type).__module__. Therefore intercept it in getattro/setattr +static PyObject* __pyx_Coroutine_getattro(__pyx_CoroutineObject *self, PyObject *attr) { + if (unlikely(PyUnicode_CompareWithASCIIString(attr, "__module__") == 0)) { + PyObject *module; + __Pyx_BEGIN_CRITICAL_SECTION(self); + module = __Pyx_XNewRef(self->gi_modulename); + __Pyx_END_CRITICAL_SECTION(); + return module ? module : __Pyx_NewRef(Py_None); + } + return PyObject_GenericGetAttr((PyObject*)self, attr); +} + +static int __pyx_Coroutine_setattro(__pyx_CoroutineObject *self, PyObject *attr, PyObject *value) { + if (unlikely(PyUnicode_CompareWithASCIIString(attr, "__module__") == 0)) { + __Pyx_BEGIN_CRITICAL_SECTION(self); + Py_XINCREF(value); + __Pyx_Py_XDECREF_SET(self->gi_modulename, value); + __Pyx_END_CRITICAL_SECTION(); + return 0; + } + return PyObject_GenericSetAttr((PyObject*)self, attr, value); +} + static CYTHON_INLINE int __Pyx_Coroutine_traverse_excstate(__Pyx_ExcInfoStruct *exc_state, visitproc visit, void *arg) { #if PY_VERSION_HEX >= 0x030B00a4 Py_VISIT(exc_state->exc_value); @@ -1793,7 +1820,7 @@ static PyObject *__Pyx_CoroutineAwait_no_new(PyTypeObject *type, PyObject *args, // We are applying this to all Python versions (hence the commented out version guard) // to make the behaviour explicit. // #if CYTHON_USE_TYPE_SPECS -static PyObject *__Pyx_CoroutineAwait_reduce_ex(__pyx_CoroutineAwaitObject *self, PyObject *arg) { +static PyObject *__Pyx_Coroutine_fail_reduce_ex(PyObject *self, PyObject *arg) { CYTHON_UNUSED_VAR(arg); __Pyx_TypeName self_type_name = __Pyx_PyType_GetFullyQualifiedName(Py_TYPE((PyObject*)self)); @@ -1812,8 +1839,8 @@ static PyMethodDef __pyx_CoroutineAwait_methods[] = { {"close", (PyCFunction) __Pyx_CoroutineAwait_Close_Method, METH_NOARGS, PyDoc_STR("close() -> raise GeneratorExit inside coroutine.")}, // only needed with type-specs, but included in all versions for clarity // #if CYTHON_USE_TYPE_SPECS - {"__reduce_ex__", (PyCFunction) __Pyx_CoroutineAwait_reduce_ex, METH_O, 0}, - {"__reduce__", (PyCFunction) __Pyx_CoroutineAwait_reduce_ex, METH_NOARGS, 0}, + {"__reduce_ex__", (PyCFunction) __Pyx_Coroutine_fail_reduce_ex, METH_O, 0}, + {"__reduce__", (PyCFunction) __Pyx_Coroutine_fail_reduce_ex, METH_NOARGS, 0}, // #endif {0, 0, 0, 0} }; @@ -1828,6 +1855,8 @@ static PyType_Slot __pyx_CoroutineAwaitType_slots[] = { {Py_tp_methods, (void *)__pyx_CoroutineAwait_methods}, {Py_tp_iter, (void *)__Pyx_CoroutineAwait_self}, {Py_tp_iternext, (void *)__Pyx_CoroutineAwait_Next}, + {Py_tp_getattro, (void*)__pyx_Coroutine_getattro}, + {Py_tp_setattro, (void *)__pyx_Coroutine_setattro}, #if __PYX_HAS_PY_AM_SEND == 1 {Py_am_send, (void *)__Pyx_CoroutineAwait_AmSend}, #endif @@ -1872,6 +1901,8 @@ static PyMethodDef __pyx_Coroutine_methods[] = { {"throw", (PyCFunction) __Pyx_Coroutine_Throw, METH_VARARGS, PyDoc_STR("throw(typ[,val[,tb]]) -> raise exception in coroutine,\nreturn next iterated value or raise StopIteration.")}, {"close", (PyCFunction) __Pyx_Coroutine_Close_Method, METH_NOARGS, PyDoc_STR("close() -> raise GeneratorExit inside coroutine.")}, + {"__reduce_ex__", (PyCFunction) __Pyx_Coroutine_fail_reduce_ex, METH_O, 0}, + {"__reduce__", (PyCFunction) __Pyx_Coroutine_fail_reduce_ex, METH_NOARGS, 0}, {0, 0, 0, 0} }; @@ -1879,7 +1910,6 @@ static PyMemberDef __pyx_Coroutine_memberlist[] = { {"cr_await", T_OBJECT, offsetof(__pyx_CoroutineObject, yieldfrom), READONLY, PyDoc_STR("object being awaited, or None")}, {"cr_code", T_OBJECT, offsetof(__pyx_CoroutineObject, gi_code), READONLY, NULL}, - {"__module__", T_OBJECT, offsetof(__pyx_CoroutineObject, gi_modulename), 0, 0}, {"__weaklistoffset__", T_PYSSIZET, offsetof(__pyx_CoroutineObject, gi_weakreflist), READONLY, 0}, {0, 0, 0, 0, 0} }; @@ -1903,7 +1933,8 @@ static PyType_Slot __pyx_CoroutineType_slots[] = { {Py_tp_methods, (void *)__pyx_Coroutine_methods}, {Py_tp_members, (void *)__pyx_Coroutine_memberlist}, {Py_tp_getset, (void *)__pyx_Coroutine_getsets}, - {Py_tp_getattro, (void *) PyObject_GenericGetAttr}, + {Py_tp_getattro, (void *)__pyx_Coroutine_getattro}, + {Py_tp_setattro, (void *)__pyx_Coroutine_setattro}, #if CYTHON_USE_TP_FINALIZE {Py_tp_finalize, (void *)__Pyx_Coroutine_del}, #endif @@ -1998,7 +2029,7 @@ static PyType_Slot __pyx_IterableCoroutineType_slots[] = { {Py_tp_methods, (void *)__pyx_Coroutine_methods}, {Py_tp_members, (void *)__pyx_Coroutine_memberlist}, {Py_tp_getset, (void *)__pyx_Coroutine_getsets}, - {Py_tp_getattro, (void *) PyObject_GenericGetAttr}, + {Py_tp_getattro, (void *)__pyx_Coroutine_getattro}, #if CYTHON_USE_TP_FINALIZE {Py_tp_finalize, (void *)__Pyx_Coroutine_del}, #endif @@ -2090,7 +2121,8 @@ static PyType_Slot __pyx_GeneratorType_slots[] = { {Py_tp_methods, (void *)__pyx_Generator_methods}, {Py_tp_members, (void *)__pyx_Generator_memberlist}, {Py_tp_getset, (void *)__pyx_Generator_getsets}, - {Py_tp_getattro, (void *) PyObject_GenericGetAttr}, + {Py_tp_getattro, (void *)__pyx_Coroutine_getattro}, + {Py_tp_setattro, (void *)__pyx_Coroutine_setattro}, #if CYTHON_USE_TP_FINALIZE {Py_tp_finalize, (void *)__Pyx_Coroutine_del}, #endif diff --git a/Cython/Utility/CythonFunction.c b/Cython/Utility/CythonFunction.c index a7e061d0c59..65020eb509e 100644 --- a/Cython/Utility/CythonFunction.c +++ b/Cython/Utility/CythonFunction.c @@ -638,20 +638,37 @@ static void __Pyx_CyFunction_raise_type_error(__pyx_CyFunctionObject *func, cons #endif } - -#if CYTHON_COMPILING_IN_LIMITED_API -static PyObject * -__Pyx_CyFunction_get_module(__pyx_CyFunctionObject *op, void *context) { - CYTHON_UNUSED_VAR(context); - return PyObject_GetAttrString(op->func, "__module__"); +// Getting and setting __module__ is particular tricky since if we make __module__ a descriptor, +// then it gets in the way of (type).__module__. Therefore intercept it in getattro/setattr +static PyObject* __pyx_CyFunction_getattro(__pyx_CyFunctionObject *self, PyObject *attr) { + if (unlikely(PyUnicode_CompareWithASCIIString(attr, "__module__") == 0)) { + #if CYTHON_COMPILING_IN_LIMITED_API + return PyObject_GetAttr(self->func, attr); + #else + PyObject *module; + __Pyx_BEGIN_CRITICAL_SECTION(self); + module = __Pyx_XNewRef(((PyCFunctionObject*)self)->m_module); + __Pyx_END_CRITICAL_SECTION(); + return module ? module : __Pyx_NewRef(Py_None); + #endif + } + return PyObject_GenericGetAttr((PyObject*)self, attr); } -static int -__Pyx_CyFunction_set_module(__pyx_CyFunctionObject *op, PyObject* value, void *context) { - CYTHON_UNUSED_VAR(context); - return PyObject_SetAttrString(op->func, "__module__", value); +static int __pyx_CyFunction_setattro(__pyx_CyFunctionObject *self, PyObject *attr, PyObject *value) { + if (unlikely(PyUnicode_CompareWithASCIIString(attr, "__module__") == 0)) { + #if CYTHON_COMPILING_IN_LIMITED_API + return PyObject_SetAttr(self->func, attr, value); + #else + __Pyx_BEGIN_CRITICAL_SECTION(self); + Py_XINCREF(value); + Py_XSETREF(((PyCFunctionObject*)self)->m_module, value); + __Pyx_END_CRITICAL_SECTION(); + return 0; + #endif + } + return PyObject_GenericSetAttr((PyObject*)self, attr, value); } -#endif static PyGetSetDef __pyx_CyFunction_getsets[] = { {"func_doc", (getter)__Pyx_CyFunction_get_doc, (setter)__Pyx_CyFunction_set_doc, 0, 0}, @@ -673,16 +690,10 @@ static PyGetSetDef __pyx_CyFunction_getsets[] = { {"__annotations__", (getter)__Pyx_CyFunction_get_annotations, (setter)__Pyx_CyFunction_set_annotations, 0, 0}, {"_is_coroutine", (getter)__Pyx_CyFunction_get_is_coroutine, 0, 0, 0}, // {"__signature__", (getter)__Pyx_CyFunction_get_signature, 0, 0, 0}, -#if CYTHON_COMPILING_IN_LIMITED_API - {"__module__", (getter)__Pyx_CyFunction_get_module, (setter)__Pyx_CyFunction_set_module, 0, 0}, -#endif {0, 0, 0, 0, 0} }; static PyMemberDef __pyx_CyFunction_members[] = { -#if !CYTHON_COMPILING_IN_LIMITED_API - {"__module__", T_OBJECT, offsetof(PyCFunctionObject, m_module), 0, 0}, -#endif {"__dictoffset__", T_PYSSIZET, offsetof(__pyx_CyFunctionObject, func_dict), READONLY, 0}, #if CYTHON_METH_FASTCALL #if CYTHON_BACKPORT_VECTORCALL || CYTHON_COMPILING_IN_LIMITED_API @@ -1227,6 +1238,8 @@ static PyType_Slot __pyx_CyFunctionType_slots[] = { {Py_tp_methods, (void *)__pyx_CyFunction_methods}, {Py_tp_members, (void *)__pyx_CyFunction_members}, {Py_tp_getset, (void *)__pyx_CyFunction_getsets}, + {Py_tp_getattro, (void *)__pyx_CyFunction_getattro}, + {Py_tp_setattro, (void *)__pyx_CyFunction_setattro}, {Py_tp_descr_get, (void *)__Pyx_PyMethod_New}, {0, 0}, }; @@ -1441,7 +1454,7 @@ __pyx_FusedFunction_descr_get_locked(__pyx_FusedFunctionObject *func, PyObject * PyObject *module; __pyx_FusedFunctionObject *meth; #if CYTHON_COMPILING_IN_LIMITED_API - module = __Pyx_CyFunction_get_module((__pyx_CyFunctionObject *) func, NULL); + module = PyObject_GetAttrString(((__pyx_CyFunctionObject*)func)->func, "__module__"); if ((unlikely(!module))) return NULL; #else module = ((PyCFunctionObject *) func)->m_module; diff --git a/tests/limited_api_bugs_38.txt b/tests/limited_api_bugs_38.txt index 39d85cab885..730384cc368 100644 --- a/tests/limited_api_bugs_38.txt +++ b/tests/limited_api_bugs_38.txt @@ -1,6 +1,7 @@ # __dict__ doesn't work on cdef classes run[.]cdef_multiple_inheritance$ test_grammar +dynamic_attributes # To do with missing attributes when initializing types weakfail diff --git a/tests/run/cyfunction.pyx b/tests/run/cyfunction.pyx index 277ff2c4f34..04166516245 100644 --- a/tests/run/cyfunction.pyx +++ b/tests/run/cyfunction.pyx @@ -2,6 +2,7 @@ # mode: run # tag: cyfunction +include "skip_limited_api_helper.pxi" def inspect_isroutine(): """ @@ -61,6 +62,7 @@ def inspect_signature(a, b, c=123, *, d=234): # return inspect_signature.__signature__ +@skip_if_limited_api("dictoffset not set", min_runtime_version=(3, 9)) def test_dict(): """ >>> test_dict.foo = 123 @@ -409,3 +411,20 @@ def test_firstlineno_decorated_function(): l1 = do_nothing.__code__.co_firstlineno l2 = test_firstlineno_decorated_function.__code__.co_firstlineno return l2 - l1 + +def test_module(): + """ + See module-level docstring. doctest uses __module__ to decide what to run. So if it's wrong + then we don't run the tests, and never find out about the failure! + """ + pass + +__doc__ = """ +>>> test_module.__module__ +'cyfunction' +>>> type(test_module).__module__.startswith("_cython") +True +>>> test_module.__module__ = "something_else" +>>> test_module.__module__ +'something_else' +""" diff --git a/tests/run/skip_limited_api_helper.pxi b/tests/run/skip_limited_api_helper.pxi index 5b2dc6323e9..e2b001c54d7 100644 --- a/tests/run/skip_limited_api_helper.pxi +++ b/tests/run/skip_limited_api_helper.pxi @@ -1,11 +1,14 @@ cdef extern from *: int CYTHON_COMPILING_IN_LIMITED_API -def skip_if_limited_api(why): +def skip_if_limited_api( + why, *, + min_runtime_version=None): + import sys def dec(f): - if CYTHON_COMPILING_IN_LIMITED_API: + if (CYTHON_COMPILING_IN_LIMITED_API and + (min_runtime_version is None or sys.version_info < min_runtime_version)): return None - else: - return f + return f return dec diff --git a/tests/run/tuple_constants.pyx b/tests/run/tuple_constants.pyx index 1b39fcd6ea9..c559f3ef2ca 100644 --- a/tests/run/tuple_constants.pyx +++ b/tests/run/tuple_constants.pyx @@ -6,6 +6,10 @@ second_module_level_tuple = (1,2,3) # should be deduplicated to be the same as string_module_level_tuple = ("1", "2") string_module_level_tuple2 = ("1", "2") +cdef extern from *: + cdef enum: + CYTHON_COMPILING_IN_LIMITED_API + def return_module_level_tuple(): """ >>> return_module_level_tuple() @@ -38,8 +42,9 @@ def test_deduplicated_args(): import sys check_identity_of_co_varnames = ( not hasattr(sys, "pypy_version_info") and # test doesn't work on PyPy (which is probably fair enough) - sys.version_info < (3, 11) # on Python 3.11 co_varnames returns a new, dynamically-calculated tuple + sys.version_info < (3, 11) and # on Python 3.11 co_varnames returns a new, dynamically-calculated tuple # each time it is run + not (CYTHON_COMPILING_IN_LIMITED_API and sys.version_info < (3, 9)) ) if check_identity_of_co_varnames: assert func1.__code__.co_varnames is func2.__code__.co_varnames