Repository: cython/cython
Pull request: Fix `__module__` attribute of shared types with metaclass
This seems the better way to do it because it doesn't impose any runtime cost to attribute lookup. It makes all of the shared types that have a `__module__` attribute inherit from a metaclass that reports the module correctly.

Supercedes https://github.com/cython/cython/pull/6845; fixes https://github.com/cython/cython/issues/6841

Problem statement:
[BUG] Invalid `member_descriptor` as __module__ on type(cython function) under 3.1.0rc1
### Describe the bug

The `__module__` of the type of a a cython-defined function ends up as a `member_descriptor` object rather than a string. This is a regression from Cython 3.0.12.


### Code to reproduce the behaviour:

```
$ uv pip install cython==3.1.0rc1
$ cat t.pyx
def foo(x):
  return x + 1
$ cythonize -b t.pyx
$ ipython
Python 3.13.2 (main, Feb 12 2025, 14:51:17) [Clang 19.1.6 ]
...

In [1]: import t
In [2]: type(t.foo).__module__
Out[2]: <member '__module__' of '_cython_3_1_0rc1.cython_function_or_method' objects>
In [3]: type(type(t.foo).__module__)
Out[3]: member_descriptor
```

### Expected behaviour

Under Cython 3.0.12, we see the following behavior, which seems more reasonable (`__module__` is a string):

```
$ uv pip install -U cython==3.0.12
$ cythonize -b t.pyx
...
$ ipython
In [1]: import t

In [2]: type(t.foo).__module__
Out[2]: '_cython_3_0_12'

In [3]: type(type(t.foo).__module__)
Out[3]: str
```

### OS

Linux

### Python version

3.13.2

### Cython version

3.1.0rc1

### Additional context

This appears to cause problems with mypy stubgen tool since it has difficulty parsing `__module__` on such functions.

Hints from issue discussion:
This bisects to a2bd48d9e6ed02e56c8c9e2a962bb901d76a7c71.

I'll have a quick look, but it may be hard to fix.
Yeah - that's going to be hard to fix unfortunately. It looks like Python has different rules for `__module__` for heap-type and static types:

https://github.com/python/cpython/blob/1bc16504ef3866cc419f3781eef6528b93aee6b4/Objects/typeobject.c#L1451-L1473.
Why is `__module__` present in the `__dict__` as a descriptor object in the first place, though?
So that you get the right answer when you do `t.foo.__module__`
Have we considered removing `__module__` as a `tp_member` and instead just adding it to the `__dict__` of the `CyFunction` during `__init__`?

e.g., the following crude hack seemed to work on a quick try?
```
--- a/cython/files/Cython/Utility/CythonFunction.c
+++ b/cython/files/Cython/Utility/CythonFunction.c
@@ -665,7 +665,7 @@ static PyGetSetDef __pyx_CyFunction_gets

 static PyMemberDef __pyx_CyFunction_members[] = {
 #if !CYTHON_COMPILING_IN_LIMITED_API
-    {"__module__", T_OBJECT, offsetof(PyCFunctionObject, m_module), 0, 0},
+    // {"__module__", T_OBJECT, offsetof(PyCFunctionObject, m_module), 0, 0},
 #endif
     {"__dictoffset__", T_PYSSIZET, offsetof(__pyx_CyFunctionObject, func_dict), READONLY, 0},
 #if CYTHON_METH_FASTCALL
@@ -732,6 +732,7 @@ static PyObject *__Pyx_CyFunction_Init(_
     Py_XINCREF(module);
     cf->m_module = module;
 #endif
+
     op->func_dict = NULL;
     op->func_name = NULL;
     Py_INCREF(qualname);
@@ -778,6 +779,11 @@ static PyObject *__Pyx_CyFunction_Init(_
         return NULL;
     }
 #endif
+
+    PyObject_SetAttrString((PyObject*)op, "__module__", module);
+    if (PyErr_Occurred()) {
+        PyErr_Print();
+    }
     return (PyObject *) op;
 }
In principle that looks like it should work. We do let people replace the function's `__dict__` so we'd probably need a little care there to preserve `__module__` when they do that.
FWIW, this breaks at least parts of Dask, which relies on `type(x).__module__` in https://github.com/dask/dask/blob/ba63b71b6ae62fa814303b48bbe8bd9c5e081a69/dask/utils.py#L747. I'm trying to confirm just how much stuff breaks, but here's an example using `distributed.Client.submit`, which tokenizes the function:

```
import t  # from https://github.com/cython/cython/issues/6841#issue-3039650288
from distributed import Client
client = Client(n_workers=1)
future = client.submit(t.foo, 1)
```

which fails with

```pytb
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[4], line 1
----> 1 future = client.submit(t.foo, 1)

File /opt/conda/envs/test/lib/python3.10/site-packages/distributed/client.py:2153, in Client.submit(self, func, key, workers, resources, retries, priority, fifo_timeout, allow_other_workers, actor, actors, pure, *args, **kwargs)
   2151 if key is None:
   2152     if pure:
-> 2153         key = funcname(func) + "-" + tokenize(func, kwargs, *args)
   2154     else:
   2155         key = funcname(func) + "-" + str(uuid.uuid4())

File /opt/conda/envs/test/lib/python3.10/site-packages/dask/tokenize.py:76, in tokenize(ensure_deterministic, *args, **kwargs)
     74     token = _ENSURE_DETERMINISTIC.set(ensure_deterministic)
     75 try:
---> 76     return _tokenize(*args, **kwargs)
     77 finally:
     78     if token:

File /opt/conda/envs/test/lib/python3.10/site-packages/dask/tokenize.py:34, in _tokenize(*args, **kwargs)
     33 def _tokenize(*args: object, **kwargs: object) -> str:
---> 34     token: object = _normalize_seq_func(args)
     35     if kwargs:
     36         token = token, _normalize_seq_func(sorted(kwargs.items()))

File /opt/conda/envs/test/lib/python3.10/site-packages/dask/tokenize.py:154, in _normalize_seq_func(seq)
    152 _SEEN[id(seq)] = len(_SEEN), seq
    153 try:
--> 154     return tuple(map(_inner_normalize_token, seq))
    155 finally:
    156     del _SEEN[id(seq)]

File /opt/conda/envs/test/lib/python3.10/site-packages/dask/tokenize.py:147, in _normalize_seq_func.<locals>._inner_normalize_token(item)
    145 if isinstance(item, _IDENTITY_DISPATCH):
    146     return item
--> 147 return normalize_token(item)

File /opt/conda/envs/test/lib/python3.10/site-packages/dask/utils.py:771, in Dispatch.__call__(self, arg, *args, **kwargs)
    767 def __call__(self, arg, *args, **kwargs):
    768     """
    769     Call the corresponding method based on type of argument.
    770     """
--> 771     meth = self.dispatch(type(arg))
    772     return meth(arg, *args, **kwargs)

File /opt/conda/envs/test/lib/python3.10/site-packages/dask/utils.py:747, in Dispatch.dispatch(self, cls)
    744 lk = self._lookup
    745 for cls2 in cls.__mro__:
    746     # Is a lazy registration function present?
--> 747     toplevel, _, _ = cls2.__module__.partition(".")
    748     try:
    749         register = self._lazy[toplevel]

AttributeError: 'member_descriptor' object has no attribute 'partition'
```
I'm running into this too when passing my cython functions to dask and it checking for `type(x).__module__` as described in the above comment.
This does seem like something we do have to fix. The three options I can think of are: 

* The version proposed here that always instantiates the dict to store the `__module__` attribute. 
* My version involving overriding `__getattribute__` and `__setattr__` (needs a small fix for Py3.8 limited API)
* Something involving metaclasses (not yet worked out)
Your PR works well for my problem, for what it's worth.

Commit message: Fix regular 3.8 build

Patch to review:
diff --git a/Cython/Compiler/ModuleNode.py b/Cython/Compiler/ModuleNode.py
index 43966071b21..d0420843a86 100644
--- a/Cython/Compiler/ModuleNode.py
+++ b/Cython/Compiler/ModuleNode.py
@@ -2855,9 +2855,10 @@ def generate_module_state_start(self, env, code):
         code.putln('PyObject *%s;' % Naming.empty_unicode)
         if Options.pre_import is not None:
             code.putln('PyObject *%s;' % Naming.preimport_cname)
-        for type_cname, used_name in Naming.used_types_and_macros:
-            code.putln('#ifdef %s' % used_name)
-            code.putln('PyTypeObject *%s;' % type_cname)
+        for type_name, linked_types in Naming.type_names_and_linked_types:
+            code.putln(f'#ifdef __Pyx_{type_name}_USED')
+            for type_name in [type_name]+linked_types:
+                code.putln(f'PyTypeObject *__pyx_{type_name}Type;')
             code.putln('#endif')
 
     def generate_module_state_end(self, env, modules, globalstate):
@@ -2928,14 +2929,11 @@ def generate_module_state_clear(self, env, code):
             Naming.empty_bytes)
         code.putln('Py_CLEAR(clear_module_state->%s);' %
             Naming.empty_unicode)
-        code.putln('#ifdef __Pyx_CyFunction_USED')
-        code.putln('Py_CLEAR(clear_module_state->%s);' %
-            Naming.cyfunction_type_cname)
-        code.putln('#endif')
-        code.putln('#ifdef __Pyx_FusedFunction_USED')
-        code.putln('Py_CLEAR(clear_module_state->%s);' %
-            Naming.fusedfunction_type_cname)
-        code.putln('#endif')
+        for tp, linked_types in Naming.type_names_and_linked_types:
+            code.putln(f'#ifdef __Pyx_{tp}_USED')
+            for tp in [tp]+linked_types:
+                code.putln(f'Py_CLEAR(clear_module_state->__pyx_{tp}Type);')
+            code.putln('#endif')
         code.putln("#if CYTHON_PEP489_MULTI_PHASE_INIT")
         # In this case we have to remove the module from our lookup table ourself
         # because Python isn't going to do it.
@@ -2953,12 +2951,11 @@ def generate_module_state_traverse(self, env, code):
         code.putln(f'__Pyx_VISIT_CONST(traverse_module_state->{Naming.empty_tuple});')
         code.putln(f'__Pyx_VISIT_CONST(traverse_module_state->{Naming.empty_bytes});')
         code.putln(f'__Pyx_VISIT_CONST(traverse_module_state->{Naming.empty_unicode});')
-        code.putln('#ifdef __Pyx_CyFunction_USED')
-        code.putln(f'Py_VISIT(traverse_module_state->{Naming.cyfunction_type_cname});')
-        code.putln('#endif')
-        code.putln('#ifdef __Pyx_FusedFunction_USED')
-        code.putln(f'Py_VISIT(traverse_module_state->{Naming.fusedfunction_type_cname});')
-        code.putln('#endif')
+        for tp, linked_types in Naming.type_names_and_linked_types:
+            code.putln(f'#ifdef __Pyx_{tp}_USED')
+            for tp in [tp]+linked_types:
+                code.putln(f'Py_VISIT(traverse_module_state->__pyx_{tp}Type);')
+            code.putln('#endif')
 
     def generate_module_init_func(self, imported_modules, env, code):
         subfunction = self.mod_init_subfunction(self.pos, self.scope, code)
@@ -3076,7 +3073,7 @@ def generate_module_init_func(self, imported_modules, env, code):
         code.putln("%s = PyUnicode_FromStringAndSize(\"\", 0); %s" % (
             empty_unicode, code.error_goto_if_null(empty_unicode, self.pos)))
 
-        for ext_type in ('CyFunction', 'FusedFunction', 'Coroutine', 'Generator', 'AsyncGen'):
+        for ext_type, _ in Naming.type_names_and_linked_types:
             code.putln("#ifdef __Pyx_%s_USED" % ext_type)
             code.put_error_if_neg(self.pos, "__pyx_%s_init(%s)" % (ext_type, env.module_cname))
             code.putln("#endif")
diff --git a/Cython/Compiler/Naming.py b/Cython/Compiler/Naming.py
index 06f3ffb8a5e..0ad81c7dcd3 100644
--- a/Cython/Compiler/Naming.py
+++ b/Cython/Compiler/Naming.py
@@ -144,8 +144,6 @@
 obj_dict_version_temp = pyrex_prefix + "obj_dict_version"
 type_dict_guard_temp = pyrex_prefix + "typedict_guard"
 cython_runtime_cname   = pyrex_prefix + "cython_runtime"
-cyfunction_type_cname = pyrex_prefix + "CyFunctionType"
-fusedfunction_type_cname = pyrex_prefix + "FusedFunctionType"
 # the name "dflt" was picked by analogy with the CPython dataclass module which stores
 # the default values in variables named f"_dflt_{field.name}" in a hidden scope that's
 # passed to the __init__ function. (The name is unimportant to the exact workings though)
@@ -197,14 +195,21 @@
 def py_version_hex(major, minor=0, micro=0, release_level=0, release_serial=0):
     return (major << 24) | (minor << 16) | (micro << 8) | (release_level << 4) | (release_serial)
 
-# there's a few places where it's useful to iterate over all of these
-used_types_and_macros = [
-    (cyfunction_type_cname, '__Pyx_CyFunction_USED'),
-    (fusedfunction_type_cname, '__Pyx_FusedFunction_USED'),
-    ('__pyx_GeneratorType', '__Pyx_Generator_USED'),
-    ('__pyx_IterableCoroutineType', '__Pyx_IterableCoroutine_USED'),
-    ('__pyx_CoroutineAwaitType', '__Pyx_Coroutine_USED'),
-    ('__pyx_CoroutineType', '__Pyx_Coroutine_USED'),
+# there's a few places where it's useful to iterate over all of these.
+# The format is:
+#  a name which is used to calculate
+#    the __Pyx_{name}_USED macro,
+#    the __pyx_{name}Type TypeObject name and
+#    the __pyx_{name}_init function
+#  a list of other types declared under the same USED macro
+type_names_and_linked_types = [
+    ('CommonTypesMetaclass', []),  # first because other types use it
+    ('CyFunction', []),
+    ('FusedFunction', []),
+    ('Generator', []),
+    ('IterableCoroutine', []),
+    ('Coroutine', ['CoroutineAwait']),
+    ('AsyncGen', ['_PyAsyncGenWrappedValue', '_PyAsyncGenASend', '_PyAsyncGenAThrow']),
 ]
 
 
diff --git a/Cython/Utility/AsyncGen.c b/Cython/Utility/AsyncGen.c
index 1ae1117096a..be871284395 100644
--- a/Cython/Utility/AsyncGen.c
+++ b/Cython/Utility/AsyncGen.c
@@ -4,11 +4,6 @@
 
 //////////////////// AsyncGenerator.module_state_decls ////////////////////
 
-PyTypeObject *__pyx__PyAsyncGenWrappedValueType;
-PyTypeObject *__pyx__PyAsyncGenASendType;
-PyTypeObject *__pyx__PyAsyncGenAThrowType;
-PyTypeObject *__pyx_AsyncGenType;
-
 // Freelists boost performance 6-10%; they also reduce memory
 // fragmentation, as _PyAsyncGenWrappedValue and PyAsyncGenASend
 // are short-living objects that are instantiated for every
@@ -370,6 +365,8 @@ static PyMethodDef __Pyx_async_gen_methods[] = {
     {"aclose", (PyCFunction)__Pyx_async_gen_aclose, METH_NOARGS, __Pyx_async_aclose_doc},
     {"__aiter__", (PyCFunction)__Pyx_async_gen_self_method, METH_NOARGS, __Pyx_async_aiter_doc},
     {"__anext__", (PyCFunction)__Pyx_async_gen_anext_method, METH_NOARGS, __Pyx_async_anext_doc},
+    {"__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}        /* Sentinel */
 };
 
@@ -982,19 +979,20 @@ __Pyx_async_gen_athrow_new(__pyx_PyAsyncGenObject *gen, PyObject *args)
 
 static int __pyx_AsyncGen_init(PyObject *module) {
     $modulestatetype_cname *mstate = __Pyx_PyModule_GetState(module);
-    mstate->__pyx_AsyncGenType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx_AsyncGenType_spec, NULL);
+    mstate->__pyx_AsyncGenType = __Pyx_FetchCommonTypeFromSpec(
+        mstate->__pyx_CommonTypesMetaclassType, module, &__pyx_AsyncGenType_spec, NULL);
     if (unlikely(!mstate->__pyx_AsyncGenType))
         return -1;
 
-    mstate->__pyx__PyAsyncGenAThrowType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx__PyAsyncGenAThrowType_spec, NULL);
+    mstate->__pyx__PyAsyncGenAThrowType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx__PyAsyncGenAThrowType_spec, NULL);
     if (unlikely(!mstate->__pyx__PyAsyncGenAThrowType))
         return -1;
 
-    mstate->__pyx__PyAsyncGenWrappedValueType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx__PyAsyncGenWrappedValueType_spec, NULL);
+    mstate->__pyx__PyAsyncGenWrappedValueType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx__PyAsyncGenWrappedValueType_spec, NULL);
     if (unlikely(!mstate->__pyx__PyAsyncGenWrappedValueType))
         return -1;
 
-    mstate->__pyx__PyAsyncGenASendType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx__PyAsyncGenASendType_spec, NULL);
+    mstate->__pyx__PyAsyncGenASendType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx__PyAsyncGenASendType_spec, NULL);
     if (unlikely(!mstate->__pyx__PyAsyncGenASendType))
         return -1;
 
diff --git a/Cython/Utility/CommonStructures.c b/Cython/Utility/CommonStructures.c
index 21cb14807ba..11ba55a6dc8 100644
--- a/Cython/Utility/CommonStructures.c
+++ b/Cython/Utility/CommonStructures.c
@@ -10,7 +10,7 @@ static PyObject *__Pyx_FetchSharedCythonABIModule(void) {
 
 /////////////// FetchCommonType.proto ///////////////
 
-static PyTypeObject* __Pyx_FetchCommonTypeFromSpec(PyObject *module, PyType_Spec *spec, PyObject *bases);
+static PyTypeObject* __Pyx_FetchCommonTypeFromSpec(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases);
 
 /////////////// FetchCommonType ///////////////
 //@requires:ExtensionTypes.c::FixUpExtensionType
@@ -18,6 +18,25 @@ static PyTypeObject* __Pyx_FetchCommonTypeFromSpec(PyObject *module, PyType_Spec
 //@requires:StringTools.c::IncludeStringH
 //@requires:Optimize.c::dict_setdefault
 
+#if __PYX_LIMITED_VERSION_HEX < 0x030C0000
+static PyObject* __Pyx_PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases) {
+    PyObject *result = __Pyx_PyType_FromModuleAndSpec(module, spec, bases);
+    if (result && metaclass) {
+        PyObject *old_tp = (PyObject*)Py_TYPE(result);
+    Py_INCREF((PyObject*)metaclass);
+#if __PYX_LIMITED_VERSION_HEX >= 0x03090000
+        Py_SET_TYPE(result, metaclass);
+#else
+        result->ob_type = metaclass;
+#endif
+        Py_DECREF(old_tp);
+    }
+    return result;
+}
+#else
+#define __Pyx_PyType_FromMetaclass(me, mo, s, b) PyType_FromMetaclass(me, mo, s, b)
+#endif
+
 static int __Pyx_VerifyCachedType(PyObject *cached_type,
                                const char *name,
                                Py_ssize_t expected_basicsize) {
@@ -29,6 +48,10 @@ static int __Pyx_VerifyCachedType(PyObject *cached_type,
         return -1;
     }
 
+    if (expected_basicsize == 0) {
+        return 0; // size is inherited, nothing useful to check
+    }
+
 #if CYTHON_COMPILING_IN_LIMITED_API
     PyObject *py_basicsize;
     py_basicsize = PyObject_GetAttrString(cached_type, "__basicsize__");
@@ -50,7 +73,7 @@ static int __Pyx_VerifyCachedType(PyObject *cached_type,
     return 0;
 }
 
-static PyTypeObject *__Pyx_FetchCommonTypeFromSpec(PyObject *module, PyType_Spec *spec, PyObject *bases) {
+static PyTypeObject *__Pyx_FetchCommonTypeFromSpec(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases) {
     PyObject *abi_module = NULL, *cached_type = NULL, *abi_module_dict, *new_cached_type, *py_object_name;
     int get_item_ref_result;
     // get the final part of the object name (after the last dot)
@@ -81,7 +104,7 @@ static PyTypeObject *__Pyx_FetchCommonTypeFromSpec(PyObject *module, PyType_Spec
 
     // We pass the ABI module reference to avoid keeping the user module alive by foreign type usages.
     CYTHON_UNUSED_VAR(module);
-    cached_type = __Pyx_PyType_FromModuleAndSpec(abi_module, spec, bases);
+    cached_type = __Pyx_PyType_FromMetaclass(metaclass, abi_module, spec, bases);
     if (unlikely(!cached_type)) goto bad;
     if (unlikely(__Pyx_fix_up_extension_type_from_spec(spec, (PyTypeObject *) cached_type) < 0)) goto bad;
 
@@ -116,3 +139,51 @@ static PyTypeObject *__Pyx_FetchCommonTypeFromSpec(PyObject *module, PyType_Spec
     goto done;
 }
 
+/////////////////////////// CommonTypesMetaclass.proto ////////////////////////
+
+static int __pyx_CommonTypesMetaclass_init(PyObject *module); /* proto */
+
+#define __Pyx_CommonTypesMetaclass_USED
+
+//////////////////// CommonTypesMetaclass ////////////////////
+//@requires: FetchCommonType
+//@substitute: naming
+
+PyObject* __pyx_CommonTypesMetaclass_get_module(CYTHON_UNUSED PyObject *self, CYTHON_UNUSED void* context) {
+    return PyUnicode_FromString(__PYX_ABI_MODULE_NAME);
+}
+
+static PyGetSetDef __pyx_CommonTypesMetaclass_getset[] = {
+    {"__module__", __pyx_CommonTypesMetaclass_get_module, NULL, NULL, NULL},
+    {0, 0, 0, 0, 0}
+};
+
+static PyType_Slot __pyx_CommonTypesMetaclass_slots[] = {
+    {Py_tp_getset, (void *)__pyx_CommonTypesMetaclass_getset},
+    {0, 0}
+};
+
+static PyType_Spec __pyx_CommonTypesMetaclass_spec = {
+    __PYX_TYPE_MODULE_PREFIX "_common_types_metatype",
+    0,
+    0,
+#if PY_VERSION_HEX >= 0x030A0000
+    Py_TPFLAGS_IMMUTABLETYPE |
+    Py_TPFLAGS_DISALLOW_INSTANTIATION |
+#endif
+    Py_TPFLAGS_DEFAULT, /*tp_flags*/
+    __pyx_CommonTypesMetaclass_slots
+};
+
+static int __pyx_CommonTypesMetaclass_init(PyObject *module) {   
+    $modulestatetype_cname *mstate = __Pyx_PyModule_GetState(module);
+    PyObject *bases = PyTuple_Pack(1, &PyType_Type);
+    if (unlikely(!bases)) {
+        return -1;
+    }
+    mstate->__pyx_CommonTypesMetaclassType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CommonTypesMetaclass_spec, bases);
+    if (unlikely(mstate->__pyx_CommonTypesMetaclassType == NULL)) {
+        return -1;
+    }
+    return 0;
+}
diff --git a/Cython/Utility/Coroutine.c b/Cython/Utility/Coroutine.c
index de29e538d9e..10080788a95 100644
--- a/Cython/Utility/Coroutine.c
+++ b/Cython/Utility/Coroutine.c
@@ -498,6 +498,8 @@ static PyObject *__Pyx_Coroutine_get_is_running_getter(PyObject *gen, void *clos
 static void __Pyx_SetBackportTypeAmSend(PyTypeObject *type, __Pyx_PyAsyncMethodsStruct *static_amsend_methods, __Pyx_pyiter_sendfunc am_send); /* proto */
 #endif
 
+static PyObject *__Pyx_Coroutine_fail_reduce_ex(PyObject *self, PyObject *arg); /*proto*/
+
 //////////////////// Coroutine.proto ////////////////////
 
 #define __Pyx_Coroutine_USED
@@ -554,6 +556,7 @@ static CYTHON_INLINE PyObject *__Pyx_Generator_GetInlinedResult(PyObject *self);
 //@requires: ObjectHandling.c::PyObjectGetAttrStrNoError
 //@requires: ObjectHandling.c::IterNextPlain
 //@requires: CommonStructures.c::FetchCommonType
+//@requires: CommonStructures.c::CommonTypesMetaclass
 //@requires: ModuleSetupCode.c::IncludeStructmemberH
 //@requires: ExtensionTypes.c::CallTypeTraverse
 
@@ -1687,6 +1690,26 @@ static void __Pyx_SetBackportTypeAmSend(PyTypeObject *type, __Pyx_PyAsyncMethods
 }
 #endif
 
+
+// In earlier versions of Python an object with no __dict__ and not __slots__ is assumed
+// to be pickleable by default. Coroutine-wrappers have significant state so shouldn't be.
+// Therefore provide a default implementation.
+// Something similar applies to heaptypes (i.e. with type_specs) with protocols 0 and 1
+// even in more recent versions.
+// 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_Coroutine_fail_reduce_ex(PyObject *self, PyObject *arg) {
+    CYTHON_UNUSED_VAR(arg);
+
+    __Pyx_TypeName self_type_name = __Pyx_PyType_GetFullyQualifiedName(Py_TYPE((PyObject*)self));
+    PyErr_Format(PyExc_TypeError, "cannot pickle '" __Pyx_FMT_TYPENAME "' object",
+                         self_type_name);
+    __Pyx_DECREF_TypeName(self_type_name);
+    return NULL;
+}
+// #endif
+
 //////////////////// Coroutine ////////////////////
 //@requires: CoroutineBase
 //@requires: ExtensionTypes.c::CallTypeTraverse
@@ -1763,25 +1786,6 @@ static PyObject *__Pyx_CoroutineAwait_no_new(PyTypeObject *type, PyObject *args,
 }
 #endif
 
-// In earlier versions of Python an object with no __dict__ and not __slots__ is assumed
-// to be pickleable by default. Coroutine-wrappers have significant state so shouldn't be.
-// Therefore provide a default implementation.
-// Something similar applies to heaptypes (i.e. with type_specs) with protocols 0 and 1
-// even in more recent versions.
-// 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) {
-    CYTHON_UNUSED_VAR(arg);
-
-    __Pyx_TypeName self_type_name = __Pyx_PyType_GetFullyQualifiedName(Py_TYPE((PyObject*)self));
-    PyErr_Format(PyExc_TypeError, "cannot pickle '" __Pyx_FMT_TYPENAME "' object",
-                         self_type_name);
-    __Pyx_DECREF_TypeName(self_type_name);
-    return NULL;
-}
-// #endif
-
 static PyMethodDef __pyx_CoroutineAwait_methods[] = {
     {"send", (PyCFunction) __Pyx_CoroutineAwait_Send, METH_O,
      PyDoc_STR("send(arg) -> send 'arg' into coroutine,\nreturn next yielded value or raise StopIteration.")},
@@ -1790,8 +1794,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}
 };
@@ -1850,6 +1854,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}
 };
 
@@ -1911,7 +1917,8 @@ static int __pyx_Coroutine_init(PyObject *module) {
     CYTHON_MAYBE_UNUSED_VAR(module);
     // on Windows, C-API functions can't be used in slots statically
     mstate = __Pyx_PyModule_GetState(module);
-    mstate->__pyx_CoroutineType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx_CoroutineType_spec, NULL);
+    mstate->__pyx_CoroutineType = __Pyx_FetchCommonTypeFromSpec(
+        mstate->__pyx_CommonTypesMetaclassType, module, &__pyx_CoroutineType_spec, NULL);
     if (unlikely(!mstate->__pyx_CoroutineType))
         return -1;
 #if __PYX_HAS_PY_AM_SEND == 2
@@ -1923,12 +1930,13 @@ static int __pyx_Coroutine_init(PyObject *module) {
         return -1;
 #endif
 
-    mstate->__pyx_CoroutineAwaitType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx_CoroutineAwaitType_spec, NULL);
+    mstate->__pyx_CoroutineAwaitType = __Pyx_FetchCommonTypeFromSpec(NULL, module, &__pyx_CoroutineAwaitType_spec, NULL);
     if (unlikely(!mstate->__pyx_CoroutineAwaitType))
         return -1;
 #if __PYX_HAS_PY_AM_SEND == 2
     __Pyx_SetBackportTypeAmSend(mstate->__pyx_CoroutineAwaitType, &__pyx_CoroutineAwait_as_async, &__Pyx_CoroutineAwait_AmSend);
 #endif
+
     return 0;
 }
 
@@ -1949,6 +1957,7 @@ static int __pyx_IterableCoroutine_init(PyObject *module);/*proto*/
 //////////////////// IterableCoroutine ////////////////////
 //@requires: Coroutine
 //@requires: CommonStructures.c::FetchCommonType
+//@requires: CommonStructures.c::CommonTypesMetaclass
 //@substitute: naming
 
 static PyType_Slot __pyx_IterableCoroutineType_slots[] = {
@@ -1984,7 +1993,8 @@ static PyType_Spec __pyx_IterableCoroutineType_spec = {
 
 static int __pyx_IterableCoroutine_init(PyObject *module) {
     $modulestatetype_cname *mstate = __Pyx_PyModule_GetState(module);
-    mstate->__pyx_IterableCoroutineType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx_IterableCoroutineType_spec, NULL);
+    mstate->__pyx_IterableCoroutineType = __Pyx_FetchCommonTypeFromSpec(
+        mstate->__pyx_CommonTypesMetaclassType, module, &__pyx_IterableCoroutineType_spec, NULL);
     if (unlikely(!mstate->__pyx_IterableCoroutineType))
         return -1;
 #if __PYX_HAS_PY_AM_SEND == 2
@@ -2005,6 +2015,8 @@ static PyMethodDef __pyx_Generator_methods[] = {
      PyDoc_STR("throw(typ[,val[,tb]]) -> raise exception in generator,\nreturn next yielded value or raise StopIteration.")},
     {"close", (PyCFunction) __Pyx_Coroutine_Close_Method, METH_NOARGS,
      PyDoc_STR("close() -> raise GeneratorExit inside generator.")},
+    {"__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}
 };
 
@@ -2063,7 +2075,8 @@ static __Pyx_PyAsyncMethodsStruct __pyx_Generator_as_async;
 
 static int __pyx_Generator_init(PyObject *module) {
     $modulestatetype_cname *mstate = __Pyx_PyModule_GetState(module);
-    mstate->__pyx_GeneratorType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx_GeneratorType_spec, NULL);
+    mstate->__pyx_GeneratorType = __Pyx_FetchCommonTypeFromSpec(
+        mstate->__pyx_CommonTypesMetaclassType, module, &__pyx_GeneratorType_spec, NULL);
     if (unlikely(!mstate->__pyx_GeneratorType)) {
         return -1;
     }
diff --git a/Cython/Utility/CythonFunction.c b/Cython/Utility/CythonFunction.c
index 987f2d80a60..49d3c0388d1 100644
--- a/Cython/Utility/CythonFunction.c
+++ b/Cython/Utility/CythonFunction.c
@@ -111,6 +111,7 @@ static PyObject * __Pyx_CyFunction_Vectorcall_FASTCALL_KEYWORDS_METHOD(PyObject
 
 //////////////////// CythonFunctionShared ////////////////////
 //@requires: CommonStructures.c::FetchCommonType
+//@requires: CommonStructures.c::CommonTypesMetaclass
 //@requires: ObjectHandling.c::PyMethodNew
 //@requires: ObjectHandling.c::PyVectorcallFastCallDict
 //@requires: ModuleSetupCode.c::IncludeStructmemberH
@@ -1238,7 +1239,8 @@ static PyType_Spec __pyx_CyFunctionType_spec = {
 
 static int __pyx_CyFunction_init(PyObject *module) {
     $modulestatetype_cname *mstate = __Pyx_PyModule_GetState(module);
-    mstate->__pyx_CyFunctionType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx_CyFunctionType_spec, NULL);
+    mstate->__pyx_CyFunctionType = __Pyx_FetchCommonTypeFromSpec(
+        mstate->__pyx_CommonTypesMetaclassType, module, &__pyx_CyFunctionType_spec, NULL);
     if (unlikely(mstate->__pyx_CyFunctionType == NULL)) {
         return -1;
     }
@@ -1705,7 +1707,8 @@ static int __pyx_FusedFunction_init(PyObject *module) {
     if (unlikely(!bases)) {
         return -1;
     }
-    mstate->__pyx_FusedFunctionType = __Pyx_FetchCommonTypeFromSpec(module, &__pyx_FusedFunctionType_spec, bases);
+    mstate->__pyx_FusedFunctionType = __Pyx_FetchCommonTypeFromSpec(
+        mstate->__pyx_CommonTypesMetaclassType, module, &__pyx_FusedFunctionType_spec, bases);
     Py_DECREF(bases);
     if (unlikely(mstate->__pyx_FusedFunctionType == NULL)) {
         return -1;
diff --git a/Cython/Utility/ExtensionTypes.c b/Cython/Utility/ExtensionTypes.c
index d0333f8c586..fd9b45bc560 100644
--- a/Cython/Utility/ExtensionTypes.c
+++ b/Cython/Utility/ExtensionTypes.c
@@ -18,13 +18,12 @@ static int __Pyx_fix_up_extension_type_from_spec(PyType_Spec *spec, PyTypeObject
         slot++;
     if (slot && slot->slot == Py_tp_members) {
         int changed = 0;
-#if !(PY_VERSION_HEX <= 0x030900b1 && CYTHON_COMPILING_IN_CPYTHON)
+#if !CYTHON_COMPILING_IN_CPYTHON
         const
-#endif
+#endif  // !CYTHON_COMPILING_IN_CPYTHON)
             PyMemberDef *memb = (PyMemberDef*) slot->pfunc;
         while (memb && memb->name) {
             if (memb->name[0] == '_' && memb->name[1] == '_') {
-#if PY_VERSION_HEX < 0x030900b1
                 if (strcmp(memb->name, "__weaklistoffset__") == 0) {
                     // The PyMemberDef must be a Py_ssize_t and readonly.
                     assert(memb->type == T_PYSSIZET);
@@ -54,11 +53,7 @@ static int __Pyx_fix_up_extension_type_from_spec(PyType_Spec *spec, PyTypeObject
                     // FIXME: is it even worth calling PyType_Modified() here?
                     changed = 1;
                 }
-#endif
-#else
-                if ((0));
-#endif
-#if PY_VERSION_HEX <= 0x030900b1 && CYTHON_COMPILING_IN_CPYTHON
+#endif  // CYTHON_METH_FASTCALL
                 else if (strcmp(memb->name, "__module__") == 0) {
                     // PyType_FromSpec() in CPython <= 3.9b1 overwrites this field with a constant string.
                     // See https://bugs.python.org/issue40703
@@ -69,21 +64,43 @@ static int __Pyx_fix_up_extension_type_from_spec(PyType_Spec *spec, PyTypeObject
                     descr = PyDescr_NewMember(type, memb);
                     if (unlikely(!descr))
                         return -1;
-                    if (unlikely(PyDict_SetItem(type->tp_dict, PyDescr_NAME(descr), descr) < 0)) {
-                        Py_DECREF(descr);
+                    int set_item_result = PyDict_SetItem(type->tp_dict, PyDescr_NAME(descr), descr);
+                    Py_DECREF(descr);
+                    if (unlikely(set_item_result < 0)) {
                         return -1;
                     }
-                    Py_DECREF(descr);
                     changed = 1;
                 }
-#endif
             }
             memb++;
         }
         if (changed)
             PyType_Modified(type);
     }
-#endif
+    slot = spec->slots;
+    while (slot && slot->slot && slot->slot != Py_tp_getset)
+        slot++;
+    if (slot && slot->slot == Py_tp_getset) {
+        int changed = 0;
+        PyGetSetDef *getset = (PyGetSetDef*) slot->pfunc;
+        while (getset && getset->name) {
+            if (getset->name[0] == '_' && getset->name[1] == '_' && strcmp(getset->name, "__module__") == 0) {
+                PyObject *descr = PyDescr_NewGetSet(type, getset);
+                if (unlikely(!descr))
+                    return -1;
+                int set_item_result = PyDict_SetItem(type->tp_dict, PyDescr_NAME(descr), descr);
+                Py_DECREF(descr);
+                if (unlikely(set_item_result < 0)) {
+                    return -1;
+                }
+                changed = 1;
+            }
+            ++getset;    
+        }
+        if (changed)
+            PyType_Modified(type);
+    }
+#endif  // // PY_VERSION_HEX > 0x030900B1 || CYTHON_COMPILING_IN_LIMITED_API
     return 0;
 }
 
diff --git a/tests/run/cyfunction.pyx b/tests/run/cyfunction.pyx
index 277ff2c4f34..d6c6552f978 100644
--- a/tests/run/cyfunction.pyx
+++ b/tests/run/cyfunction.pyx
@@ -2,6 +2,11 @@
 # mode: run
 # tag: cyfunction
 
+cimport cython
+
+include "skip_limited_api_helper.pxi"
+
+import sys
 
 def inspect_isroutine():
     """
@@ -409,3 +414,33 @@ def test_firstlineno_decorated_function():
     l1 = do_nothing.__code__.co_firstlineno
     l2 = test_firstlineno_decorated_function.__code__.co_firstlineno
     return l2 - l1
+
+@skip_if_limited_api("https://bugs.python.org/issue38140 and/or https://bugs.python.org/issue40703", sys.version_info < (3, 9))
+def test_module():
+    """
+    >>> test_module.__module__
+    'cyfunction'
+    >>> type(test_module).__module__.startswith("_cython")
+    True
+    >>> test_module.__module__ = "something_else"
+    >>> test_module.__module__
+    'something_else'
+    >>> del test_module.__module__
+    >>> test_module.__module__
+    """
+    pass
+
+@skip_if_limited_api("https://bugs.python.org/issue38140 and/or https://bugs.python.org/issue40703", sys.version_info < (3, 9))
+def test_fused_module(cython.numeric arg):
+    """
+    >>> test_fused_module.__module__
+    'cyfunction'
+    >>> type(test_fused_module).__module__.startswith("_cython")
+    True
+    >>> test_fused_module.__module__ = "something_else"
+    >>> test_fused_module.__module__
+    'something_else'
+    >>> del test_fused_module.__module__
+    >>> test_fused_module.__module__
+    """
+    pass
diff --git a/tests/run/skip_limited_api_helper.pxi b/tests/run/skip_limited_api_helper.pxi
index 5b2dc6323e9..d91ed8356bc 100644
--- a/tests/run/skip_limited_api_helper.pxi
+++ b/tests/run/skip_limited_api_helper.pxi
@@ -1,9 +1,9 @@
 cdef extern from *:
     int CYTHON_COMPILING_IN_LIMITED_API
 
-def skip_if_limited_api(why):
+def skip_if_limited_api(why, other_test=True):
     def dec(f):
-        if CYTHON_COMPILING_IN_LIMITED_API:
+        if CYTHON_COMPILING_IN_LIMITED_API and other_test:
             return None
         else:
             return f


Code review comments:
diff_hunk: @@ -2928,14 +2929,11 @@ def generate_module_state_clear(self, env, code):
             Naming.empty_bytes)
         code.putln('Py_CLEAR(clear_module_state->%s);' %
             Naming.empty_unicode)
-        code.putln('#ifdef __Pyx_CyFunction_USED')
-        code.putln('Py_CLEAR(clear_module_state->%s);' %
-            Naming.cyfunction_type_cname)
-        code.putln('#endif')
-        code.putln('#ifdef __Pyx_FusedFunction_USED')
-        code.putln('Py_CLEAR(clear_module_state->%s);' %
-            Naming.fusedfunction_type_cname)
-        code.putln('#endif')
+        for tp, linked_types in Naming.type_names_and_linked_types:
path: Cython/Compiler/ModuleNode.py
text: @user1:
Can't we move these to (new) utility code sections? That would put them close to the places where the globals are created, instead of letting multiple places know the complete list of such globals plus their (potentially changing) dependencies. It would also avoid all this `#ifdef …_USED`.

@author:
Yes that might make sense. Essentially what I originally did with AsyncGen and undid with this PR....

I think it's just "traverse" that doesn't currently have a utility code section, but it's easy enough to add one