Coverage for pydantic/_internal/_dataclasses.py: 97.52%

97 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-20 16:49 +0000

1"""Private logic for creating pydantic dataclasses.""" 

2 

3from __future__ import annotations as _annotations 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

4 

5import copy 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

6import dataclasses 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

7import sys 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

8import typing 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

9import warnings 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

10from collections.abc import Generator 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

11from contextlib import contextmanager 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

12from functools import partial 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

13from typing import Any, ClassVar, cast 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

14 

15from pydantic_core import ( 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

16 ArgsKwargs, 

17 SchemaSerializer, 

18 SchemaValidator, 

19 core_schema, 

20) 

21from typing_extensions import TypeAlias, TypeIs 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

22 

23from ..errors import PydanticUndefinedAnnotation 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

24from ..fields import FieldInfo 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

25from ..plugin._schema_validator import PluggableSchemaValidator, create_schema_validator 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

26from ..warnings import PydanticDeprecatedSince20 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

27from . import _config, _decorators 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

28from ._fields import collect_dataclass_fields 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

29from ._generate_schema import GenerateSchema, InvalidSchemaError 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

30from ._generics import get_standard_typevars_map 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

31from ._mock_val_ser import set_dataclass_mocks 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

32from ._namespace_utils import NsResolver 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

33from ._signature import generate_pydantic_signature 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

34from ._utils import LazyClassAttribute 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

35 

36if typing.TYPE_CHECKING: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

37 from _typeshed import DataclassInstance as StandardDataclass 

38 

39 from ..config import ConfigDict 

40 

41 class PydanticDataclass(StandardDataclass, typing.Protocol): 1k

42 """A protocol containing attributes only available once a class has been decorated as a Pydantic dataclass. 

43 

44 Attributes: 

45 __pydantic_config__: Pydantic-specific configuration settings for the dataclass. 

46 __pydantic_complete__: Whether dataclass building is completed, or if there are still undefined fields. 

47 __pydantic_core_schema__: The pydantic-core schema used to build the SchemaValidator and SchemaSerializer. 

48 __pydantic_decorators__: Metadata containing the decorators defined on the dataclass. 

49 __pydantic_fields__: Metadata about the fields defined on the dataclass. 

50 __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the dataclass. 

51 __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the dataclass. 

52 """ 

53 

54 __pydantic_config__: ClassVar[ConfigDict] 

55 __pydantic_complete__: ClassVar[bool] 

56 __pydantic_core_schema__: ClassVar[core_schema.CoreSchema] 

57 __pydantic_decorators__: ClassVar[_decorators.DecoratorInfos] 

58 __pydantic_fields__: ClassVar[dict[str, FieldInfo]] 

59 __pydantic_serializer__: ClassVar[SchemaSerializer] 

60 __pydantic_validator__: ClassVar[SchemaValidator | PluggableSchemaValidator] 

61 

62 @classmethod 

63 def __pydantic_fields_complete__(cls) -> bool: ... 

64 

65else: 

66 # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915 

67 # and https://youtrack.jetbrains.com/issue/PY-51428 

68 DeprecationWarning = PydanticDeprecatedSince20 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

69 

70 

71def set_dataclass_fields( 1FGabcdefghijMHIlmnopqrstuNPJKvwxyzABCDEO

72 cls: type[StandardDataclass], 

73 config_wrapper: _config.ConfigWrapper, 

74 ns_resolver: NsResolver | None = None, 

75) -> None: 

76 """Collect and set `cls.__pydantic_fields__`. 

77 

78 Args: 

79 cls: The class. 

80 config_wrapper: The config wrapper instance. 

81 ns_resolver: Namespace resolver to use when getting dataclass annotations. 

82 """ 

83 typevars_map = get_standard_typevars_map(cls) 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

84 fields = collect_dataclass_fields( 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

85 cls, ns_resolver=ns_resolver, typevars_map=typevars_map, config_wrapper=config_wrapper 

86 ) 

87 

88 cls.__pydantic_fields__ = fields # type: ignore 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

89 

90 

91def complete_dataclass( 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

92 cls: type[Any], 

93 config_wrapper: _config.ConfigWrapper, 

94 *, 

95 raise_errors: bool = True, 

96 ns_resolver: NsResolver | None = None, 

97 _force_build: bool = False, 

98) -> bool: 

99 """Finish building a pydantic dataclass. 

100 

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

102 

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

104 

105 Args: 

106 cls: The class. 

107 config_wrapper: The config wrapper instance. 

108 raise_errors: Whether to raise errors, defaults to `True`. 

109 ns_resolver: The namespace resolver instance to use when collecting dataclass fields 

110 and during schema building. 

111 _force_build: Whether to force building the dataclass, no matter if 

112 [`defer_build`][pydantic.config.ConfigDict.defer_build] is set. 

113 

114 Returns: 

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

116 

117 Raises: 

118 PydanticUndefinedAnnotation: If `raise_error` is `True` and there is an undefined annotations. 

119 """ 

120 original_init = cls.__init__ 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

121 

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

123 # and so that the mock validator is used if building was deferred: 

124 def __init__(__dataclass_self__: PydanticDataclass, *args: Any, **kwargs: Any) -> None: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

125 __tracebackhide__ = True 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

126 s = __dataclass_self__ 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

127 s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s) 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

128 

129 __init__.__qualname__ = f'{cls.__qualname__}.__init__' 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

130 

131 cls.__init__ = __init__ # type: ignore 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

132 cls.__pydantic_config__ = config_wrapper.config_dict # type: ignore 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

133 

134 set_dataclass_fields(cls, config_wrapper=config_wrapper, ns_resolver=ns_resolver) 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

135 

136 if not _force_build and config_wrapper.defer_build: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

137 set_dataclass_mocks(cls) 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

138 return False 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

139 

140 if hasattr(cls, '__post_init_post_parse__'): 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

141 warnings.warn( 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

142 'Support for `__post_init_post_parse__` has been dropped, the method will not be called', DeprecationWarning 

143 ) 

144 

145 typevars_map = get_standard_typevars_map(cls) 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

146 gen_schema = GenerateSchema( 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

147 config_wrapper, 

148 ns_resolver=ns_resolver, 

149 typevars_map=typevars_map, 

150 ) 

151 

152 # set __signature__ attr only for the class, but not for its instances 

153 # (because instances can define `__call__`, and `inspect.signature` shouldn't 

154 # use the `__signature__` attribute and instead generate from `__call__`). 

155 cls.__signature__ = LazyClassAttribute( 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

156 '__signature__', 

157 partial( 

158 generate_pydantic_signature, 

159 # It's important that we reference the `original_init` here, 

160 # as it is the one synthesized by the stdlib `dataclass` module: 

161 init=original_init, 

162 fields=cls.__pydantic_fields__, # type: ignore 

163 validate_by_name=config_wrapper.validate_by_name, 

164 extra=config_wrapper.extra, 

165 is_dataclass=True, 

166 ), 

167 ) 

168 

169 try: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

170 schema = gen_schema.generate_schema(cls) 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

171 except PydanticUndefinedAnnotation as e: 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

172 if raise_errors: 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

173 raise 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE

174 set_dataclass_mocks(cls, f'`{e.name}`') 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

175 return False 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

176 

177 core_config = config_wrapper.core_config(title=cls.__name__) 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

178 

179 try: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

180 schema = gen_schema.clean_schema(schema) 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

181 except InvalidSchemaError: 

182 set_dataclass_mocks(cls) 

183 return False 

184 

185 # We are about to set all the remaining required properties expected for this cast; 

186 # __pydantic_decorators__ and __pydantic_fields__ should already be set 

187 cls = typing.cast('type[PydanticDataclass]', cls) 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

188 

189 cls.__pydantic_core_schema__ = schema 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

190 cls.__pydantic_validator__ = create_schema_validator( 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

191 schema, cls, cls.__module__, cls.__qualname__, 'dataclass', core_config, config_wrapper.plugin_settings 

192 ) 

193 cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config) 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

194 cls.__pydantic_complete__ = True 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

195 return True 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

196 

197 

198def is_stdlib_dataclass(cls: type[Any], /) -> TypeIs[type[StandardDataclass]]: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

199 """Returns `True` if the class is a stdlib dataclass and *not* a Pydantic dataclass. 

200 

201 Unlike the stdlib `dataclasses.is_dataclass()` function, this does *not* include subclasses 

202 of a dataclass that are themselves not dataclasses. 

203 

204 Args: 

205 cls: The class. 

206 

207 Returns: 

208 `True` if the class is a stdlib dataclass, `False` otherwise. 

209 """ 

210 return '__dataclass_fields__' in cls.__dict__ and not hasattr(cls, '__pydantic_validator__') 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

211 

212 

213def as_dataclass_field(pydantic_field: FieldInfo) -> dataclasses.Field[Any]: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

214 field_args: dict[str, Any] = {'default': pydantic_field} 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

215 

216 if sys.version_info >= (3, 10) and pydantic_field.kw_only: 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

217 field_args['kw_only'] = True 1abcdefghijMklmnopqrstuNvwxyzABCDEO

218 

219 if pydantic_field.repr is not True: 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

220 field_args['repr'] = pydantic_field.repr 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

221 

222 return dataclasses.field(**field_args) 1FGabcdefghijMLkHIlmnopqrstuNJKvwxyzABCDEO

223 

224 

225DcFields: TypeAlias = dict[str, dataclasses.Field[Any]] 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

226 

227 

228@contextmanager 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

229def patch_base_fields(cls: type[Any]) -> Generator[None]: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

230 """Temporarily patch the stdlib dataclasses bases of `cls` if the Pydantic `Field()` function is used. 

231 

232 When creating a Pydantic dataclass, it is possible to inherit from stdlib dataclasses, where 

233 the Pydantic `Field()` function is used. To create this Pydantic dataclass, we first apply 

234 the stdlib `@dataclass` decorator on it. During the construction of the stdlib dataclass, 

235 the `kw_only` and `repr` field arguments need to be understood by the stdlib *during* the 

236 dataclass construction. To do so, we temporarily patch the fields dictionary of the affected 

237 bases. 

238 

239 For instance, with the following example: 

240 

241 ```python {test="skip" lint="skip"} 

242 import dataclasses as stdlib_dc 

243 

244 import pydantic 

245 import pydantic.dataclasses as pydantic_dc 

246 

247 @stdlib_dc.dataclass 

248 class A: 

249 a: int = pydantic.Field(repr=False) 

250 

251 # Notice that the `repr` attribute of the dataclass field is `True`: 

252 A.__dataclass_fields__['a'] 

253 #> dataclass.Field(default=FieldInfo(repr=False), repr=True, ...) 

254 

255 @pydantic_dc.dataclass 

256 class B(A): 

257 b: int = pydantic.Field(repr=False) 

258 ``` 

259 

260 When passing `B` to the stdlib `@dataclass` decorator, it will look for fields in the parent classes 

261 and reuse them directly. When this context manager is active, `A` will be temporarily patched to be 

262 equivalent to: 

263 

264 ```python {test="skip" lint="skip"} 

265 @stdlib_dc.dataclass 

266 class A: 

267 a: int = stdlib_dc.field(default=Field(repr=False), repr=False) 

268 ``` 

269 

270 !!! note 

271 This is only applied to the bases of `cls`, and not `cls` itself. The reason is that the Pydantic 

272 dataclass decorator "owns" `cls` (in the previous example, `B`). As such, we instead modify the fields 

273 directly (in the previous example, we simply do `setattr(B, 'b', as_dataclass_field(pydantic_field))`). 

274 

275 !!! note 

276 This approach is far from ideal, and can probably be the source of unwanted side effects/race conditions. 

277 The previous implemented approach was mutating the `__annotations__` dict of `cls`, which is no longer a 

278 safe operation in Python 3.14+, and resulted in unexpected behavior with field ordering anyway. 

279 """ 

280 # A list of two-tuples, the first element being a reference to the 

281 # dataclass fields dictionary, the second element being a mapping between 

282 # the field names that were modified, and their original `Field`: 

283 original_fields_list: list[tuple[DcFields, DcFields]] = [] 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

284 

285 for base in cls.__mro__[1:]: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

286 dc_fields: dict[str, dataclasses.Field[Any]] = base.__dict__.get('__dataclass_fields__', {}) 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

287 dc_fields_with_pydantic_field_defaults = { 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

288 field_name: field 

289 for field_name, field in dc_fields.items() 

290 if isinstance(field.default, FieldInfo) 

291 # Only do the patching if one of the affected attributes is set: 

292 and (field.default.kw_only or field.default.repr is not True) 

293 } 

294 if dc_fields_with_pydantic_field_defaults: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

295 original_fields_list.append((dc_fields, dc_fields_with_pydantic_field_defaults)) 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE

296 for field_name, field in dc_fields_with_pydantic_field_defaults.items(): 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE

297 default = cast(FieldInfo, field.default) 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE

298 # `dataclasses.Field` isn't documented as working with `copy.copy()`. 

299 # It is a class with `__slots__`, so should work (and we hope for the best): 

300 new_dc_field = copy.copy(field) 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE

301 if default.kw_only: 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE

302 new_dc_field.kw_only = True 1abcdefghijklmnopqrstuvwxyzABCDE

303 if default.repr is not True: 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE

304 new_dc_field.repr = default.repr 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE

305 dc_fields[field_name] = new_dc_field 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE

306 

307 try: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

308 yield 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

309 finally: 

310 for fields, original_fields in original_fields_list: 1FGabcdefghijMLkHIlmnopqrstuNPJKvwxyzABCDEO

311 for field_name, original_field in original_fields.items(): 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE

312 fields[field_name] = original_field 1FGabcdefghijLkHIlmnopqrstuJKvwxyzABCDE