forked from pydantic/pydantic
/
dataclasses.py
320 lines (267 loc) · 12.6 KB
/
dataclasses.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
"""Provide an enhanced dataclass that performs validation."""
from __future__ import annotations as _annotations
import dataclasses
import sys
import types
from typing import TYPE_CHECKING, Any, Callable, Generic, NoReturn, TypeVar, overload
from typing_extensions import Literal, TypeGuard, dataclass_transform
from ._internal import _config, _decorators, _typing_extra
from ._internal import _dataclasses as _pydantic_dataclasses
from ._migration import getattr_migration
from .config import ConfigDict
from .fields import Field, FieldInfo
if TYPE_CHECKING:
from ._internal._dataclasses import PydanticDataclass
__all__ = 'dataclass', 'rebuild_dataclass'
_T = TypeVar('_T')
if sys.version_info >= (3, 10):
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
@overload
def dataclass(
*,
init: Literal[False] = False,
repr: bool = True,
eq: bool = True,
order: bool = False,
unsafe_hash: bool = False,
frozen: bool = False,
config: ConfigDict | type[object] | None = None,
validate_on_init: bool | None = None,
kw_only: bool = ...,
slots: bool = ...,
) -> Callable[[type[_T]], type[PydanticDataclass]]: # type: ignore
...
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
@overload
def dataclass(
_cls: type[_T], # type: ignore
*,
init: Literal[False] = False,
repr: bool = True,
eq: bool = True,
order: bool = False,
unsafe_hash: bool = False,
frozen: bool = False,
config: ConfigDict | type[object] | None = None,
validate_on_init: bool | None = None,
kw_only: bool = ...,
slots: bool = ...,
) -> type[PydanticDataclass]:
...
else:
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
@overload
def dataclass(
*,
init: Literal[False] = False,
repr: bool = True,
eq: bool = True,
order: bool = False,
unsafe_hash: bool = False,
frozen: bool = False,
config: ConfigDict | type[object] | None = None,
validate_on_init: bool | None = None,
) -> Callable[[type[_T]], type[PydanticDataclass]]: # type: ignore
...
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
@overload
def dataclass(
_cls: type[_T], # type: ignore
*,
init: Literal[False] = False,
repr: bool = True,
eq: bool = True,
order: bool = False,
unsafe_hash: bool = False,
frozen: bool = False,
config: ConfigDict | type[object] | None = None,
validate_on_init: bool | None = None,
) -> type[PydanticDataclass]:
...
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
def dataclass(
_cls: type[_T] | None = None,
*,
init: Literal[False] = False,
repr: bool = True,
eq: bool = True,
order: bool = False,
unsafe_hash: bool = False,
frozen: bool = False,
config: ConfigDict | type[object] | None = None,
validate_on_init: bool | None = None,
kw_only: bool = False,
slots: bool = False,
) -> Callable[[type[_T]], type[PydanticDataclass]] | type[PydanticDataclass]:
"""Usage docs: https://docs.pydantic.dev/2.6/concepts/dataclasses/
A decorator used to create a Pydantic-enhanced dataclass, similar to the standard Python `dataclass`,
but with added validation.
This function should be used similarly to `dataclasses.dataclass`.
Args:
_cls: The target `dataclass`.
init: Included for signature compatibility with `dataclasses.dataclass`, and is passed through to
`dataclasses.dataclass` when appropriate. If specified, must be set to `False`, as pydantic inserts its
own `__init__` function.
repr: A boolean indicating whether to include the field in the `__repr__` output.
eq: Determines if a `__eq__` method should be generated for the class.
order: Determines if comparison magic methods should be generated, such as `__lt__`, but not `__eq__`.
unsafe_hash: Determines if a `__hash__` method should be included in the class, as in `dataclasses.dataclass`.
frozen: Determines if the generated class should be a 'frozen' `dataclass`, which does not allow its
attributes to be modified after it has been initialized.
config: The Pydantic config to use for the `dataclass`.
validate_on_init: A deprecated parameter included for backwards compatibility; in V2, all Pydantic dataclasses
are validated on init.
kw_only: Determines if `__init__` method parameters must be specified by keyword only. Defaults to `False`.
slots: Determines if the generated class should be a 'slots' `dataclass`, which does not allow the addition of
new attributes after instantiation.
Returns:
A decorator that accepts a class as its argument and returns a Pydantic `dataclass`.
Raises:
AssertionError: Raised if `init` is not `False` or `validate_on_init` is `False`.
"""
assert init is False, 'pydantic.dataclasses.dataclass only supports init=False'
assert validate_on_init is not False, 'validate_on_init=False is no longer supported'
if sys.version_info >= (3, 10):
kwargs = dict(kw_only=kw_only, slots=slots)
else:
kwargs = {}
def make_pydantic_fields_compatible(cls: type[Any]) -> None:
"""Make sure that stdlib `dataclasses` understands `Field` kwargs like `kw_only`
To do that, we simply change
`x: int = pydantic.Field(..., kw_only=True)`
into
`x: int = dataclasses.field(default=pydantic.Field(..., kw_only=True), kw_only=True)`
"""
# In Python < 3.9, `__annotations__` might not be present if there are no fields.
# we therefore need to use `getattr` to avoid an `AttributeError`.
for field_name in getattr(cls, '__annotations__', []):
field_value = getattr(cls, field_name, None)
# Process only if this is an instance of `FieldInfo`.
if not isinstance(field_value, FieldInfo):
continue
# Initialize arguments for the standard `dataclasses.field`.
field_args: dict = {'default': field_value}
# Handle `kw_only` for Python 3.10+
if sys.version_info >= (3, 10) and field_value.kw_only:
field_args['kw_only'] = True
# Set `repr` attribute if it's explicitly specified to be not `True`.
if field_value.repr is not True:
field_args['repr'] = field_value.repr
setattr(cls, field_name, dataclasses.field(**field_args))
def create_dataclass(cls: type[Any]) -> type[PydanticDataclass]:
"""Create a Pydantic dataclass from a regular dataclass.
Args:
cls: The class to create the Pydantic dataclass from.
Returns:
A Pydantic dataclass.
"""
original_cls = cls
config_dict = config
if config_dict is None:
# if not explicitly provided, read from the type
cls_config = getattr(cls, '__pydantic_config__', None)
if cls_config is not None:
config_dict = cls_config
config_wrapper = _config.ConfigWrapper(config_dict)
decorators = _decorators.DecoratorInfos.build(cls)
# Keep track of the original __doc__ so that we can restore it after applying the dataclasses decorator
# Otherwise, classes with no __doc__ will have their signature added into the JSON schema description,
# since dataclasses.dataclass will set this as the __doc__
original_doc = cls.__doc__
if _pydantic_dataclasses.is_builtin_dataclass(cls):
# Don't preserve the docstring for vanilla dataclasses, as it may include the signature
# This matches v1 behavior, and there was an explicit test for it
original_doc = None
# We don't want to add validation to the existing std lib dataclass, so we will subclass it
# If the class is generic, we need to make sure the subclass also inherits from Generic
# with all the same parameters.
bases = (cls,)
if issubclass(cls, Generic):
generic_base = Generic[cls.__parameters__] # type: ignore
bases = bases + (generic_base,)
cls = types.new_class(cls.__name__, bases)
make_pydantic_fields_compatible(cls)
cls = dataclasses.dataclass( # type: ignore[call-overload]
cls,
# 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,
unsafe_hash=unsafe_hash,
frozen=frozen,
**kwargs,
)
cls.__pydantic_decorators__ = decorators # type: ignore
cls.__doc__ = original_doc
cls.__module__ = original_cls.__module__
cls.__qualname__ = original_cls.__qualname__
pydantic_complete = _pydantic_dataclasses.complete_dataclass(
cls, config_wrapper, raise_errors=False, types_namespace=None
)
cls.__pydantic_complete__ = pydantic_complete # type: ignore
return cls
if _cls is None:
return create_dataclass
return create_dataclass(_cls)
__getattr__ = getattr_migration(__name__)
if (3, 8) <= sys.version_info < (3, 11):
# Monkeypatch dataclasses.InitVar so that typing doesn't error if it occurs as a type when evaluating type hints
# Starting in 3.11, typing.get_type_hints will not raise an error if the retrieved type hints are not callable.
def _call_initvar(*args: Any, **kwargs: Any) -> NoReturn:
"""This function does nothing but raise an error that is as similar as possible to what you'd get
if you were to try calling `InitVar[int]()` without this monkeypatch. The whole purpose is just
to ensure typing._type_check does not error if the type hint evaluates to `InitVar[<parameter>]`.
"""
raise TypeError("'InitVar' object is not callable")
dataclasses.InitVar.__call__ = _call_initvar
def rebuild_dataclass(
cls: type[PydanticDataclass],
*,
force: bool = False,
raise_errors: bool = True,
_parent_namespace_depth: int = 2,
_types_namespace: dict[str, Any] | None = None,
) -> bool | None:
"""Try to rebuild the pydantic-core schema for the dataclass.
This may be necessary when one of the annotations is a ForwardRef which could not be resolved during
the initial attempt to build the schema, and automatic rebuilding fails.
This is analogous to `BaseModel.model_rebuild`.
Args:
cls: The class to rebuild the pydantic-core schema for.
force: Whether to force the rebuilding of the schema, defaults to `False`.
raise_errors: Whether to raise errors, defaults to `True`.
_parent_namespace_depth: The depth level of the parent namespace, defaults to 2.
_types_namespace: The types namespace, defaults to `None`.
Returns:
Returns `None` if the schema is already "complete" and rebuilding was not required.
If rebuilding _was_ required, returns `True` if rebuilding was successful, otherwise `False`.
"""
if not force and cls.__pydantic_complete__:
return None
else:
if _types_namespace is not None:
types_namespace: dict[str, Any] | None = _types_namespace.copy()
else:
if _parent_namespace_depth > 0:
frame_parent_ns = _typing_extra.parent_frame_namespace(parent_depth=_parent_namespace_depth) or {}
# Note: we may need to add something similar to cls.__pydantic_parent_namespace__ from BaseModel
# here when implementing handling of recursive generics. See BaseModel.model_rebuild for reference.
types_namespace = frame_parent_ns
else:
types_namespace = {}
types_namespace = _typing_extra.get_cls_types_namespace(cls, types_namespace)
return _pydantic_dataclasses.complete_dataclass(
cls,
_config.ConfigWrapper(cls.__pydantic_config__, check=False),
raise_errors=raise_errors,
types_namespace=types_namespace,
)
def is_pydantic_dataclass(__cls: type[Any]) -> TypeGuard[type[PydanticDataclass]]:
"""Whether a class is a pydantic dataclass.
Args:
__cls: The class.
Returns:
`True` if the class is a pydantic dataclass, `False` otherwise.
"""
return dataclasses.is_dataclass(__cls) and '__pydantic_validator__' in __cls.__dict__