1"""Private logic for creating pydantic dataclasses."""
2
3from __future__ import annotations as _annotations
4
5import copy
6import dataclasses
7import sys
8import warnings
9from collections.abc import Generator
10from contextlib import contextmanager
11from functools import partial
12from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast
13
14from pydantic_core import (
15 ArgsKwargs,
16 SchemaSerializer,
17 SchemaValidator,
18 core_schema,
19)
20from typing_extensions import TypeAlias, TypeIs
21
22from ..errors import PydanticUndefinedAnnotation
23from ..fields import FieldInfo
24from ..plugin._schema_validator import PluggableSchemaValidator, create_schema_validator
25from ..warnings import PydanticDeprecatedSince20
26from . import _config, _decorators
27from ._fields import collect_dataclass_fields
28from ._generate_schema import GenerateSchema, InvalidSchemaError
29from ._generics import get_standard_typevars_map
30from ._mock_val_ser import set_dataclass_mocks
31from ._namespace_utils import NsResolver
32from ._signature import generate_pydantic_signature
33from ._utils import LazyClassAttribute
34
35if TYPE_CHECKING:
36 from _typeshed import DataclassInstance as StandardDataclass
37
38 from ..config import ConfigDict
39
40 class PydanticDataclass(StandardDataclass, Protocol):
41 """A protocol containing attributes only available once a class has been decorated as a Pydantic dataclass.
42
43 Attributes:
44 __pydantic_config__: Pydantic-specific configuration settings for the dataclass.
45 __pydantic_complete__: Whether dataclass building is completed, or if there are still undefined fields.
46 __pydantic_core_schema__: The pydantic-core schema used to build the SchemaValidator and SchemaSerializer.
47 __pydantic_decorators__: Metadata containing the decorators defined on the dataclass.
48 __pydantic_fields__: Metadata about the fields defined on the dataclass.
49 __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the dataclass.
50 __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the dataclass.
51 """
52
53 __pydantic_config__: ClassVar[ConfigDict]
54 __pydantic_complete__: ClassVar[bool]
55 __pydantic_core_schema__: ClassVar[core_schema.CoreSchema]
56 __pydantic_decorators__: ClassVar[_decorators.DecoratorInfos]
57 __pydantic_fields__: ClassVar[dict[str, FieldInfo]]
58 __pydantic_serializer__: ClassVar[SchemaSerializer]
59 __pydantic_validator__: ClassVar[SchemaValidator | PluggableSchemaValidator]
60
61 @classmethod
62 def __pydantic_fields_complete__(cls) -> bool: ...
63
64
65def set_dataclass_fields(
66 cls: type[StandardDataclass],
67 config_wrapper: _config.ConfigWrapper,
68 ns_resolver: NsResolver | None = None,
69) -> None:
70 """Collect and set `cls.__pydantic_fields__`.
71
72 Args:
73 cls: The class.
74 config_wrapper: The config wrapper instance.
75 ns_resolver: Namespace resolver to use when getting dataclass annotations.
76 """
77 typevars_map = get_standard_typevars_map(cls)
78 fields = collect_dataclass_fields(
79 cls, ns_resolver=ns_resolver, typevars_map=typevars_map, config_wrapper=config_wrapper
80 )
81
82 cls.__pydantic_fields__ = fields # type: ignore
83
84
85def complete_dataclass(
86 cls: type[Any],
87 config_wrapper: _config.ConfigWrapper,
88 *,
89 raise_errors: bool = True,
90 ns_resolver: NsResolver | None = None,
91 _force_build: bool = False,
92) -> bool:
93 """Finish building a pydantic dataclass.
94
95 This logic is called on a class which has already been wrapped in `dataclasses.dataclass()`.
96
97 This is somewhat analogous to `pydantic._internal._model_construction.complete_model_class`.
98
99 Args:
100 cls: The class.
101 config_wrapper: The config wrapper instance.
102 raise_errors: Whether to raise errors, defaults to `True`.
103 ns_resolver: The namespace resolver instance to use when collecting dataclass fields
104 and during schema building.
105 _force_build: Whether to force building the dataclass, no matter if
106 [`defer_build`][pydantic.config.ConfigDict.defer_build] is set.
107
108 Returns:
109 `True` if building a pydantic dataclass is successfully completed, `False` otherwise.
110
111 Raises:
112 PydanticUndefinedAnnotation: If `raise_error` is `True` and there is an undefined annotations.
113 """
114 original_init = cls.__init__
115
116 # dataclass.__init__ must be defined here so its `__qualname__` can be changed since functions can't be copied,
117 # and so that the mock validator is used if building was deferred:
118 def __init__(__dataclass_self__: PydanticDataclass, *args: Any, **kwargs: Any) -> None:
119 __tracebackhide__ = True
120 s = __dataclass_self__
121 s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)
122
123 __init__.__qualname__ = f'{cls.__qualname__}.__init__'
124
125 cls.__init__ = __init__ # type: ignore
126 cls.__pydantic_config__ = config_wrapper.config_dict # type: ignore
127
128 set_dataclass_fields(cls, config_wrapper=config_wrapper, ns_resolver=ns_resolver)
129
130 if not _force_build and config_wrapper.defer_build:
131 set_dataclass_mocks(cls)
132 return False
133
134 if hasattr(cls, '__post_init_post_parse__'):
135 warnings.warn(
136 'Support for `__post_init_post_parse__` has been dropped, the method will not be called',
137 PydanticDeprecatedSince20,
138 )
139
140 typevars_map = get_standard_typevars_map(cls)
141 gen_schema = GenerateSchema(
142 config_wrapper,
143 ns_resolver=ns_resolver,
144 typevars_map=typevars_map,
145 )
146
147 # set __signature__ attr only for the class, but not for its instances
148 # (because instances can define `__call__`, and `inspect.signature` shouldn't
149 # use the `__signature__` attribute and instead generate from `__call__`).
150 cls.__signature__ = LazyClassAttribute(
151 '__signature__',
152 partial(
153 generate_pydantic_signature,
154 # It's important that we reference the `original_init` here,
155 # as it is the one synthesized by the stdlib `dataclass` module:
156 init=original_init,
157 fields=cls.__pydantic_fields__, # type: ignore
158 validate_by_name=config_wrapper.validate_by_name,
159 extra=config_wrapper.extra,
160 is_dataclass=True,
161 ),
162 )
163
164 try:
165 schema = gen_schema.generate_schema(cls)
166 except PydanticUndefinedAnnotation as e:
167 if raise_errors:
168 raise
169 set_dataclass_mocks(cls, f'`{e.name}`')
170 return False
171
172 core_config = config_wrapper.core_config(title=cls.__name__)
173
174 try:
175 schema = gen_schema.clean_schema(schema)
176 except InvalidSchemaError:
177 set_dataclass_mocks(cls)
178 return False
179
180 # We are about to set all the remaining required properties expected for this cast;
181 # __pydantic_decorators__ and __pydantic_fields__ should already be set
182 cls = cast('type[PydanticDataclass]', cls)
183
184 cls.__pydantic_core_schema__ = schema
185 cls.__pydantic_validator__ = create_schema_validator(
186 schema, cls, cls.__module__, cls.__qualname__, 'dataclass', core_config, config_wrapper.plugin_settings
187 )
188 cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config)
189 cls.__pydantic_complete__ = True
190 return True
191
192
193def is_stdlib_dataclass(cls: type[Any], /) -> TypeIs[type[StandardDataclass]]:
194 """Returns `True` if the class is a stdlib dataclass and *not* a Pydantic dataclass.
195
196 Unlike the stdlib `dataclasses.is_dataclass()` function, this does *not* include subclasses
197 of a dataclass that are themselves not dataclasses.
198
199 Args:
200 cls: The class.
201
202 Returns:
203 `True` if the class is a stdlib dataclass, `False` otherwise.
204 """
205 return '__dataclass_fields__' in cls.__dict__ and not hasattr(cls, '__pydantic_validator__')
206
207
208def as_dataclass_field(pydantic_field: FieldInfo) -> dataclasses.Field[Any]:
209 field_args: dict[str, Any] = {'default': pydantic_field}
210
211 # Needed because if `doc` is set, the dataclass slots will be a dict (field name -> doc) instead of a tuple:
212 if sys.version_info >= (3, 14) and pydantic_field.description is not None:
213 field_args['doc'] = pydantic_field.description
214
215 # Needed as the stdlib dataclass module processes kw_only in a specific way during class construction:
216 if sys.version_info >= (3, 10) and pydantic_field.kw_only:
217 field_args['kw_only'] = True
218
219 # Needed as the stdlib dataclass modules generates `__repr__()` during class construction:
220 if pydantic_field.repr is not True:
221 field_args['repr'] = pydantic_field.repr
222
223 return dataclasses.field(**field_args)
224
225
226DcFields: TypeAlias = dict[str, dataclasses.Field[Any]]
227
228
229@contextmanager
230def patch_base_fields(cls: type[Any]) -> Generator[None]:
231 """Temporarily patch the stdlib dataclasses bases of `cls` if the Pydantic `Field()` function is used.
232
233 When creating a Pydantic dataclass, it is possible to inherit from stdlib dataclasses, where
234 the Pydantic `Field()` function is used. To create this Pydantic dataclass, we first apply
235 the stdlib `@dataclass` decorator on it. During the construction of the stdlib dataclass,
236 the `kw_only` and `repr` field arguments need to be understood by the stdlib *during* the
237 dataclass construction. To do so, we temporarily patch the fields dictionary of the affected
238 bases.
239
240 For instance, with the following example:
241
242 ```python {test="skip" lint="skip"}
243 import dataclasses as stdlib_dc
244
245 import pydantic
246 import pydantic.dataclasses as pydantic_dc
247
248 @stdlib_dc.dataclass
249 class A:
250 a: int = pydantic.Field(repr=False)
251
252 # Notice that the `repr` attribute of the dataclass field is `True`:
253 A.__dataclass_fields__['a']
254 #> dataclass.Field(default=FieldInfo(repr=False), repr=True, ...)
255
256 @pydantic_dc.dataclass
257 class B(A):
258 b: int = pydantic.Field(repr=False)
259 ```
260
261 When passing `B` to the stdlib `@dataclass` decorator, it will look for fields in the parent classes
262 and reuse them directly. When this context manager is active, `A` will be temporarily patched to be
263 equivalent to:
264
265 ```python {test="skip" lint="skip"}
266 @stdlib_dc.dataclass
267 class A:
268 a: int = stdlib_dc.field(default=Field(repr=False), repr=False)
269 ```
270
271 !!! note
272 This is only applied to the bases of `cls`, and not `cls` itself. The reason is that the Pydantic
273 dataclass decorator "owns" `cls` (in the previous example, `B`). As such, we instead modify the fields
274 directly (in the previous example, we simply do `setattr(B, 'b', as_dataclass_field(pydantic_field))`).
275
276 !!! note
277 This approach is far from ideal, and can probably be the source of unwanted side effects/race conditions.
278 The previous implemented approach was mutating the `__annotations__` dict of `cls`, which is no longer a
279 safe operation in Python 3.14+, and resulted in unexpected behavior with field ordering anyway.
280 """
281 # A list of two-tuples, the first element being a reference to the
282 # dataclass fields dictionary, the second element being a mapping between
283 # the field names that were modified, and their original `Field`:
284 original_fields_list: list[tuple[DcFields, DcFields]] = []
285
286 for base in cls.__mro__[1:]:
287 dc_fields: dict[str, dataclasses.Field[Any]] = base.__dict__.get('__dataclass_fields__', {})
288 dc_fields_with_pydantic_field_defaults = {
289 field_name: field
290 for field_name, field in dc_fields.items()
291 if isinstance(field.default, FieldInfo)
292 # Only do the patching if one of the affected attributes is set:
293 and (field.default.description is not None or field.default.kw_only or field.default.repr is not True)
294 }
295 if dc_fields_with_pydantic_field_defaults:
296 original_fields_list.append((dc_fields, dc_fields_with_pydantic_field_defaults))
297 for field_name, field in dc_fields_with_pydantic_field_defaults.items():
298 default = cast(FieldInfo, field.default)
299 # `dataclasses.Field` isn't documented as working with `copy.copy()`.
300 # It is a class with `__slots__`, so should work (and we hope for the best):
301 new_dc_field = copy.copy(field)
302 # For base fields, no need to set `doc` from `FieldInfo.description`, this is only relevant
303 # for the class under construction and handled in `as_dataclass_field()`.
304 if sys.version_info >= (3, 10) and default.kw_only:
305 new_dc_field.kw_only = True
306 if default.repr is not True:
307 new_dc_field.repr = default.repr
308 dc_fields[field_name] = new_dc_field
309
310 try:
311 yield
312 finally:
313 for fields, original_fields in original_fields_list:
314 for field_name, original_field in original_fields.items():
315 fields[field_name] = original_field