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)