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

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

153 statements  

1from __future__ import annotations as _annotations 

2 

3import warnings 

4from contextlib import contextmanager 

5from re import Pattern 

6from typing import ( 

7 TYPE_CHECKING, 

8 Any, 

9 Callable, 

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 

29class ConfigWrapper: 

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

31 

32 __slots__ = ('config_dict',) 

33 

34 config_dict: ConfigDict 

35 

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

37 # stop matching 

38 title: str | None 

39 str_to_lower: bool 

40 str_to_upper: bool 

41 str_strip_whitespace: bool 

42 str_min_length: int 

43 str_max_length: int | None 

44 extra: ExtraValues | None 

45 frozen: bool 

46 populate_by_name: bool 

47 use_enum_values: bool 

48 validate_assignment: bool 

49 arbitrary_types_allowed: bool 

50 from_attributes: bool 

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

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

53 loc_by_alias: bool 

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

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

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

57 ignored_types: tuple[type, ...] 

58 allow_inf_nan: bool 

59 json_schema_extra: JsonDict | JsonSchemaExtraCallable | None 

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

61 

62 # new in V2 

63 strict: bool 

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

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

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

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

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

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

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

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

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

73 validate_default: bool 

74 validate_return: bool 

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

76 hide_input_in_errors: bool 

77 defer_build: bool 

78 plugin_settings: dict[str, object] | None 

79 schema_generator: type[GenerateSchema] | None 

80 json_schema_serialization_defaults_required: bool 

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

82 coerce_numbers_to_str: bool 

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

84 validation_error_cause: bool 

85 use_attribute_docstrings: bool 

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

87 validate_by_alias: bool 

88 validate_by_name: bool 

89 serialize_by_alias: bool 

90 url_preserve_empty_path: bool 

91 polymorphic_serialization: bool 

92 

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

94 if check: 

95 self.config_dict = prepare_config(config) 

96 else: 

97 self.config_dict = cast(ConfigDict, config) 

98 

99 @classmethod 

100 def for_model( 

101 cls, 

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

103 namespace: dict[str, Any], 

104 raw_annotations: dict[str, Any], 

105 kwargs: dict[str, Any], 

106 ) -> Self: 

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

108 

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

110 - options from `kwargs` 

111 - options from the `namespace` 

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

113 

114 Args: 

115 bases: A tuple of base classes. 

116 namespace: The namespace of the class being created. 

117 raw_annotations: The (non-evaluated) annotations of the model. 

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

119 

120 Returns: 

121 A `ConfigWrapper` instance for `BaseModel`. 

122 """ 

123 config_new = ConfigDict() 

124 for base in bases: 

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

126 if config: 

127 config_new.update(config.copy()) 

128 

129 config_class_from_namespace = namespace.get('Config') 

130 config_dict_from_namespace = namespace.get('model_config') 

131 

132 if raw_annotations.get('model_config') and config_dict_from_namespace is None: 

133 raise PydanticUserError( 

134 '`model_config` cannot be used as a model field name. Use `model_config` for model configuration.', 

135 code='model-config-invalid-field-name', 

136 ) 

137 

138 if config_class_from_namespace and config_dict_from_namespace: 

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

140 

141 config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace) 

142 

143 config_new.update(config_from_namespace) 

144 

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

146 if k in config_keys: 

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

148 

149 return cls(config_new) 

150 

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

152 if not TYPE_CHECKING: # pragma: no branch 

153 

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

155 try: 

156 return self.config_dict[name] 

157 except KeyError: 

158 try: 

159 return config_defaults[name] 

160 except KeyError: 

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

162 

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

164 """Create a pydantic-core config. 

165 

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

167 

168 Args: 

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

170 

171 Returns: 

172 A `CoreConfig` object created from config. 

173 """ 

174 config = self.config_dict 

175 

176 if config.get('schema_generator') is not None: 

177 warnings.warn( 

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

179 PydanticDeprecatedSince210, 

180 stacklevel=2, 

181 ) 

182 

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

184 # We include this patch for backwards compatibility purposes, but this config setting will be deprecated in v3.0, and likely removed in v4.0. 

185 # Thus, the above warning and this patch can be removed then as well. 

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

187 config['validate_by_alias'] = True 

188 config['validate_by_name'] = populate_by_name 

189 

190 # We dynamically patch validate_by_name to be True if validate_by_alias is set to False 

191 # and validate_by_name is not explicitly set. 

192 if config.get('validate_by_alias') is False and config.get('validate_by_name') is None: 

193 config['validate_by_name'] = True 

194 

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

196 raise PydanticUserError( 

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

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

199 ) 

200 

201 return core_schema.CoreConfig( 

202 **{ # pyright: ignore[reportArgumentType] 

203 k: v 

204 for k, v in ( 

205 ('title', config.get('title') or title or None), 

206 ('extra_fields_behavior', config.get('extra')), 

207 ('allow_inf_nan', config.get('allow_inf_nan')), 

208 ('str_strip_whitespace', config.get('str_strip_whitespace')), 

209 ('str_to_lower', config.get('str_to_lower')), 

210 ('str_to_upper', config.get('str_to_upper')), 

211 ('strict', config.get('strict')), 

212 ('ser_json_timedelta', config.get('ser_json_timedelta')), 

213 ('ser_json_temporal', config.get('ser_json_temporal')), 

214 ('val_temporal_unit', config.get('val_temporal_unit')), 

215 ('ser_json_bytes', config.get('ser_json_bytes')), 

216 ('val_json_bytes', config.get('val_json_bytes')), 

217 ('ser_json_inf_nan', config.get('ser_json_inf_nan')), 

218 ('from_attributes', config.get('from_attributes')), 

219 ('loc_by_alias', config.get('loc_by_alias')), 

220 ('revalidate_instances', config.get('revalidate_instances')), 

221 ('validate_default', config.get('validate_default')), 

222 ('str_max_length', config.get('str_max_length')), 

223 ('str_min_length', config.get('str_min_length')), 

224 ('hide_input_in_errors', config.get('hide_input_in_errors')), 

225 ('coerce_numbers_to_str', config.get('coerce_numbers_to_str')), 

226 ('regex_engine', config.get('regex_engine')), 

227 ('validation_error_cause', config.get('validation_error_cause')), 

228 ('cache_strings', config.get('cache_strings')), 

229 ('validate_by_alias', config.get('validate_by_alias')), 

230 ('validate_by_name', config.get('validate_by_name')), 

231 ('serialize_by_alias', config.get('serialize_by_alias')), 

232 ('url_preserve_empty_path', config.get('url_preserve_empty_path')), 

233 ('polymorphic_serialization', config.get('polymorphic_serialization')), 

234 ) 

235 if v is not None 

236 } 

237 ) 

238 

239 def __repr__(self): 

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

241 return f'ConfigWrapper({c})' 

242 

243 

244class ConfigWrapperStack: 

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

246 

247 def __init__(self, config_wrapper: ConfigWrapper): 

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

249 

250 @property 

251 def tail(self) -> ConfigWrapper: 

252 return self._config_wrapper_stack[-1] 

253 

254 @contextmanager 

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

256 if config_wrapper is None: 

257 yield 

258 return 

259 

260 if not isinstance(config_wrapper, ConfigWrapper): 

261 config_wrapper = ConfigWrapper(config_wrapper, check=False) 

262 

263 self._config_wrapper_stack.append(config_wrapper) 

264 try: 

265 yield 

266 finally: 

267 self._config_wrapper_stack.pop() 

268 

269 

270config_defaults = ConfigDict( 

271 title=None, 

272 str_to_lower=False, 

273 str_to_upper=False, 

274 str_strip_whitespace=False, 

275 str_min_length=0, 

276 str_max_length=None, 

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

278 extra=None, 

279 frozen=False, 

280 populate_by_name=False, 

281 use_enum_values=False, 

282 validate_assignment=False, 

283 arbitrary_types_allowed=False, 

284 from_attributes=False, 

285 loc_by_alias=True, 

286 alias_generator=None, 

287 model_title_generator=None, 

288 field_title_generator=None, 

289 ignored_types=(), 

290 allow_inf_nan=True, 

291 json_schema_extra=None, 

292 strict=False, 

293 revalidate_instances='never', 

294 ser_json_timedelta='iso8601', 

295 ser_json_temporal='iso8601', 

296 val_temporal_unit='infer', 

297 ser_json_bytes='utf8', 

298 val_json_bytes='utf8', 

299 ser_json_inf_nan='null', 

300 validate_default=False, 

301 validate_return=False, 

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

303 hide_input_in_errors=False, 

304 json_encoders=None, 

305 defer_build=False, 

306 schema_generator=None, 

307 plugin_settings=None, 

308 json_schema_serialization_defaults_required=False, 

309 json_schema_mode_override=None, 

310 coerce_numbers_to_str=False, 

311 regex_engine='rust-regex', 

312 validation_error_cause=False, 

313 use_attribute_docstrings=False, 

314 cache_strings=True, 

315 validate_by_alias=True, 

316 validate_by_name=False, 

317 serialize_by_alias=False, 

318 url_preserve_empty_path=False, 

319 polymorphic_serialization=False, 

320) 

321 

322 

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

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

325 

326 Args: 

327 config: The input config. 

328 

329 Returns: 

330 A ConfigDict object created from config. 

331 """ 

332 if config is None: 

333 return ConfigDict() 

334 

335 if not isinstance(config, dict): 

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

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

338 

339 config_dict = cast(ConfigDict, config) 

340 check_deprecated(config_dict) 

341 return config_dict 

342 

343 

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

345 

346 

347V2_REMOVED_KEYS = { 

348 'allow_mutation', 

349 'error_msg_templates', 

350 'fields', 

351 'getter_dict', 

352 'smart_union', 

353 'underscore_attrs_are_private', 

354 'json_loads', 

355 'json_dumps', 

356 'copy_on_model_validation', 

357 'post_init_call', 

358} 

359V2_RENAMED_KEYS = { 

360 'allow_population_by_field_name': 'validate_by_name', 

361 'anystr_lower': 'str_to_lower', 

362 'anystr_strip_whitespace': 'str_strip_whitespace', 

363 'anystr_upper': 'str_to_upper', 

364 'keep_untouched': 'ignored_types', 

365 'max_anystr_length': 'str_max_length', 

366 'min_anystr_length': 'str_min_length', 

367 'orm_mode': 'from_attributes', 

368 'schema_extra': 'json_schema_extra', 

369 'validate_all': 'validate_default', 

370} 

371 

372 

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

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

375 

376 Args: 

377 config_dict: The input config. 

378 """ 

379 deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys() 

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

381 if deprecated_removed_keys or deprecated_renamed_keys: 

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

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

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

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

386 warnings.warn(message, UserWarning)