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)