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)