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

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 not TYPE_CHECKING: 

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

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

25 DeprecationWarning = PydanticDeprecatedSince20 

26 

27if TYPE_CHECKING: 

28 from .._internal._schema_generation_shared import GenerateSchema 

29 from ..fields import ComputedFieldInfo, FieldInfo 

30 

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

32 

33 

34class ConfigWrapper: 

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

36 

37 __slots__ = ('config_dict',) 

38 

39 config_dict: ConfigDict 

40 

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

42 # stop matching 

43 title: str | None 

44 str_to_lower: bool 

45 str_to_upper: bool 

46 str_strip_whitespace: bool 

47 str_min_length: int 

48 str_max_length: int | None 

49 extra: ExtraValues | None 

50 frozen: bool 

51 populate_by_name: bool 

52 use_enum_values: bool 

53 validate_assignment: bool 

54 arbitrary_types_allowed: bool 

55 from_attributes: bool 

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

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

58 loc_by_alias: bool 

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

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

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

62 ignored_types: tuple[type, ...] 

63 allow_inf_nan: bool 

64 json_schema_extra: JsonDict | JsonSchemaExtraCallable | None 

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

66 

67 # new in V2 

68 strict: bool 

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

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

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

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

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

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

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

76 validate_default: bool 

77 validate_return: bool 

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

79 hide_input_in_errors: bool 

80 defer_build: bool 

81 plugin_settings: dict[str, object] | None 

82 schema_generator: type[GenerateSchema] | None 

83 json_schema_serialization_defaults_required: bool 

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

85 coerce_numbers_to_str: bool 

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

87 validation_error_cause: bool 

88 use_attribute_docstrings: bool 

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

90 validate_by_alias: bool 

91 validate_by_name: bool 

92 serialize_by_alias: bool 

93 

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

95 if check: 

96 self.config_dict = prepare_config(config) 

97 else: 

98 self.config_dict = cast(ConfigDict, config) 

99 

100 @classmethod 

101 def for_model(cls, bases: tuple[type[Any], ...], namespace: dict[str, Any], kwargs: dict[str, Any]) -> Self: 

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

103 

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

105 - options from `kwargs` 

106 - options from the `namespace` 

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

108 

109 Args: 

110 bases: A tuple of base classes. 

111 namespace: The namespace of the class being created. 

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

113 

114 Returns: 

115 A `ConfigWrapper` instance for `BaseModel`. 

116 """ 

117 config_new = ConfigDict() 

118 for base in bases: 

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

120 if config: 

121 config_new.update(config.copy()) 

122 

123 config_class_from_namespace = namespace.get('Config') 

124 config_dict_from_namespace = namespace.get('model_config') 

125 

126 raw_annotations = namespace.get('__annotations__', {}) 

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

128 raise PydanticUserError( 

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

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

131 ) 

132 

133 if config_class_from_namespace and config_dict_from_namespace: 

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

135 

136 config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace) 

137 

138 config_new.update(config_from_namespace) 

139 

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

141 if k in config_keys: 

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

143 

144 return cls(config_new) 

145 

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

147 if not TYPE_CHECKING: # pragma: no branch 

148 

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

150 try: 

151 return self.config_dict[name] 

152 except KeyError: 

153 try: 

154 return config_defaults[name] 

155 except KeyError: 

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

157 

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

159 """Create a pydantic-core config. 

160 

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

162 

163 Args: 

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

165 

166 Returns: 

167 A `CoreConfig` object created from config. 

168 """ 

169 config = self.config_dict 

170 

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

172 warnings.warn( 

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

174 PydanticDeprecatedSince210, 

175 stacklevel=2, 

176 ) 

177 

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

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

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

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

182 config['validate_by_alias'] = True 

183 config['validate_by_name'] = populate_by_name 

184 

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

186 # and validate_by_name is not explicitly set. 

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

188 config['validate_by_name'] = True 

189 

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

191 raise PydanticUserError( 

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

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

194 ) 

195 

196 return core_schema.CoreConfig( 

197 **{ # pyright: ignore[reportArgumentType] 

198 k: v 

199 for k, v in ( 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

225 ) 

226 if v is not None 

227 } 

228 ) 

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_bytes='utf8', 

287 val_json_bytes='utf8', 

288 ser_json_inf_nan='null', 

289 validate_default=False, 

290 validate_return=False, 

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

292 hide_input_in_errors=False, 

293 json_encoders=None, 

294 defer_build=False, 

295 schema_generator=None, 

296 plugin_settings=None, 

297 json_schema_serialization_defaults_required=False, 

298 json_schema_mode_override=None, 

299 coerce_numbers_to_str=False, 

300 regex_engine='rust-regex', 

301 validation_error_cause=False, 

302 use_attribute_docstrings=False, 

303 cache_strings=True, 

304 validate_by_alias=True, 

305 validate_by_name=False, 

306 serialize_by_alias=False, 

307) 

308 

309 

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

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

312 

313 Args: 

314 config: The input config. 

315 

316 Returns: 

317 A ConfigDict object created from config. 

318 """ 

319 if config is None: 

320 return ConfigDict() 

321 

322 if not isinstance(config, dict): 

323 warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning) 

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

325 

326 config_dict = cast(ConfigDict, config) 

327 check_deprecated(config_dict) 

328 return config_dict 

329 

330 

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

332 

333 

334V2_REMOVED_KEYS = { 

335 'allow_mutation', 

336 'error_msg_templates', 

337 'fields', 

338 'getter_dict', 

339 'smart_union', 

340 'underscore_attrs_are_private', 

341 'json_loads', 

342 'json_dumps', 

343 'copy_on_model_validation', 

344 'post_init_call', 

345} 

346V2_RENAMED_KEYS = { 

347 'allow_population_by_field_name': 'validate_by_name', 

348 'anystr_lower': 'str_to_lower', 

349 'anystr_strip_whitespace': 'str_strip_whitespace', 

350 'anystr_upper': 'str_to_upper', 

351 'keep_untouched': 'ignored_types', 

352 'max_anystr_length': 'str_max_length', 

353 'min_anystr_length': 'str_min_length', 

354 'orm_mode': 'from_attributes', 

355 'schema_extra': 'json_schema_extra', 

356 'validate_all': 'validate_default', 

357} 

358 

359 

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

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

362 

363 Args: 

364 config_dict: The input config. 

365 """ 

366 deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys() 

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

368 if deprecated_removed_keys or deprecated_renamed_keys: 

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

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

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

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

373 warnings.warn(message, UserWarning)