Repository: pydantic/pydantic

Issue:
[PYD-179] Pydantic dataclasses have under-specified signature
This is used in FastAPI, and causes problems with using dataclasses in endpoints. Here's a fairly minimal reproduction showing that it's misbehaving in v2:

```
import inspect
from typing import Union
import pydantic
from pydantic.dataclasses import dataclass as pydantic_dc_v2
from pydantic.v1.dataclasses import dataclass as pydantic_dc_v1
from dataclasses import dataclass


@dataclass()
class CommonQueryParams:
    q: Union[str, None] = None
    skip: int = 0
    limit: int = 100


@pydantic_dc_v1()
class CommonQueryParamsV1:
    q: Union[str, None] = None
    skip: int = 0
    limit: int = 100


@pydantic_dc_v2()
class CommonQueryParamsV2:
    q: Union[str, None] = None
    skip: int = pydantic.Field(default=0)
    limit: int = 100


print(inspect.signature(CommonQueryParams))
#> (q: Optional[str] = None, skip: int = 0, limit: int = 100) -> None
print(inspect.signature(CommonQueryParamsV1))
#> (q: Optional[str] = None, skip: int = 0, limit: int = 100) -> None
print(inspect.signature(CommonQueryParamsV2))
#> (*args: 'Any', **kwargs: 'Any') -> 'None'
```
The main challenge is making sure that we get the right signature when there are `= pydantic.Field(...)`

<sub>[PYD-179](https://linear.app/pydantic/issue/PYD-179/pydantic-dataclasses-have-under-specified-signature)</sub>


Discussion:

Resolving patch:
diff --git a/pydantic/_internal/_dataclasses.py b/pydantic/_internal/_dataclasses.py
--- a/pydantic/_internal/_dataclasses.py
+++ b/pydantic/_internal/_dataclasses.py
@@ -2,12 +2,14 @@
 from __future__ import annotations as _annotations
 
 import dataclasses
+import inspect
 import typing
 import warnings
 from functools import partial, wraps
+from inspect import Parameter, Signature, signature
 from typing import Any, Callable, ClassVar
 
-from pydantic_core import ArgsKwargs, SchemaSerializer, SchemaValidator, core_schema
+from pydantic_core import ArgsKwargs, PydanticUndefined, SchemaSerializer, SchemaValidator, core_schema
 from typing_extensions import TypeGuard
 
 from ..errors import PydanticUndefinedAnnotation
@@ -122,7 +124,9 @@ def __init__(__dataclass_self__: PydanticDataclass, *args: Any, **kwargs: Any) -
         s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)
 
     __init__.__qualname__ = f'{cls.__qualname__}.__init__'
+    sig = generate_dataclass_signature(cls)
     cls.__init__ = __init__  # type: ignore
+    cls.__signature__ = sig  # type: ignore
     cls.__pydantic_config__ = config_wrapper.config_dict  # type: ignore
 
     get_core_schema = getattr(cls, '__get_pydantic_core_schema__', None)
@@ -172,6 +176,54 @@ def validated_setattr(instance: Any, __field: str, __value: str) -> None:
     return True
 
 
+def generate_dataclass_signature(cls: type[StandardDataclass]) -> Signature:
+    """Generate signature for a pydantic dataclass.
+
+    This implementation assumes we do not support custom `__init__`, which is currently true for pydantic dataclasses.
+    If we change this eventually, we should make this function's logic more closely mirror that from
+    `pydantic._internal._model_construction.generate_model_signature`.
+
+    Args:
+        cls: The dataclass.
+
+    Returns:
+        The signature.
+    """
+    sig = signature(cls)
+    final_params: dict[str, Parameter] = {}
+
+    for param in sig.parameters.values():
+        param_default = param.default
+        if isinstance(param_default, FieldInfo):
+            annotation = param.annotation
+            # Replace the annotation if appropriate
+            # inspect does "clever" things to show annotations as strings because we have
+            # `from __future__ import annotations` in main, we don't want that
+            if annotation == 'Any':
+                annotation = Any
+
+            # Replace the field name with the alias if present
+            name = param.name
+            if param_default.validation_alias is None and isinstance(param_default.alias, str):
+                name = param_default.alias
+            elif isinstance(param_default.validation_alias, str):
+                name = param_default.validation_alias
+
+            # Replace the field default
+            default = param_default.default
+            if default is PydanticUndefined:
+                if param_default.default_factory is PydanticUndefined:
+                    default = inspect.Signature.empty
+                else:
+                    # this is used by dataclasses to indicate a factory exists:
+                    default = dataclasses._HAS_DEFAULT_FACTORY  # type: ignore
+
+            param = param.replace(annotation=annotation, name=name, default=default)
+        final_params[param.name] = param
+
+    return Signature(parameters=list(final_params.values()), return_annotation=None)
+
+
 def is_builtin_dataclass(_cls: type[Any]) -> TypeGuard[type[StandardDataclass]]:
     """Returns True if a class is a stdlib dataclass and *not* a pydantic dataclass.
 
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
--- a/pydantic/dataclasses.py
+++ b/pydantic/dataclasses.py
@@ -185,7 +185,8 @@ def create_dataclass(cls: type[Any]) -> type[PydanticDataclass]:
 
         cls = dataclasses.dataclass(  # type: ignore[call-overload]
             cls,
-            init=init,
+            # the value of init here doesn't affect anything except that it makes it easier to generate a signature
+            init=True,
             repr=repr,
             eq=eq,
             order=order,


Test patch:
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -1,4 +1,5 @@
 import dataclasses
+import inspect
 import pickle
 import re
 import sys
@@ -2408,3 +2409,18 @@ class Model:
         x: int = dataclasses.field(default_factory=lambda: 3, init=False)
 
     assert Model().x == 3
+
+
+def test_signature():
+    @pydantic.dataclasses.dataclass
+    class Model:
+        x: int
+        y: str = 'y'
+        z: float = dataclasses.field(default=1.0)
+        a: float = dataclasses.field(default_factory=float)
+        b: float = Field(default=1.0)
+        c: float = Field(default_factory=float)
+
+    assert str(inspect.signature(Model)) == (
+        "(x: int, y: str = 'y', z: float = 1.0, a: float = <factory>, b: float = 1.0, c: float = <factory>) -> None"
+    )