1"""Private logic for creating pydantic dataclasses."""
2
3from __future__ import annotations as _annotations
4
5import dataclasses
6import typing
7import warnings
8from functools import partial, wraps
9from typing import Any, ClassVar
10
11from pydantic_core import (
12 ArgsKwargs,
13 SchemaSerializer,
14 SchemaValidator,
15 core_schema,
16)
17from typing_extensions import TypeGuard
18
19from ..errors import PydanticUndefinedAnnotation
20from ..plugin._schema_validator import PluggableSchemaValidator, create_schema_validator
21from ..warnings import PydanticDeprecatedSince20
22from . import _config, _decorators
23from ._fields import collect_dataclass_fields
24from ._generate_schema import GenerateSchema, InvalidSchemaError
25from ._generics import get_standard_typevars_map
26from ._mock_val_ser import set_dataclass_mocks
27from ._namespace_utils import NsResolver
28from ._signature import generate_pydantic_signature
29from ._utils import LazyClassAttribute
30
31if typing.TYPE_CHECKING:
32 from _typeshed import DataclassInstance as StandardDataclass
33
34 from ..config import ConfigDict
35 from ..fields import FieldInfo
36
37 class PydanticDataclass(StandardDataclass, typing.Protocol):
38 """A protocol containing attributes only available once a class has been decorated as a Pydantic dataclass.
39
40 Attributes:
41 __pydantic_config__: Pydantic-specific configuration settings for the dataclass.
42 __pydantic_complete__: Whether dataclass building is completed, or if there are still undefined fields.
43 __pydantic_core_schema__: The pydantic-core schema used to build the SchemaValidator and SchemaSerializer.
44 __pydantic_decorators__: Metadata containing the decorators defined on the dataclass.
45 __pydantic_fields__: Metadata about the fields defined on the dataclass.
46 __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the dataclass.
47 __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the dataclass.
48 """
49
50 __pydantic_config__: ClassVar[ConfigDict]
51 __pydantic_complete__: ClassVar[bool]
52 __pydantic_core_schema__: ClassVar[core_schema.CoreSchema]
53 __pydantic_decorators__: ClassVar[_decorators.DecoratorInfos]
54 __pydantic_fields__: ClassVar[dict[str, FieldInfo]]
55 __pydantic_serializer__: ClassVar[SchemaSerializer]
56 __pydantic_validator__: ClassVar[SchemaValidator | PluggableSchemaValidator]
57
58 @classmethod
59 def __pydantic_fields_complete__(cls) -> bool: ...
60
61else:
62 # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915
63 # and https://youtrack.jetbrains.com/issue/PY-51428
64 DeprecationWarning = PydanticDeprecatedSince20
65
66
67def set_dataclass_fields(
68 cls: type[StandardDataclass],
69 ns_resolver: NsResolver | None = None,
70 config_wrapper: _config.ConfigWrapper | None = None,
71) -> None:
72 """Collect and set `cls.__pydantic_fields__`.
73
74 Args:
75 cls: The class.
76 ns_resolver: Namespace resolver to use when getting dataclass annotations.
77 config_wrapper: The config wrapper instance, defaults to `None`.
78 """
79 typevars_map = get_standard_typevars_map(cls)
80 fields = collect_dataclass_fields(
81 cls, ns_resolver=ns_resolver, typevars_map=typevars_map, config_wrapper=config_wrapper
82 )
83
84 cls.__pydantic_fields__ = fields # type: ignore
85
86
87def complete_dataclass(
88 cls: type[Any],
89 config_wrapper: _config.ConfigWrapper,
90 *,
91 raise_errors: bool = True,
92 ns_resolver: NsResolver | None = None,
93 _force_build: bool = False,
94) -> bool:
95 """Finish building a pydantic dataclass.
96
97 This logic is called on a class which has already been wrapped in `dataclasses.dataclass()`.
98
99 This is somewhat analogous to `pydantic._internal._model_construction.complete_model_class`.
100
101 Args:
102 cls: The class.
103 config_wrapper: The config wrapper instance.
104 raise_errors: Whether to raise errors, defaults to `True`.
105 ns_resolver: The namespace resolver instance to use when collecting dataclass fields
106 and during schema building.
107 _force_build: Whether to force building the dataclass, no matter if
108 [`defer_build`][pydantic.config.ConfigDict.defer_build] is set.
109
110 Returns:
111 `True` if building a pydantic dataclass is successfully completed, `False` otherwise.
112
113 Raises:
114 PydanticUndefinedAnnotation: If `raise_error` is `True` and there is an undefined annotations.
115 """
116 original_init = cls.__init__
117
118 # dataclass.__init__ must be defined here so its `__qualname__` can be changed since functions can't be copied,
119 # and so that the mock validator is used if building was deferred:
120 def __init__(__dataclass_self__: PydanticDataclass, *args: Any, **kwargs: Any) -> None:
121 __tracebackhide__ = True
122 s = __dataclass_self__
123 s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)
124
125 __init__.__qualname__ = f'{cls.__qualname__}.__init__'
126
127 cls.__init__ = __init__ # type: ignore
128 cls.__pydantic_config__ = config_wrapper.config_dict # type: ignore
129
130 set_dataclass_fields(cls, ns_resolver, config_wrapper=config_wrapper)
131
132 if not _force_build and config_wrapper.defer_build:
133 set_dataclass_mocks(cls)
134 return False
135
136 if hasattr(cls, '__post_init_post_parse__'):
137 warnings.warn(
138 'Support for `__post_init_post_parse__` has been dropped, the method will not be called', DeprecationWarning
139 )
140
141 typevars_map = get_standard_typevars_map(cls)
142 gen_schema = GenerateSchema(
143 config_wrapper,
144 ns_resolver=ns_resolver,
145 typevars_map=typevars_map,
146 )
147
148 # set __signature__ attr only for the class, but not for its instances
149 # (because instances can define `__call__`, and `inspect.signature` shouldn't
150 # use the `__signature__` attribute and instead generate from `__call__`).
151 cls.__signature__ = LazyClassAttribute(
152 '__signature__',
153 partial(
154 generate_pydantic_signature,
155 # It's important that we reference the `original_init` here,
156 # as it is the one synthesized by the stdlib `dataclass` module:
157 init=original_init,
158 fields=cls.__pydantic_fields__, # type: ignore
159 validate_by_name=config_wrapper.validate_by_name,
160 extra=config_wrapper.extra,
161 is_dataclass=True,
162 ),
163 )
164
165 try:
166 schema = gen_schema.generate_schema(cls)
167 except PydanticUndefinedAnnotation as e:
168 if raise_errors:
169 raise
170 set_dataclass_mocks(cls, f'`{e.name}`')
171 return False
172
173 core_config = config_wrapper.core_config(title=cls.__name__)
174
175 try:
176 schema = gen_schema.clean_schema(schema)
177 except InvalidSchemaError:
178 set_dataclass_mocks(cls)
179 return False
180
181 # We are about to set all the remaining required properties expected for this cast;
182 # __pydantic_decorators__ and __pydantic_fields__ should already be set
183 cls = typing.cast('type[PydanticDataclass]', cls)
184 # debug(schema)
185
186 cls.__pydantic_core_schema__ = schema
187 cls.__pydantic_validator__ = validator = create_schema_validator(
188 schema, cls, cls.__module__, cls.__qualname__, 'dataclass', core_config, config_wrapper.plugin_settings
189 )
190 cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config)
191
192 if config_wrapper.validate_assignment:
193
194 @wraps(cls.__setattr__)
195 def validated_setattr(instance: Any, field: str, value: str, /) -> None:
196 validator.validate_assignment(instance, field, value)
197
198 cls.__setattr__ = validated_setattr.__get__(None, cls) # type: ignore
199
200 cls.__pydantic_complete__ = True
201 return True
202
203
204def is_builtin_dataclass(_cls: type[Any]) -> TypeGuard[type[StandardDataclass]]:
205 """Returns True if a class is a stdlib dataclass and *not* a pydantic dataclass.
206
207 We check that
208 - `_cls` is a dataclass
209 - `_cls` does not inherit from a processed pydantic dataclass (and thus have a `__pydantic_validator__`)
210 - `_cls` does not have any annotations that are not dataclass fields
211 e.g.
212 ```python
213 import dataclasses
214
215 import pydantic.dataclasses
216
217 @dataclasses.dataclass
218 class A:
219 x: int
220
221 @pydantic.dataclasses.dataclass
222 class B(A):
223 y: int
224 ```
225 In this case, when we first check `B`, we make an extra check and look at the annotations ('y'),
226 which won't be a superset of all the dataclass fields (only the stdlib fields i.e. 'x')
227
228 Args:
229 cls: The class.
230
231 Returns:
232 `True` if the class is a stdlib dataclass, `False` otherwise.
233 """
234 return (
235 dataclasses.is_dataclass(_cls)
236 and not hasattr(_cls, '__pydantic_validator__')
237 and set(_cls.__dataclass_fields__).issuperset(set(getattr(_cls, '__annotations__', {})))
238 )