Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pydantic/v1/dataclasses.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

220 statements  

1""" 

2The main purpose is to enhance stdlib dataclasses by adding validation 

3A pydantic dataclass can be generated from scratch or from a stdlib one. 

4 

5Behind the scene, a pydantic dataclass is just like a regular one on which we attach 

6a `BaseModel` and magic methods to trigger the validation of the data. 

7`__init__` and `__post_init__` are hence overridden and have extra logic to be 

8able to validate input data. 

9 

10When a pydantic dataclass is generated from scratch, it's just a plain dataclass 

11with validation triggered at initialization 

12 

13The tricky part if for stdlib dataclasses that are converted after into pydantic ones e.g. 

14 

15```py 

16@dataclasses.dataclass 

17class M: 

18 x: int 

19 

20ValidatedM = pydantic.dataclasses.dataclass(M) 

21``` 

22 

23We indeed still want to support equality, hashing, repr, ... as if it was the stdlib one! 

24 

25```py 

26assert isinstance(ValidatedM(x=1), M) 

27assert ValidatedM(x=1) == M(x=1) 

28``` 

29 

30This means we **don't want to create a new dataclass that inherits from it** 

31The trick is to create a wrapper around `M` that will act as a proxy to trigger 

32validation without altering default `M` behaviour. 

33""" 

34import copy 

35import dataclasses 

36import sys 

37from contextlib import contextmanager 

38from functools import wraps 

39 

40try: 

41 from functools import cached_property 

42except ImportError: 

43 # cached_property available only for python3.8+ 

44 pass 

45 

46from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Generator, Optional, Type, TypeVar, Union, overload 

47 

48from typing_extensions import dataclass_transform 

49 

50from pydantic.v1.class_validators import gather_all_validators 

51from pydantic.v1.config import BaseConfig, ConfigDict, Extra, get_config 

52from pydantic.v1.error_wrappers import ValidationError 

53from pydantic.v1.errors import DataclassTypeError 

54from pydantic.v1.fields import Field, FieldInfo, Required, Undefined 

55from pydantic.v1.main import create_model, validate_model 

56from pydantic.v1.utils import ClassAttribute 

57 

58if TYPE_CHECKING: 

59 from pydantic.v1.main import BaseModel 

60 from pydantic.v1.typing import CallableGenerator, NoArgAnyCallable 

61 

62 DataclassT = TypeVar('DataclassT', bound='Dataclass') 

63 

64 DataclassClassOrWrapper = Union[Type['Dataclass'], 'DataclassProxy'] 

65 

66 class Dataclass: 

67 # stdlib attributes 

68 __dataclass_fields__: ClassVar[Dict[str, Any]] 

69 __dataclass_params__: ClassVar[Any] # in reality `dataclasses._DataclassParams` 

70 __post_init__: ClassVar[Callable[..., None]] 

71 

72 # Added by pydantic 

73 __pydantic_run_validation__: ClassVar[bool] 

74 __post_init_post_parse__: ClassVar[Callable[..., None]] 

75 __pydantic_initialised__: ClassVar[bool] 

76 __pydantic_model__: ClassVar[Type[BaseModel]] 

77 __pydantic_validate_values__: ClassVar[Callable[['Dataclass'], None]] 

78 __pydantic_has_field_info_default__: ClassVar[bool] # whether a `pydantic.Field` is used as default value 

79 

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

81 pass 

82 

83 @classmethod 

84 def __get_validators__(cls: Type['Dataclass']) -> 'CallableGenerator': 

85 pass 

86 

87 @classmethod 

88 def __validate__(cls: Type['DataclassT'], v: Any) -> 'DataclassT': 

89 pass 

90 

91 

92__all__ = [ 

93 'dataclass', 

94 'set_validation', 

95 'create_pydantic_model_from_dataclass', 

96 'is_builtin_dataclass', 

97 'make_dataclass_validator', 

98] 

99 

100_T = TypeVar('_T') 

101 

102if sys.version_info >= (3, 10): 

103 

104 @dataclass_transform(field_specifiers=(dataclasses.field, Field)) 

105 @overload 

106 def dataclass( 

107 *, 

108 init: bool = True, 

109 repr: bool = True, 

110 eq: bool = True, 

111 order: bool = False, 

112 unsafe_hash: bool = False, 

113 frozen: bool = False, 

114 config: Union[ConfigDict, Type[object], None] = None, 

115 validate_on_init: Optional[bool] = None, 

116 use_proxy: Optional[bool] = None, 

117 kw_only: bool = ..., 

118 ) -> Callable[[Type[_T]], 'DataclassClassOrWrapper']: 

119 ... 

120 

121 @dataclass_transform(field_specifiers=(dataclasses.field, Field)) 

122 @overload 

123 def dataclass( 

124 _cls: Type[_T], 

125 *, 

126 init: bool = True, 

127 repr: bool = True, 

128 eq: bool = True, 

129 order: bool = False, 

130 unsafe_hash: bool = False, 

131 frozen: bool = False, 

132 config: Union[ConfigDict, Type[object], None] = None, 

133 validate_on_init: Optional[bool] = None, 

134 use_proxy: Optional[bool] = None, 

135 kw_only: bool = ..., 

136 ) -> 'DataclassClassOrWrapper': 

137 ... 

138 

139else: 

140 

141 @dataclass_transform(field_specifiers=(dataclasses.field, Field)) 

142 @overload 

143 def dataclass( 

144 *, 

145 init: bool = True, 

146 repr: bool = True, 

147 eq: bool = True, 

148 order: bool = False, 

149 unsafe_hash: bool = False, 

150 frozen: bool = False, 

151 config: Union[ConfigDict, Type[object], None] = None, 

152 validate_on_init: Optional[bool] = None, 

153 use_proxy: Optional[bool] = None, 

154 ) -> Callable[[Type[_T]], 'DataclassClassOrWrapper']: 

155 ... 

156 

157 @dataclass_transform(field_specifiers=(dataclasses.field, Field)) 

158 @overload 

159 def dataclass( 

160 _cls: Type[_T], 

161 *, 

162 init: bool = True, 

163 repr: bool = True, 

164 eq: bool = True, 

165 order: bool = False, 

166 unsafe_hash: bool = False, 

167 frozen: bool = False, 

168 config: Union[ConfigDict, Type[object], None] = None, 

169 validate_on_init: Optional[bool] = None, 

170 use_proxy: Optional[bool] = None, 

171 ) -> 'DataclassClassOrWrapper': 

172 ... 

173 

174 

175@dataclass_transform(field_specifiers=(dataclasses.field, Field)) 

176def dataclass( 

177 _cls: Optional[Type[_T]] = None, 

178 *, 

179 init: bool = True, 

180 repr: bool = True, 

181 eq: bool = True, 

182 order: bool = False, 

183 unsafe_hash: bool = False, 

184 frozen: bool = False, 

185 config: Union[ConfigDict, Type[object], None] = None, 

186 validate_on_init: Optional[bool] = None, 

187 use_proxy: Optional[bool] = None, 

188 kw_only: bool = False, 

189) -> Union[Callable[[Type[_T]], 'DataclassClassOrWrapper'], 'DataclassClassOrWrapper']: 

190 """ 

191 Like the python standard lib dataclasses but with type validation. 

192 The result is either a pydantic dataclass that will validate input data 

193 or a wrapper that will trigger validation around a stdlib dataclass 

194 to avoid modifying it directly 

195 """ 

196 the_config = get_config(config) 

197 

198 def wrap(cls: Type[Any]) -> 'DataclassClassOrWrapper': 

199 should_use_proxy = ( 

200 use_proxy 

201 if use_proxy is not None 

202 else ( 

203 is_builtin_dataclass(cls) 

204 and (cls.__bases__[0] is object or set(dir(cls)) == set(dir(cls.__bases__[0]))) 

205 ) 

206 ) 

207 if should_use_proxy: 

208 dc_cls_doc = '' 

209 dc_cls = DataclassProxy(cls) 

210 default_validate_on_init = False 

211 else: 

212 dc_cls_doc = cls.__doc__ or '' # needs to be done before generating dataclass 

213 if sys.version_info >= (3, 10): 

214 dc_cls = dataclasses.dataclass( 

215 cls, 

216 init=init, 

217 repr=repr, 

218 eq=eq, 

219 order=order, 

220 unsafe_hash=unsafe_hash, 

221 frozen=frozen, 

222 kw_only=kw_only, 

223 ) 

224 else: 

225 dc_cls = dataclasses.dataclass( # type: ignore 

226 cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen 

227 ) 

228 default_validate_on_init = True 

229 

230 should_validate_on_init = default_validate_on_init if validate_on_init is None else validate_on_init 

231 _add_pydantic_validation_attributes(cls, the_config, should_validate_on_init, dc_cls_doc) 

232 localns = {cls.__name__: cls} 

233 dc_cls.__pydantic_model__.__try_update_forward_refs__(**localns) 

234 return dc_cls 

235 

236 if _cls is None: 

237 return wrap 

238 

239 return wrap(_cls) 

240 

241 

242@contextmanager 

243def set_validation(cls: Type['DataclassT'], value: bool) -> Generator[Type['DataclassT'], None, None]: 

244 original_run_validation = cls.__pydantic_run_validation__ 

245 try: 

246 cls.__pydantic_run_validation__ = value 

247 yield cls 

248 finally: 

249 cls.__pydantic_run_validation__ = original_run_validation 

250 

251 

252class DataclassProxy: 

253 __slots__ = '__dataclass__' 

254 

255 def __init__(self, dc_cls: Type['Dataclass']) -> None: 

256 object.__setattr__(self, '__dataclass__', dc_cls) 

257 

258 def __call__(self, *args: Any, **kwargs: Any) -> Any: 

259 with set_validation(self.__dataclass__, True): 

260 return self.__dataclass__(*args, **kwargs) 

261 

262 def __getattr__(self, name: str) -> Any: 

263 return getattr(self.__dataclass__, name) 

264 

265 def __setattr__(self, __name: str, __value: Any) -> None: 

266 return setattr(self.__dataclass__, __name, __value) 

267 

268 def __instancecheck__(self, instance: Any) -> bool: 

269 return isinstance(instance, self.__dataclass__) 

270 

271 def __copy__(self) -> 'DataclassProxy': 

272 return DataclassProxy(copy.copy(self.__dataclass__)) 

273 

274 def __deepcopy__(self, memo: Any) -> 'DataclassProxy': 

275 return DataclassProxy(copy.deepcopy(self.__dataclass__, memo)) 

276 

277 

278def _add_pydantic_validation_attributes( # noqa: C901 (ignore complexity) 

279 dc_cls: Type['Dataclass'], 

280 config: Type[BaseConfig], 

281 validate_on_init: bool, 

282 dc_cls_doc: str, 

283) -> None: 

284 """ 

285 We need to replace the right method. If no `__post_init__` has been set in the stdlib dataclass 

286 it won't even exist (code is generated on the fly by `dataclasses`) 

287 By default, we run validation after `__init__` or `__post_init__` if defined 

288 """ 

289 init = dc_cls.__init__ 

290 

291 @wraps(init) 

292 def handle_extra_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None: 

293 if config.extra == Extra.ignore: 

294 init(self, *args, **{k: v for k, v in kwargs.items() if k in self.__dataclass_fields__}) 

295 

296 elif config.extra == Extra.allow: 

297 for k, v in kwargs.items(): 

298 # internal attributes are not valid extra fields, and should not be set from user input: 

299 if not k.startswith('__pydantic'): 

300 self.__dict__.setdefault(k, v) 

301 init(self, *args, **{k: v for k, v in kwargs.items() if k in self.__dataclass_fields__}) 

302 

303 else: 

304 init(self, *args, **kwargs) 

305 

306 if hasattr(dc_cls, '__post_init__'): 

307 try: 

308 post_init = dc_cls.__post_init__.__wrapped__ # type: ignore[attr-defined] 

309 except AttributeError: 

310 post_init = dc_cls.__post_init__ 

311 

312 @wraps(post_init) 

313 def new_post_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None: 

314 if config.post_init_call == 'before_validation': 

315 post_init(self, *args, **kwargs) 

316 

317 if self.__class__.__pydantic_run_validation__: 

318 self.__pydantic_validate_values__() 

319 if hasattr(self, '__post_init_post_parse__'): 

320 self.__post_init_post_parse__(*args, **kwargs) 

321 

322 if config.post_init_call == 'after_validation': 

323 post_init(self, *args, **kwargs) 

324 

325 setattr(dc_cls, '__init__', handle_extra_init) 

326 setattr(dc_cls, '__post_init__', new_post_init) 

327 

328 else: 

329 

330 @wraps(init) 

331 def new_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None: 

332 handle_extra_init(self, *args, **kwargs) 

333 

334 if self.__class__.__pydantic_run_validation__: 

335 self.__pydantic_validate_values__() 

336 

337 if hasattr(self, '__post_init_post_parse__'): 

338 # We need to find again the initvars. To do that we use `__dataclass_fields__` instead of 

339 # public method `dataclasses.fields` 

340 

341 # get all initvars and their default values 

342 initvars_and_values: Dict[str, Any] = {} 

343 for i, f in enumerate(self.__class__.__dataclass_fields__.values()): 

344 if f._field_type is dataclasses._FIELD_INITVAR: # type: ignore[attr-defined] 

345 try: 

346 # set arg value by default 

347 initvars_and_values[f.name] = args[i] 

348 except IndexError: 

349 initvars_and_values[f.name] = kwargs.get(f.name, f.default) 

350 

351 self.__post_init_post_parse__(**initvars_and_values) 

352 

353 setattr(dc_cls, '__init__', new_init) 

354 

355 setattr(dc_cls, '__pydantic_run_validation__', ClassAttribute('__pydantic_run_validation__', validate_on_init)) 

356 setattr(dc_cls, '__pydantic_initialised__', False) 

357 setattr(dc_cls, '__pydantic_model__', create_pydantic_model_from_dataclass(dc_cls, config, dc_cls_doc)) 

358 setattr(dc_cls, '__pydantic_validate_values__', _dataclass_validate_values) 

359 setattr(dc_cls, '__validate__', classmethod(_validate_dataclass)) 

360 setattr(dc_cls, '__get_validators__', classmethod(_get_validators)) 

361 

362 if dc_cls.__pydantic_model__.__config__.validate_assignment and not dc_cls.__dataclass_params__.frozen: 

363 setattr(dc_cls, '__setattr__', _dataclass_validate_assignment_setattr) 

364 

365 

366def _get_validators(cls: 'DataclassClassOrWrapper') -> 'CallableGenerator': 

367 yield cls.__validate__ 

368 

369 

370def _validate_dataclass(cls: Type['DataclassT'], v: Any) -> 'DataclassT': 

371 with set_validation(cls, True): 

372 if isinstance(v, cls): 

373 v.__pydantic_validate_values__() 

374 return v 

375 elif isinstance(v, (list, tuple)): 

376 return cls(*v) 

377 elif isinstance(v, dict): 

378 return cls(**v) 

379 else: 

380 raise DataclassTypeError(class_name=cls.__name__) 

381 

382 

383def create_pydantic_model_from_dataclass( 

384 dc_cls: Type['Dataclass'], 

385 config: Type[Any] = BaseConfig, 

386 dc_cls_doc: Optional[str] = None, 

387) -> Type['BaseModel']: 

388 field_definitions: Dict[str, Any] = {} 

389 for field in dataclasses.fields(dc_cls): 

390 default: Any = Undefined 

391 default_factory: Optional['NoArgAnyCallable'] = None 

392 field_info: FieldInfo 

393 

394 if field.default is not dataclasses.MISSING: 

395 default = field.default 

396 elif field.default_factory is not dataclasses.MISSING: 

397 default_factory = field.default_factory 

398 else: 

399 default = Required 

400 

401 if isinstance(default, FieldInfo): 

402 field_info = default 

403 dc_cls.__pydantic_has_field_info_default__ = True 

404 else: 

405 field_info = Field(default=default, default_factory=default_factory, **field.metadata) 

406 

407 field_definitions[field.name] = (field.type, field_info) 

408 

409 validators = gather_all_validators(dc_cls) 

410 model: Type['BaseModel'] = create_model( 

411 dc_cls.__name__, 

412 __config__=config, 

413 __module__=dc_cls.__module__, 

414 __validators__=validators, 

415 __cls_kwargs__={'__resolve_forward_refs__': False}, 

416 **field_definitions, 

417 ) 

418 model.__doc__ = dc_cls_doc if dc_cls_doc is not None else dc_cls.__doc__ or '' 

419 return model 

420 

421 

422if sys.version_info >= (3, 8): 

423 

424 def _is_field_cached_property(obj: 'Dataclass', k: str) -> bool: 

425 return isinstance(getattr(type(obj), k, None), cached_property) 

426 

427else: 

428 

429 def _is_field_cached_property(obj: 'Dataclass', k: str) -> bool: 

430 return False 

431 

432 

433def _dataclass_validate_values(self: 'Dataclass') -> None: 

434 # validation errors can occur if this function is called twice on an already initialised dataclass. 

435 # for example if Extra.forbid is enabled, it would consider __pydantic_initialised__ an invalid extra property 

436 if getattr(self, '__pydantic_initialised__'): 

437 return 

438 if getattr(self, '__pydantic_has_field_info_default__', False): 

439 # We need to remove `FieldInfo` values since they are not valid as input 

440 # It's ok to do that because they are obviously the default values! 

441 input_data = { 

442 k: v 

443 for k, v in self.__dict__.items() 

444 if not (isinstance(v, FieldInfo) or _is_field_cached_property(self, k)) 

445 } 

446 else: 

447 input_data = {k: v for k, v in self.__dict__.items() if not _is_field_cached_property(self, k)} 

448 d, _, validation_error = validate_model(self.__pydantic_model__, input_data, cls=self.__class__) 

449 if validation_error: 

450 raise validation_error 

451 self.__dict__.update(d) 

452 object.__setattr__(self, '__pydantic_initialised__', True) 

453 

454 

455def _dataclass_validate_assignment_setattr(self: 'Dataclass', name: str, value: Any) -> None: 

456 if self.__pydantic_initialised__: 

457 d = dict(self.__dict__) 

458 d.pop(name, None) 

459 known_field = self.__pydantic_model__.__fields__.get(name, None) 

460 if known_field: 

461 value, error_ = known_field.validate(value, d, loc=name, cls=self.__class__) 

462 if error_: 

463 raise ValidationError([error_], self.__class__) 

464 

465 object.__setattr__(self, name, value) 

466 

467 

468def is_builtin_dataclass(_cls: Type[Any]) -> bool: 

469 """ 

470 Whether a class is a stdlib dataclass 

471 (useful to discriminated a pydantic dataclass that is actually a wrapper around a stdlib dataclass) 

472 

473 we check that 

474 - `_cls` is a dataclass 

475 - `_cls` is not a processed pydantic dataclass (with a basemodel attached) 

476 - `_cls` is not a pydantic dataclass inheriting directly from a stdlib dataclass 

477 e.g. 

478 ``` 

479 @dataclasses.dataclass 

480 class A: 

481 x: int 

482 

483 @pydantic.dataclasses.dataclass 

484 class B(A): 

485 y: int 

486 ``` 

487 In this case, when we first check `B`, we make an extra check and look at the annotations ('y'), 

488 which won't be a superset of all the dataclass fields (only the stdlib fields i.e. 'x') 

489 """ 

490 return ( 

491 dataclasses.is_dataclass(_cls) 

492 and not hasattr(_cls, '__pydantic_model__') 

493 and set(_cls.__dataclass_fields__).issuperset(set(getattr(_cls, '__annotations__', {}))) 

494 ) 

495 

496 

497def make_dataclass_validator(dc_cls: Type['Dataclass'], config: Type[BaseConfig]) -> 'CallableGenerator': 

498 """ 

499 Create a pydantic.dataclass from a builtin dataclass to add type validation 

500 and yield the validators 

501 It retrieves the parameters of the dataclass and forwards them to the newly created dataclass 

502 """ 

503 yield from _get_validators(dataclass(dc_cls, config=config, use_proxy=True))