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

152 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 

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

93 if check: 

94 self.config_dict = prepare_config(config) 

95 else: 

96 self.config_dict = cast(ConfigDict, config) 

97 

98 @classmethod 

99 def for_model( 

100 cls, 

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

102 namespace: dict[str, Any], 

103 raw_annotations: dict[str, Any], 

104 kwargs: dict[str, Any], 

105 ) -> Self: 

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

107 

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

109 - options from `kwargs` 

110 - options from the `namespace` 

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

112 

113 Args: 

114 bases: A tuple of base classes. 

115 namespace: The namespace of the class being created. 

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

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

118 

119 Returns: 

120 A `ConfigWrapper` instance for `BaseModel`. 

121 """ 

122 config_new = ConfigDict() 

123 for base in bases: 

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

125 if config: 

126 config_new.update(config.copy()) 

127 

128 config_class_from_namespace = namespace.get('Config') 

129 config_dict_from_namespace = namespace.get('model_config') 

130 

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

132 raise PydanticUserError( 

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

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

135 ) 

136 

137 if config_class_from_namespace and config_dict_from_namespace: 

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

139 

140 config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace) 

141 

142 config_new.update(config_from_namespace) 

143 

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

145 if k in config_keys: 

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

147 

148 return cls(config_new) 

149 

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

151 if not TYPE_CHECKING: # pragma: no branch 

152 

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

154 try: 

155 return self.config_dict[name] 

156 except KeyError: 

157 try: 

158 return config_defaults[name] 

159 except KeyError: 

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

161 

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

163 """Create a pydantic-core config. 

164 

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

166 

167 Args: 

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

169 

170 Returns: 

171 A `CoreConfig` object created from config. 

172 """ 

173 config = self.config_dict 

174 

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

176 warnings.warn( 

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

178 PydanticDeprecatedSince210, 

179 stacklevel=2, 

180 ) 

181 

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

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

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

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

186 config['validate_by_alias'] = True 

187 config['validate_by_name'] = populate_by_name 

188 

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

190 # and validate_by_name is not explicitly set. 

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

192 config['validate_by_name'] = True 

193 

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

195 raise PydanticUserError( 

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

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

198 ) 

199 

200 return core_schema.CoreConfig( 

201 **{ # pyright: ignore[reportArgumentType] 

202 k: v 

203 for k, v in ( 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

232 ) 

233 if v is not None 

234 } 

235 ) 

236 

237 def __repr__(self): 

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

239 return f'ConfigWrapper({c})' 

240 

241 

242class ConfigWrapperStack: 

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

244 

245 def __init__(self, config_wrapper: ConfigWrapper): 

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

247 

248 @property 

249 def tail(self) -> ConfigWrapper: 

250 return self._config_wrapper_stack[-1] 

251 

252 @contextmanager 

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

254 if config_wrapper is None: 

255 yield 

256 return 

257 

258 if not isinstance(config_wrapper, ConfigWrapper): 

259 config_wrapper = ConfigWrapper(config_wrapper, check=False) 

260 

261 self._config_wrapper_stack.append(config_wrapper) 

262 try: 

263 yield 

264 finally: 

265 self._config_wrapper_stack.pop() 

266 

267 

268config_defaults = ConfigDict( 

269 title=None, 

270 str_to_lower=False, 

271 str_to_upper=False, 

272 str_strip_whitespace=False, 

273 str_min_length=0, 

274 str_max_length=None, 

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

276 extra=None, 

277 frozen=False, 

278 populate_by_name=False, 

279 use_enum_values=False, 

280 validate_assignment=False, 

281 arbitrary_types_allowed=False, 

282 from_attributes=False, 

283 loc_by_alias=True, 

284 alias_generator=None, 

285 model_title_generator=None, 

286 field_title_generator=None, 

287 ignored_types=(), 

288 allow_inf_nan=True, 

289 json_schema_extra=None, 

290 strict=False, 

291 revalidate_instances='never', 

292 ser_json_timedelta='iso8601', 

293 ser_json_temporal='iso8601', 

294 val_temporal_unit='infer', 

295 ser_json_bytes='utf8', 

296 val_json_bytes='utf8', 

297 ser_json_inf_nan='null', 

298 validate_default=False, 

299 validate_return=False, 

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

301 hide_input_in_errors=False, 

302 json_encoders=None, 

303 defer_build=False, 

304 schema_generator=None, 

305 plugin_settings=None, 

306 json_schema_serialization_defaults_required=False, 

307 json_schema_mode_override=None, 

308 coerce_numbers_to_str=False, 

309 regex_engine='rust-regex', 

310 validation_error_cause=False, 

311 use_attribute_docstrings=False, 

312 cache_strings=True, 

313 validate_by_alias=True, 

314 validate_by_name=False, 

315 serialize_by_alias=False, 

316 url_preserve_empty_path=False, 

317) 

318 

319 

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

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

322 

323 Args: 

324 config: The input config. 

325 

326 Returns: 

327 A ConfigDict object created from config. 

328 """ 

329 if config is None: 

330 return ConfigDict() 

331 

332 if not isinstance(config, dict): 

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

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

335 

336 config_dict = cast(ConfigDict, config) 

337 check_deprecated(config_dict) 

338 return config_dict 

339 

340 

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

342 

343 

344V2_REMOVED_KEYS = { 

345 'allow_mutation', 

346 'error_msg_templates', 

347 'fields', 

348 'getter_dict', 

349 'smart_union', 

350 'underscore_attrs_are_private', 

351 'json_loads', 

352 'json_dumps', 

353 'copy_on_model_validation', 

354 'post_init_call', 

355} 

356V2_RENAMED_KEYS = { 

357 'allow_population_by_field_name': 'validate_by_name', 

358 'anystr_lower': 'str_to_lower', 

359 'anystr_strip_whitespace': 'str_strip_whitespace', 

360 'anystr_upper': 'str_to_upper', 

361 'keep_untouched': 'ignored_types', 

362 'max_anystr_length': 'str_max_length', 

363 'min_anystr_length': 'str_min_length', 

364 'orm_mode': 'from_attributes', 

365 'schema_extra': 'json_schema_extra', 

366 'validate_all': 'validate_default', 

367} 

368 

369 

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

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

372 

373 Args: 

374 config_dict: The input config. 

375 """ 

376 deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys() 

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

378 if deprecated_removed_keys or deprecated_renamed_keys: 

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

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

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

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

383 warnings.warn(message, UserWarning)