<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->

## Change Summary

I noticed similar or identical logic in several places and extracted a couple of methods to reduce duplication.

The two commits are fairly independent and it may be easier to view their diffs separately to understand the changes. The first is probably a bit more compelling.

Note that the original code replaced by `clean_schema` wasn't always exactly the same, but the differences didn't really strike me as intentional, and either way it seemed good to draw attention to those differences and consider discussing them or adding comments to clarify.

## Related issue number

There's no issue because this isn't fixing/adding anything notable, it's just refactoring. I thought it made more sense to discuss the changes here rather than in a separate issue.

## Checklist

* [x] The pull request title is a good summary of the changes - it will be used in the changelog
* [x] Unit tests for the changes exist
* [x] Tests pass on CI
* [x] Documentation reflects the changes where applicable
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Selected Reviewer: @adriangb
Refactoring schema-related code. <!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->

## Change Summary

I noticed similar or identical logic in several places and extracted a couple of methods to reduce duplication.

The two commits are fairly independent and it may be easier to view their diffs separately to understand the changes. The first is probably a bit more compelling.

Note that the original code replaced by `clean_schema` wasn't always exactly the same, but the differences didn't really strike me as intentional, and either way it seemed good to draw attention to those differences and consider discussing them or adding comments to clarify.

## Related issue number

There's no issue because this isn't fixing/adding anything notable, it's just refactoring. I thought it made more sense to discuss the changes here rather than in a separate issue.

## Checklist

* [x] The pull request title is a good summary of the changes - it will be used in the changelog
* [x] Unit tests for the changes exist
* [x] Tests pass on CI
* [x] Documentation reflects the changes where applicable
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Selected Reviewer: @adriangb
"""Private logic for creating pydantic dataclasses."""
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,
    PydanticUndefined,
    SchemaSerializer,
    SchemaValidator,
    core_schema,
)
from typing_extensions import TypeGuard

from ..errors import PydanticUndefinedAnnotation
from ..fields import FieldInfo
from ..plugin._schema_validator import create_schema_validator
from ..warnings import PydanticDeprecatedSince20
from . import _config, _decorators, _discriminated_union, _typing_extra
from ._core_utils import collect_invalid_schemas, simplify_schema_references, validate_core_schema
from ._fields import collect_dataclass_fields
from ._generate_schema import GenerateSchema
from ._generics import get_standard_typevars_map
from ._mock_val_ser import set_dataclass_mocks
from ._schema_generation_shared import CallbackGetCoreSchemaHandler
from ._utils import is_valid_identifier

if typing.TYPE_CHECKING:
    from ..config import ConfigDict

    class StandardDataclass(typing.Protocol):
        __dataclass_fields__: ClassVar[dict[str, Any]]
        __dataclass_params__: ClassVar[Any]  # in reality `dataclasses._DataclassParams`
        __post_init__: ClassVar[Callable[..., None]]

        def __init__(self, *args: object, **kwargs: object) -> None:
            pass

    class PydanticDataclass(StandardDataclass, typing.Protocol):
        """A protocol containing attributes only available once a class has been decorated as a Pydantic dataclass.

        Attributes:
            __pydantic_config__: Pydantic-specific configuration settings for the dataclass.
            __pydantic_complete__: Whether dataclass building is completed, or if there are still undefined fields.
            __pydantic_core_schema__: The pydantic-core schema used to build the SchemaValidator and SchemaSerializer.
            __pydantic_decorators__: Metadata containing the decorators defined on the dataclass.
            __pydantic_fields__: Metadata about the fields defined on the dataclass.
            __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the dataclass.
            __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the dataclass.
        """

        __pydantic_config__: ClassVar[ConfigDict]
        __pydantic_complete__: ClassVar[bool]
        __pydantic_core_schema__: ClassVar[core_schema.CoreSchema]
        __pydantic_decorators__: ClassVar[_decorators.DecoratorInfos]
        __pydantic_fields__: ClassVar[dict[str, FieldInfo]]
        __pydantic_serializer__: ClassVar[SchemaSerializer]
        __pydantic_validator__: ClassVar[SchemaValidator]

else:
    # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915
    # and https://youtrack.jetbrains.com/issue/PY-51428
    DeprecationWarning = PydanticDeprecatedSince20


def set_dataclass_fields(cls: type[StandardDataclass], types_namespace: dict[str, Any] | None = None) -> None:
    """Collect and set `cls.__pydantic_fields__`.

    Args:
        cls: The class.
        types_namespace: The types namespace, defaults to `None`.
    """
    typevars_map = get_standard_typevars_map(cls)
    fields = collect_dataclass_fields(cls, types_namespace, typevars_map=typevars_map)

    cls.__pydantic_fields__ = fields  # type: ignore


def complete_dataclass(
    cls: type[Any],
    config_wrapper: _config.ConfigWrapper,
    *,
    raise_errors: bool = True,
    types_namespace: dict[str, Any] | None,
) -> bool:
    """Finish building a pydantic dataclass.

    This logic is called on a class which has already been wrapped in `dataclasses.dataclass()`.

    This is somewhat analogous to `pydantic._internal._model_construction.complete_model_class`.

    Args:
        cls: The class.
        config_wrapper: The config wrapper instance.
        raise_errors: Whether to raise errors, defaults to `True`.
        types_namespace: The types namespace.

    Returns:
        `True` if building a pydantic dataclass is successfully completed, `False` otherwise.

    Raises:
        PydanticUndefinedAnnotation: If `raise_error` is `True` and there is an undefined annotations.
    """
    if hasattr(cls, '__post_init_post_parse__'):
        warnings.warn(
            'Support for `__post_init_post_parse__` has been dropped, the method will not be called', DeprecationWarning
        )

    if types_namespace is None:
        types_namespace = _typing_extra.get_cls_types_namespace(cls)

    set_dataclass_fields(cls, types_namespace)

    typevars_map = get_standard_typevars_map(cls)
    gen_schema = GenerateSchema(
        config_wrapper,
        types_namespace,
        typevars_map,
    )

    # dataclass.__init__ must be defined here so its `__qualname__` can be changed since functions can't be copied.

    def __init__(__dataclass_self__: PydanticDataclass, *args: Any, **kwargs: Any) -> None:
        __tracebackhide__ = True
        s = __dataclass_self__
        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)
    try:
        if get_core_schema:
            schema = get_core_schema(
                cls,
                CallbackGetCoreSchemaHandler(
                    partial(gen_schema.generate_schema, from_dunder_get_core_schema=False),
                    gen_schema,
                    ref_mode='unpack',
                ),
            )
        else:
            schema = gen_schema.generate_schema(cls, from_dunder_get_core_schema=False)
    except PydanticUndefinedAnnotation as e:
        if raise_errors:
            raise
        set_dataclass_mocks(cls, cls.__name__, f'`{e.name}`')
        return False

    core_config = config_wrapper.core_config(cls)

    schema = gen_schema.collect_definitions(schema)
    if collect_invalid_schemas(schema):
        set_dataclass_mocks(cls, cls.__name__, 'all referenced types')
        return False

    schema = _discriminated_union.apply_discriminators(simplify_schema_references(schema))

    # We are about to set all the remaining required properties expected for this cast;
    # __pydantic_decorators__ and __pydantic_fields__ should already be set
    cls = typing.cast('type[PydanticDataclass]', cls)
    # debug(schema)

    cls.__pydantic_core_schema__ = schema = validate_core_schema(schema)
    cls.__pydantic_validator__ = validator = create_schema_validator(
        schema, core_config, config_wrapper.plugin_settings
    )
    cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config)

    if config_wrapper.validate_assignment:

        @wraps(cls.__setattr__)
        def validated_setattr(instance: Any, __field: str, __value: str) -> None:
            validator.validate_assignment(instance, __field, __value)

        cls.__setattr__ = validated_setattr.__get__(None, cls)  # type: ignore

    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
            alias = param_default.alias
            validation_alias = param_default.validation_alias
            if validation_alias is None and isinstance(alias, str) and is_valid_identifier(alias):
                name = alias
            elif isinstance(validation_alias, str) and is_valid_identifier(validation_alias):
                name = 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.

    We check that
    - `_cls` is a dataclass
    - `_cls` does not inherit from a processed pydantic dataclass (and thus have a `__pydantic_validator__`)
    - `_cls` does not have any annotations that are not dataclass fields
    e.g.
    ```py
    import dataclasses

    import pydantic.dataclasses

    @dataclasses.dataclass
    class A:
        x: int

    @pydantic.dataclasses.dataclass
    class B(A):
        y: int
    ```
    In this case, when we first check `B`, we make an extra check and look at the annotations ('y'),
    which won't be a superset of all the dataclass fields (only the stdlib fields i.e. 'x')

    Args:
        cls: The class.

    Returns:
        `True` if the class is a stdlib dataclass, `False` otherwise.
    """
    return (
        dataclasses.is_dataclass(_cls)
        and not hasattr(_cls, '__pydantic_validator__')
        and set(_cls.__dataclass_fields__).issuperset(set(getattr(_cls, '__annotations__', {})))
    )

"""Private logic for creating pydantic dataclasses."""
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,
    PydanticUndefined,
    SchemaSerializer,
    SchemaValidator,
    core_schema,
)
from typing_extensions import TypeGuard

from ..errors import PydanticUndefinedAnnotation
from ..fields import FieldInfo
from ..plugin._schema_validator import create_schema_validator
from ..warnings import PydanticDeprecatedSince20
from . import _config, _decorators, _typing_extra
from ._fields import collect_dataclass_fields
from ._generate_schema import GenerateSchema
from ._generics import get_standard_typevars_map
from ._mock_val_ser import set_dataclass_mocks
from ._schema_generation_shared import CallbackGetCoreSchemaHandler
from ._utils import is_valid_identifier

if typing.TYPE_CHECKING:
    from ..config import ConfigDict

    class StandardDataclass(typing.Protocol):
        __dataclass_fields__: ClassVar[dict[str, Any]]
        __dataclass_params__: ClassVar[Any]  # in reality `dataclasses._DataclassParams`
        __post_init__: ClassVar[Callable[..., None]]

        def __init__(self, *args: object, **kwargs: object) -> None:
            pass

    class PydanticDataclass(StandardDataclass, typing.Protocol):
        """A protocol containing attributes only available once a class has been decorated as a Pydantic dataclass.

        Attributes:
            __pydantic_config__: Pydantic-specific configuration settings for the dataclass.
            __pydantic_complete__: Whether dataclass building is completed, or if there are still undefined fields.
            __pydantic_core_schema__: The pydantic-core schema used to build the SchemaValidator and SchemaSerializer.
            __pydantic_decorators__: Metadata containing the decorators defined on the dataclass.
            __pydantic_fields__: Metadata about the fields defined on the dataclass.
            __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the dataclass.
            __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the dataclass.
        """

        __pydantic_config__: ClassVar[ConfigDict]
        __pydantic_complete__: ClassVar[bool]
        __pydantic_core_schema__: ClassVar[core_schema.CoreSchema]
        __pydantic_decorators__: ClassVar[_decorators.DecoratorInfos]
        __pydantic_fields__: ClassVar[dict[str, FieldInfo]]
        __pydantic_serializer__: ClassVar[SchemaSerializer]
        __pydantic_validator__: ClassVar[SchemaValidator]

else:
    # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915
    # and https://youtrack.jetbrains.com/issue/PY-51428
    DeprecationWarning = PydanticDeprecatedSince20


def set_dataclass_fields(cls: type[StandardDataclass], types_namespace: dict[str, Any] | None = None) -> None:
    """Collect and set `cls.__pydantic_fields__`.

    Args:
        cls: The class.
        types_namespace: The types namespace, defaults to `None`.
    """
    typevars_map = get_standard_typevars_map(cls)
    fields = collect_dataclass_fields(cls, types_namespace, typevars_map=typevars_map)

    cls.__pydantic_fields__ = fields  # type: ignore


def complete_dataclass(
    cls: type[Any],
    config_wrapper: _config.ConfigWrapper,
    *,
    raise_errors: bool = True,
    types_namespace: dict[str, Any] | None,
) -> bool:
    """Finish building a pydantic dataclass.

    This logic is called on a class which has already been wrapped in `dataclasses.dataclass()`.

    This is somewhat analogous to `pydantic._internal._model_construction.complete_model_class`.

    Args:
        cls: The class.
        config_wrapper: The config wrapper instance.
        raise_errors: Whether to raise errors, defaults to `True`.
        types_namespace: The types namespace.

    Returns:
        `True` if building a pydantic dataclass is successfully completed, `False` otherwise.

    Raises:
        PydanticUndefinedAnnotation: If `raise_error` is `True` and there is an undefined annotations.
    """
    if hasattr(cls, '__post_init_post_parse__'):
        warnings.warn(
            'Support for `__post_init_post_parse__` has been dropped, the method will not be called', DeprecationWarning
        )

    if types_namespace is None:
        types_namespace = _typing_extra.get_cls_types_namespace(cls)

    set_dataclass_fields(cls, types_namespace)

    typevars_map = get_standard_typevars_map(cls)
    gen_schema = GenerateSchema(
        config_wrapper,
        types_namespace,
        typevars_map,
    )

    # dataclass.__init__ must be defined here so its `__qualname__` can be changed since functions can't be copied.

    def __init__(__dataclass_self__: PydanticDataclass, *args: Any, **kwargs: Any) -> None:
        __tracebackhide__ = True
        s = __dataclass_self__
        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)
    try:
        if get_core_schema:
            schema = get_core_schema(
                cls,
                CallbackGetCoreSchemaHandler(
                    partial(gen_schema.generate_schema, from_dunder_get_core_schema=False),
                    gen_schema,
                    ref_mode='unpack',
                ),
            )
        else:
            schema = gen_schema.generate_schema(cls, from_dunder_get_core_schema=False)
    except PydanticUndefinedAnnotation as e:
        if raise_errors:
            raise
        set_dataclass_mocks(cls, cls.__name__, f'`{e.name}`')
        return False

    core_config = config_wrapper.core_config(cls)

    try:
        schema = gen_schema.clean_schema(schema)
    except gen_schema.CollectedInvalid:
        set_dataclass_mocks(cls, cls.__name__, 'all referenced types')
        return False

    # We are about to set all the remaining required properties expected for this cast;
    # __pydantic_decorators__ and __pydantic_fields__ should already be set
    cls = typing.cast('type[PydanticDataclass]', cls)
    # debug(schema)

    cls.__pydantic_core_schema__ = schema
    cls.__pydantic_validator__ = validator = create_schema_validator(
        schema, core_config, config_wrapper.plugin_settings
    )
    cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config)

    if config_wrapper.validate_assignment:

        @wraps(cls.__setattr__)
        def validated_setattr(instance: Any, __field: str, __value: str) -> None:
            validator.validate_assignment(instance, __field, __value)

        cls.__setattr__ = validated_setattr.__get__(None, cls)  # type: ignore

    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
            alias = param_default.alias
            validation_alias = param_default.validation_alias
            if validation_alias is None and isinstance(alias, str) and is_valid_identifier(alias):
                name = alias
            elif isinstance(validation_alias, str) and is_valid_identifier(validation_alias):
                name = 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.

    We check that
    - `_cls` is a dataclass
    - `_cls` does not inherit from a processed pydantic dataclass (and thus have a `__pydantic_validator__`)
    - `_cls` does not have any annotations that are not dataclass fields
    e.g.
    ```py
    import dataclasses

    import pydantic.dataclasses

    @dataclasses.dataclass
    class A:
        x: int

    @pydantic.dataclasses.dataclass
    class B(A):
        y: int
    ```
    In this case, when we first check `B`, we make an extra check and look at the annotations ('y'),
    which won't be a superset of all the dataclass fields (only the stdlib fields i.e. 'x')

    Args:
        cls: The class.

    Returns:
        `True` if the class is a stdlib dataclass, `False` otherwise.
    """
    return (
        dataclasses.is_dataclass(_cls)
        and not hasattr(_cls, '__pydantic_validator__')
        and set(_cls.__dataclass_fields__).issuperset(set(getattr(_cls, '__annotations__', {})))
    )
