Repository: cython/cython

Issue:
[BUG] "pure_cdef_class_dataclass" test fails in Py3.12
### Describe the bug

The `_DataclassParams` class apparently changed its constructor.
```
======================================================================
ERROR: runTest (__main__.CythonRunTestCase.runTest)
[2] compiling (c/cy2) and running pure_cdef_class_dataclass
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tests/run/pure_cdef_class_dataclass.py", line 10, in init pure_cdef_class_dataclass (pure_cdef_class_dataclass.c:8809)
    class MyDataclass:
TypeError: _DataclassParams.__init__() missing 4 required positional arguments: 'match_args', 'kw_only', 'slots', and 'weakref_slot'
```

### Code to reproduce the behaviour:

_No response_

### Expected behaviour

_No response_

### OS

_No response_

### Python version

Py3.12a6

### Cython version

3.0.0b2

### Additional context

_No response_


Discussion:
I think this is (indirectly) my fault - I suggested that they add a bunch of fields that were missing from `_DataclassParams`.
FWIW I have a branch where this is fixed. I hadn't made a PR for it because it was on-top of [another pending PR](https://github.com/cython/cython/pull/4989). But I'll see if I can quickly pick it out.

Resolving patch:
diff --git a/Cython/Compiler/Dataclass.py b/Cython/Compiler/Dataclass.py
--- a/Cython/Compiler/Dataclass.py
+++ b/Cython/Compiler/Dataclass.py
@@ -34,7 +34,21 @@ def make_dataclasses_module_callnode(pos):
         args=[],
     )
 
-_INTERNAL_DEFAULTSHOLDER_NAME = EncodedString('__pyx_dataclass_defaults')
+def make_dataclass_call_helper(pos, callable, kwds):
+    utility_code = UtilityCode.load_cached("DataclassesCallHelper", "Dataclasses.c")
+    func_type = PyrexTypes.CFuncType(
+        PyrexTypes.py_object_type, [
+            PyrexTypes.CFuncTypeArg("callable", PyrexTypes.py_object_type, None),
+            PyrexTypes.CFuncTypeArg("kwds", PyrexTypes.py_object_type, None)
+        ],
+    )
+    return ExprNodes.PythonCapiCallNode(
+        pos,
+        function_name="__Pyx_DataclassesCallHelper",
+        func_type=func_type,
+        utility_code=utility_code,
+        args=[callable, kwds],
+    )
 
 
 class RemoveAssignmentsToNames(VisitorTransform, SkipDeclarations):
@@ -298,8 +312,7 @@ def handle_cclass_dataclass(node, dataclass_args, analyse_decs_transform):
                       "Arguments passed to cython.dataclasses.dataclass must be True or False")
             kwargs[k] = v.value
 
-    # remove everything that does not belong into _DataclassParams()
-    kw_only = kwargs.pop("kw_only")
+    kw_only = kwargs['kw_only']
 
     fields = process_class_get_fields(node)
 
@@ -314,11 +327,14 @@ def handle_cclass_dataclass(node, dataclass_args, analyse_decs_transform):
         node.pos,
         [ (ExprNodes.IdentifierStringNode(node.pos, value=EncodedString(k)),
            ExprNodes.BoolNode(node.pos, value=v))
-          for k, v in kwargs.items() ])
-    dataclass_params = ExprNodes.GeneralCallNode(node.pos,
-                                                 function = dataclass_params_func,
-                                                 positional_args = ExprNodes.TupleNode(node.pos, args=[]),
-                                                 keyword_args = dataclass_params_keywords)
+          for k, v in kwargs.items() ] +
+        [ (ExprNodes.IdentifierStringNode(node.pos, value=EncodedString(k)),
+           ExprNodes.BoolNode(node.pos, value=v))
+          for k, v in [('kw_only', kw_only), ('match_args', False),
+                       ('slots', False), ('weakref_slot', False)]
+        ])
+    dataclass_params = make_dataclass_call_helper(
+        node.pos, dataclass_params_func, dataclass_params_keywords)
     dataclass_params_assignment = Nodes.SingleAssignmentNode(
         node.pos,
         lhs = ExprNodes.NameNode(node.pos, name=EncodedString("__dataclass_params__")),
@@ -792,10 +808,9 @@ def _set_up_dataclass_fields(node, fields, dataclass_module):
               for k, v in field.iterate_record_node_arguments()]
 
         )
-        dc_field_call = ExprNodes.GeneralCallNode(
-            node.pos, function = field_func,
-            positional_args = ExprNodes.TupleNode(node.pos, args=[]),
-            keyword_args = dc_field_keywords)
+        dc_field_call = make_dataclass_call_helper(
+            node.pos, field_func, dc_field_keywords
+        )
         dc_fields.key_value_pairs.append(
             ExprNodes.DictItemNode(
                 node.pos,
diff --git a/Cython/Utility/Dataclasses.py b/Cython/Utility/Dataclasses.py
--- a/Cython/Utility/Dataclasses.py
+++ b/Cython/Utility/Dataclasses.py
@@ -18,7 +18,8 @@ class _MISSING_TYPE(object):
 MISSING = _MISSING_TYPE()
 
 _DataclassParams = namedtuple('_DataclassParams',
-    ["init", "repr", "eq", "order", "unsafe_hash", "frozen"])
+    ["init", "repr", "eq", "order", "unsafe_hash", "frozen",
+     "match_args", "kw_only", "slots", "weakref_slot"])
 class Field(object):
     __slots__ = ('name',
                  'type',
@@ -29,11 +30,12 @@ class Field(object):
                  'init',
                  'compare',
                  'metadata',
+                 'kw_only',
                  '_field_type',  # Private: not to be used by user code.
                  )
 
     def __init__(self, default, default_factory, init, repr, hash, compare,
-                 metadata):
+                 metadata, kw_only):
         self.name = None
         self.type = None
         self.default = default
@@ -47,6 +49,7 @@ def __init__(self, default, default_factory, init, repr, hash, compare,
         self.metadata = (MappingProxyType({})
                          if metadata is None else
                          MappingProxyType(metadata))
+        self.kw_only = kw_only
         self._field_type = None
 
     def __repr__(self):
@@ -60,10 +63,11 @@ def __repr__(self):
                 'hash={6!r},'
                 'compare={7!r},'
                 'metadata={8!r},'
+                'kwonly={9!r},'
                 ')'.format(self.name, self.type, self.default,
                            self.default_factory, self.init,
                            self.repr, self.hash, self.compare,
-                           self.metadata))
+                           self.metadata, self.kw_only))
 
 # A sentinel object for default values to signal that a default
 # factory will be used.  This is given a nice repr() which will appear
@@ -95,6 +99,7 @@ def field(*ignore, **kwds):
     hash = kwds.pop("hash", None)
     compare = kwds.pop("compare", True)
     metadata = kwds.pop("metadata", None)
+    kw_only = kwds.pop("kw_only", None)
 
     if kwds:
         raise ValueError("field received unexpected keyword arguments: %s"
@@ -103,4 +108,5 @@ def field(*ignore, **kwds):
         raise ValueError('cannot specify both default and default_factory')
     if ignore:
         raise ValueError("'field' does not take any positional arguments")
-    return Field(default, default_factory, init, repr, hash, compare, metadata)
+    return Field(default, default_factory, init,
+                 repr, hash, compare, metadata, kw_only)


Test patch: