Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pydantic/_internal/_config.py: 88%

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

168 statements  

1from __future__ import annotations as _annotations 

2 

3import warnings 

4from collections.abc import Callable 

5from contextlib import contextmanager 

6from re import Pattern 

7from typing import ( 

8 TYPE_CHECKING, 

9 Any, 

10 Literal, 

11 cast, 

12) 

13 

14from pydantic_core import core_schema 

15from typing_extensions import Self 

16 

17from ..aliases import AliasGenerator 

18from ..config import ConfigDict, ExtraValues, JsonDict, JsonEncoder, JsonSchemaExtraCallable 

19from ..errors import PydanticUserError 

20from ..warnings import PydanticDeprecatedSince20, PydanticDeprecatedSince210 

21 

22if TYPE_CHECKING: 

23 from .._internal._schema_generation_shared import GenerateSchema 

24 from ..fields import ComputedFieldInfo, FieldInfo 

25 

26DEPRECATION_MESSAGE = 'Support for class-based `config` is deprecated, use ConfigDict instead.' 

27 

28# Maps the `ConfigDict` keys that have a `CoreConfig` counterpart to the corresponding 

29# `CoreConfig` key (`'title'` is omitted as it requires special handling, see `ConfigWrapper.core_config()`): 

30_config_dict_to_core_config_key: dict[str, str] = { 

31 'extra': 'extra_fields_behavior', 

32 'allow_inf_nan': 'allow_inf_nan', 

33 'str_strip_whitespace': 'str_strip_whitespace', 

34 'str_to_lower': 'str_to_lower', 

35 'str_to_upper': 'str_to_upper', 

36 'strict': 'strict', 

37 'ser_json_timedelta': 'ser_json_timedelta', 

38 'ser_json_temporal': 'ser_json_temporal', 

39 'val_temporal_unit': 'val_temporal_unit', 

40 'ser_json_bytes': 'ser_json_bytes', 

41 'val_json_bytes': 'val_json_bytes', 

42 'ser_json_inf_nan': 'ser_json_inf_nan', 

43 'from_attributes': 'from_attributes', 

44 'loc_by_alias': 'loc_by_alias', 

45 'revalidate_instances': 'revalidate_instances', 

46 'validate_default': 'validate_default', 

47 'str_max_length': 'str_max_length', 

48 'str_min_length': 'str_min_length', 

49 'hide_input_in_errors': 'hide_input_in_errors', 

50 'coerce_numbers_to_str': 'coerce_numbers_to_str', 

51 'regex_engine': 'regex_engine', 

52 'validation_error_cause': 'validation_error_cause', 

53 'cache_strings': 'cache_strings', 

54 'validate_by_alias': 'validate_by_alias', 

55 'validate_by_name': 'validate_by_name', 

56 'serialize_by_alias': 'serialize_by_alias', 

57 'url_preserve_empty_path': 'url_preserve_empty_path', 

58 'polymorphic_serialization': 'polymorphic_serialization', 

59} 

60 

61 

62class ConfigWrapper: 

63 """Internal wrapper for Config which exposes ConfigDict items as attributes.""" 

64 

65 __slots__ = ('config_dict', 'effective_config') 

66 

67 config_dict: ConfigDict 

68 """The configuration as declared by the user. 

69 

70 This is the mapping exposed as the `model_config` attribute of models (and the 

71 `__pydantic_config__` attribute of dataclasses), and the one merged with the 

72 configuration of the bases when subclassing. As such, it must never be mutated. 

73 """ 

74 

75 effective_config: ConfigDict 

76 """The configuration actually in effect, with the deprecated settings applied. 

77 

78 This is what every consumer of the configuration should read (both the attribute 

79 access on this wrapper and `core_config()` do), as `config_dict` alone does not 

80 account for the deprecated settings (see `_build_effective_config()`). 

81 """ 

82 

83 # all annotations are copied directly from ConfigDict, and should be kept up to date, a test will fail if they 

84 # stop matching 

85 title: str | None 

86 str_to_lower: bool 

87 str_to_upper: bool 

88 str_strip_whitespace: bool 

89 str_min_length: int 

90 str_max_length: int | None 

91 extra: ExtraValues | None 

92 frozen: bool 

93 populate_by_name: bool 

94 use_enum_values: bool 

95 validate_assignment: bool 

96 arbitrary_types_allowed: bool 

97 from_attributes: bool 

98 # whether to use the actual key provided in the data (e.g. alias or first alias for "field required" errors) instead of field_names 

99 # to construct error `loc`s, default `True` 

100 loc_by_alias: bool 

101 alias_generator: Callable[[str], str] | AliasGenerator | None 

102 model_title_generator: Callable[[type], str] | None 

103 field_title_generator: Callable[[str, FieldInfo | ComputedFieldInfo], str] | None 

104 ignored_types: tuple[type, ...] 

105 allow_inf_nan: bool 

106 json_schema_extra: JsonDict | JsonSchemaExtraCallable | None 

107 json_encoders: dict[type[object], JsonEncoder] | None 

108 

109 # new in V2 

110 strict: bool 

111 # whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never' 

112 revalidate_instances: Literal['always', 'never', 'subclass-instances'] 

113 ser_json_timedelta: Literal['iso8601', 'float'] 

114 ser_json_temporal: Literal['iso8601', 'seconds', 'milliseconds'] 

115 val_temporal_unit: Literal['seconds', 'milliseconds', 'infer'] 

116 ser_json_bytes: Literal['utf8', 'base64', 'hex'] 

117 val_json_bytes: Literal['utf8', 'base64', 'hex'] 

118 ser_json_inf_nan: Literal['null', 'constants', 'strings'] 

119 # whether to validate default values during validation, default False 

120 validate_default: bool 

121 validate_return: bool 

122 protected_namespaces: tuple[str | Pattern[str], ...] 

123 hide_input_in_errors: bool 

124 defer_build: bool 

125 plugin_settings: dict[str, object] | None 

126 schema_generator: type[GenerateSchema] | None 

127 json_schema_serialization_defaults_required: bool 

128 json_schema_mode_override: Literal['validation', 'serialization', None] 

129 coerce_numbers_to_str: bool 

130 regex_engine: Literal['rust-regex', 'python-re'] 

131 validation_error_cause: bool 

132 use_attribute_docstrings: bool 

133 cache_strings: bool | Literal['all', 'keys', 'none'] 

134 validate_by_alias: bool 

135 validate_by_name: bool 

136 serialize_by_alias: bool 

137 url_preserve_empty_path: bool 

138 polymorphic_serialization: bool 

139 

140 def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, check: bool = True): 

141 if check: 

142 self.config_dict = prepare_config(config) 

143 else: 

144 self.config_dict = cast(ConfigDict, config) 

145 self.effective_config = _build_effective_config(self.config_dict) 

146 

147 @classmethod 

148 def for_model( 

149 cls, 

150 bases: tuple[type[Any], ...], 

151 namespace: dict[str, Any], 

152 kwargs: dict[str, Any], 

153 ) -> Self: 

154 """Build a new `ConfigWrapper` instance for a `BaseModel`. 

155 

156 The config wrapper built based on (in descending order of priority): 

157 - options from `kwargs` 

158 - options from the `namespace` 

159 - options from the base classes (`bases`) 

160 

161 Args: 

162 bases: A tuple of base classes. 

163 namespace: The namespace of the class being created. 

164 kwargs: The kwargs passed to the class being created. 

165 

166 Returns: 

167 A `ConfigWrapper` instance for `BaseModel`. 

168 """ 

169 config_new = ConfigDict() 

170 for base in bases: 

171 config = getattr(base, 'model_config', None) 

172 if config: 

173 config_new.update(config.copy()) 

174 

175 config_class_from_namespace = namespace.get('Config') 

176 config_dict_from_namespace = namespace.get('model_config') 

177 

178 if config_class_from_namespace and config_dict_from_namespace: 

179 raise PydanticUserError('"Config" and "model_config" cannot be used together', code='config-both') 

180 

181 config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace) 

182 

183 config_new.update(config_from_namespace) 

184 

185 for k in list(kwargs.keys()): 

186 if k in config_keys: 

187 config_new[k] = kwargs.pop(k) 

188 

189 return cls(config_new) 

190 

191 # we don't show `__getattr__` to type checkers so missing attributes cause errors 

192 if not TYPE_CHECKING: # pragma: no branch 

193 

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

195 try: 

196 return self.effective_config[name] 

197 except KeyError: 

198 try: 

199 return config_defaults[name] 

200 except KeyError: 

201 raise AttributeError(f'Config has no attribute {name!r}') from None 

202 

203 def core_config(self, title: str | None) -> core_schema.CoreConfig: 

204 """Create a pydantic-core config. 

205 

206 We don't use getattr here since we don't want to populate with defaults. 

207 

208 Args: 

209 title: The title to use if not set in config. 

210 

211 Returns: 

212 A `CoreConfig` object created from config. 

213 """ 

214 config = self.effective_config 

215 

216 if not config: 

217 # Fast path for the common case of an empty (default) config: 

218 return core_schema.CoreConfig(title=title) if title else core_schema.CoreConfig() 

219 

220 core_config_values: dict[str, Any] = {} 

221 for k, v in config.items(): 

222 if v is not None and (core_config_key := _config_dict_to_core_config_key.get(k)) is not None: 

223 core_config_values[core_config_key] = v 

224 title = config.get('title') or title 

225 if title: 

226 core_config_values['title'] = title 

227 

228 return cast(core_schema.CoreConfig, core_config_values) 

229 

230 def __repr__(self): 

231 c = ', '.join(f'{k}={v!r}' for k, v in self.config_dict.items()) 

232 return f'ConfigWrapper({c})' 

233 

234 

235class ConfigWrapperStack: 

236 """A stack of `ConfigWrapper` instances.""" 

237 

238 def __init__(self, config_wrapper: ConfigWrapper): 

239 self._config_wrapper_stack: list[ConfigWrapper] = [config_wrapper] 

240 

241 @property 

242 def tail(self) -> ConfigWrapper: 

243 return self._config_wrapper_stack[-1] 

244 

245 @contextmanager 

246 def push(self, config_wrapper: ConfigWrapper | ConfigDict | None): 

247 if config_wrapper is None: 

248 yield 

249 return 

250 

251 if not isinstance(config_wrapper, ConfigWrapper): 

252 config_wrapper = ConfigWrapper(config_wrapper, check=False) 

253 

254 self._config_wrapper_stack.append(config_wrapper) 

255 try: 

256 yield 

257 finally: 

258 self._config_wrapper_stack.pop() 

259 

260 

261config_defaults = ConfigDict( 

262 title=None, 

263 str_to_lower=False, 

264 str_to_upper=False, 

265 str_strip_whitespace=False, 

266 str_min_length=0, 

267 str_max_length=None, 

268 # let the model / dataclass decide how to handle it 

269 extra=None, 

270 frozen=False, 

271 populate_by_name=False, 

272 use_enum_values=False, 

273 validate_assignment=False, 

274 arbitrary_types_allowed=False, 

275 from_attributes=False, 

276 loc_by_alias=True, 

277 alias_generator=None, 

278 model_title_generator=None, 

279 field_title_generator=None, 

280 ignored_types=(), 

281 allow_inf_nan=True, 

282 json_schema_extra=None, 

283 strict=False, 

284 revalidate_instances='never', 

285 ser_json_timedelta='iso8601', 

286 ser_json_temporal='iso8601', 

287 val_temporal_unit='infer', 

288 ser_json_bytes='utf8', 

289 val_json_bytes='utf8', 

290 ser_json_inf_nan='null', 

291 validate_default=False, 

292 validate_return=False, 

293 protected_namespaces=('model_validate', 'model_dump'), 

294 hide_input_in_errors=False, 

295 json_encoders=None, 

296 defer_build=False, 

297 schema_generator=None, 

298 plugin_settings=None, 

299 json_schema_serialization_defaults_required=False, 

300 json_schema_mode_override=None, 

301 coerce_numbers_to_str=False, 

302 regex_engine='rust-regex', 

303 validation_error_cause=False, 

304 use_attribute_docstrings=False, 

305 cache_strings=True, 

306 validate_by_alias=True, 

307 validate_by_name=False, 

308 serialize_by_alias=False, 

309 url_preserve_empty_path=False, 

310 polymorphic_serialization=False, 

311) 

312 

313 

314def _build_effective_config(config: ConfigDict) -> ConfigDict: 

315 """Build the configuration in effect from the configuration as declared. 

316 

317 Raises: 

318 PydanticUserError: If validation can happen by neither alias nor name. 

319 """ 

320 if config.get('validate_by_name') is None: 

321 if (populate_by_name := config.get('populate_by_name')) is not None: 

322 config = cast(ConfigDict, {**config, 'validate_by_alias': True, 'validate_by_name': populate_by_name}) 

323 elif config.get('validate_by_alias') is False: 

324 config = cast(ConfigDict, {**config, 'validate_by_name': True}) 

325 

326 if (not config.get('validate_by_alias', True)) and (not config.get('validate_by_name', False)): 

327 raise PydanticUserError( 

328 'At least one of `validate_by_alias` or `validate_by_name` must be set to True.', 

329 code='validate-by-alias-and-name-false', 

330 ) 

331 

332 return config 

333 

334 

335def prepare_config(config: ConfigDict | dict[str, Any] | type[Any] | None) -> ConfigDict: 

336 """Create a `ConfigDict` instance from an existing dict, a class (e.g. old class-based config) or None. 

337 

338 Args: 

339 config: The input config. 

340 

341 Returns: 

342 A ConfigDict object created from config. 

343 """ 

344 if config is None: 

345 return ConfigDict() 

346 

347 if not isinstance(config, dict): 

348 warnings.warn(DEPRECATION_MESSAGE, PydanticDeprecatedSince20, stacklevel=4) 

349 config = {k: getattr(config, k) for k in dir(config) if not k.startswith('__')} 

350 else: 

351 # Copy, so that the `ConfigWrapper` owns its configuration and can never 

352 # mutate the mapping provided by the user: 

353 config = config.copy() 

354 

355 config_dict = cast(ConfigDict, config) 

356 check_deprecated(config_dict) 

357 return config_dict 

358 

359 

360config_keys = set(ConfigDict.__annotations__.keys()) 

361 

362 

363V2_REMOVED_KEYS = { 

364 'allow_mutation', 

365 'error_msg_templates', 

366 'fields', 

367 'getter_dict', 

368 'smart_union', 

369 'underscore_attrs_are_private', 

370 'json_loads', 

371 'json_dumps', 

372 'copy_on_model_validation', 

373 'post_init_call', 

374} 

375V2_RENAMED_KEYS = { 

376 'allow_population_by_field_name': 'validate_by_name', 

377 'anystr_lower': 'str_to_lower', 

378 'anystr_strip_whitespace': 'str_strip_whitespace', 

379 'anystr_upper': 'str_to_upper', 

380 'keep_untouched': 'ignored_types', 

381 'max_anystr_length': 'str_max_length', 

382 'min_anystr_length': 'str_min_length', 

383 'orm_mode': 'from_attributes', 

384 'schema_extra': 'json_schema_extra', 

385 'validate_all': 'validate_default', 

386} 

387 

388 

389def check_deprecated(config_dict: ConfigDict) -> None: 

390 """Check for deprecated config keys and warn the user. 

391 

392 Args: 

393 config_dict: The input config. 

394 """ 

395 if config_dict.get('schema_generator') is not None: 

396 warnings.warn( 

397 'The `schema_generator` setting has been deprecated since v2.10. This setting no longer has any effect.', 

398 PydanticDeprecatedSince210, 

399 ) 

400 

401 deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys() 

402 deprecated_renamed_keys = V2_RENAMED_KEYS.keys() & config_dict.keys() 

403 if deprecated_removed_keys or deprecated_renamed_keys: 

404 renamings = {k: V2_RENAMED_KEYS[k] for k in sorted(deprecated_renamed_keys)} 

405 renamed_bullets = [f'* {k!r} has been renamed to {v!r}' for k, v in renamings.items()] 

406 removed_bullets = [f'* {k!r} has been removed' for k in sorted(deprecated_removed_keys)] 

407 message = '\n'.join(['Valid config keys have changed in V2:'] + renamed_bullets + removed_bullets) 

408 warnings.warn(message, UserWarning)