Coverage for pydantic/_internal/_config.py: 100.00%
138 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-13 19:35 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-13 19:35 +0000
1from __future__ import annotations as _annotations 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
3import warnings 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
4from contextlib import contextmanager 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
5from re import Pattern 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
6from typing import ( 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
7 TYPE_CHECKING,
8 Any,
9 Callable,
10 Literal,
11 cast,
12)
14from pydantic_core import core_schema 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
15from typing_extensions import Self 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
17from ..aliases import AliasGenerator 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
18from ..config import ConfigDict, ExtraValues, JsonDict, JsonEncoder, JsonSchemaExtraCallable 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
19from ..errors import PydanticUserError 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
20from ..warnings import PydanticDeprecatedSince20, PydanticDeprecatedSince210 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
22if not TYPE_CHECKING: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
23 # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915
24 # and https://youtrack.jetbrains.com/issue/PY-51428
25 DeprecationWarning = PydanticDeprecatedSince20 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
27if TYPE_CHECKING: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
28 from .._internal._schema_generation_shared import GenerateSchema
29 from ..fields import ComputedFieldInfo, FieldInfo
31DEPRECATION_MESSAGE = 'Support for class-based `config` is deprecated, use ConfigDict instead.' 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
34class ConfigWrapper: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
35 """Internal wrapper for Config which exposes ConfigDict items as attributes."""
37 __slots__ = ('config_dict',) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
39 config_dict: ConfigDict 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
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 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
44 str_to_lower: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
45 str_to_upper: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
46 str_strip_whitespace: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
47 str_min_length: int 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
48 str_max_length: int | None 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
49 extra: ExtraValues | None 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
50 frozen: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
51 populate_by_name: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
52 use_enum_values: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
53 validate_assignment: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
54 arbitrary_types_allowed: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
55 from_attributes: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
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 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
59 alias_generator: Callable[[str], str] | AliasGenerator | None 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
60 model_title_generator: Callable[[type], str] | None 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
61 field_title_generator: Callable[[str, FieldInfo | ComputedFieldInfo], str] | None 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
62 ignored_types: tuple[type, ...] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
63 allow_inf_nan: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
64 json_schema_extra: JsonDict | JsonSchemaExtraCallable | None 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
65 json_encoders: dict[type[object], JsonEncoder] | None 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
67 # new in V2
68 strict: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
69 # whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never'
70 revalidate_instances: Literal['always', 'never', 'subclass-instances'] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
71 ser_json_timedelta: Literal['iso8601', 'float'] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
72 ser_json_bytes: Literal['utf8', 'base64', 'hex'] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
73 val_json_bytes: Literal['utf8', 'base64', 'hex'] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
74 ser_json_inf_nan: Literal['null', 'constants', 'strings'] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
75 # whether to validate default values during validation, default False
76 validate_default: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
77 validate_return: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
78 protected_namespaces: tuple[str | Pattern[str], ...] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
79 hide_input_in_errors: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
80 defer_build: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
81 plugin_settings: dict[str, object] | None 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
82 schema_generator: type[GenerateSchema] | None 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
83 json_schema_serialization_defaults_required: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
84 json_schema_mode_override: Literal['validation', 'serialization', None] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
85 coerce_numbers_to_str: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
86 regex_engine: Literal['rust-regex', 'python-re'] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
87 validation_error_cause: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
88 use_attribute_docstrings: bool 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
89 cache_strings: bool | Literal['all', 'keys', 'none'] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
91 def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, check: bool = True): 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
92 if check: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
93 self.config_dict = prepare_config(config) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
94 else:
95 self.config_dict = cast(ConfigDict, config) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
97 @classmethod 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
98 def for_model(cls, bases: tuple[type[Any], ...], namespace: dict[str, Any], kwargs: dict[str, Any]) -> Self: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
99 """Build a new `ConfigWrapper` instance for a `BaseModel`.
101 The config wrapper built based on (in descending order of priority):
102 - options from `kwargs`
103 - options from the `namespace`
104 - options from the base classes (`bases`)
106 Args:
107 bases: A tuple of base classes.
108 namespace: The namespace of the class being created.
109 kwargs: The kwargs passed to the class being created.
111 Returns:
112 A `ConfigWrapper` instance for `BaseModel`.
113 """
114 config_new = ConfigDict() 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
115 for base in bases: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
116 config = getattr(base, 'model_config', None) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
117 if config: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
118 config_new.update(config.copy()) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
120 config_class_from_namespace = namespace.get('Config') 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
121 config_dict_from_namespace = namespace.get('model_config') 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
123 raw_annotations = namespace.get('__annotations__', {}) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
124 if raw_annotations.get('model_config') and config_dict_from_namespace is None: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
125 raise PydanticUserError( 1abcdefghijklmnopqrstuvwxyzABCDEF
126 '`model_config` cannot be used as a model field name. Use `model_config` for model configuration.',
127 code='model-config-invalid-field-name',
128 )
130 if config_class_from_namespace and config_dict_from_namespace: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
131 raise PydanticUserError('"Config" and "model_config" cannot be used together', code='config-both') 1abcdefghijklmnopqrstuvwxyzABCDEF
133 config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
135 config_new.update(config_from_namespace) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
137 for k in list(kwargs.keys()): 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
138 if k in config_keys: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
139 config_new[k] = kwargs.pop(k) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
141 return cls(config_new) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
143 # we don't show `__getattr__` to type checkers so missing attributes cause errors
144 if not TYPE_CHECKING: # pragma: no branch 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
146 def __getattr__(self, name: str) -> Any: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
147 try: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
148 return self.config_dict[name] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
149 except KeyError: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
150 try: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
151 return config_defaults[name] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
152 except KeyError: 1abcdefghijklmnopqrstuvwxyzABCDEF
153 raise AttributeError(f'Config has no attribute {name!r}') from None 1abcdefghijklmnopqrstuvwxyzABCDEF
155 def core_config(self, title: str | None) -> core_schema.CoreConfig: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
156 """Create a pydantic-core config.
158 We don't use getattr here since we don't want to populate with defaults.
160 Args:
161 title: The title to use if not set in config.
163 Returns:
164 A `CoreConfig` object created from config.
165 """
166 config = self.config_dict 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
168 if config.get('schema_generator') is not None: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
169 warnings.warn( 1abcdefghijklmnopqrstuvwxyzABCDEF
170 'The `schema_generator` setting has been deprecated since v2.10. This setting no longer has any effect.',
171 PydanticDeprecatedSince210,
172 stacklevel=2,
173 )
175 return core_schema.CoreConfig( 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
176 **{ # pyright: ignore[reportArgumentType]
177 k: v
178 for k, v in (
179 ('title', config.get('title') or title or None),
180 ('extra_fields_behavior', config.get('extra')),
181 ('allow_inf_nan', config.get('allow_inf_nan')),
182 ('populate_by_name', config.get('populate_by_name')),
183 ('str_strip_whitespace', config.get('str_strip_whitespace')),
184 ('str_to_lower', config.get('str_to_lower')),
185 ('str_to_upper', config.get('str_to_upper')),
186 ('strict', config.get('strict')),
187 ('ser_json_timedelta', config.get('ser_json_timedelta')),
188 ('ser_json_bytes', config.get('ser_json_bytes')),
189 ('val_json_bytes', config.get('val_json_bytes')),
190 ('ser_json_inf_nan', config.get('ser_json_inf_nan')),
191 ('from_attributes', config.get('from_attributes')),
192 ('loc_by_alias', config.get('loc_by_alias')),
193 ('revalidate_instances', config.get('revalidate_instances')),
194 ('validate_default', config.get('validate_default')),
195 ('str_max_length', config.get('str_max_length')),
196 ('str_min_length', config.get('str_min_length')),
197 ('hide_input_in_errors', config.get('hide_input_in_errors')),
198 ('coerce_numbers_to_str', config.get('coerce_numbers_to_str')),
199 ('regex_engine', config.get('regex_engine')),
200 ('validation_error_cause', config.get('validation_error_cause')),
201 ('cache_strings', config.get('cache_strings')),
202 )
203 if v is not None
204 }
205 )
207 def __repr__(self): 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
208 c = ', '.join(f'{k}={v!r}' for k, v in self.config_dict.items()) 1abcdefghijklmnopqrstuvwxyzABCDEF
209 return f'ConfigWrapper({c})' 1abcdefghijklmnopqrstuvwxyzABCDEF
212class ConfigWrapperStack: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
213 """A stack of `ConfigWrapper` instances."""
215 def __init__(self, config_wrapper: ConfigWrapper): 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
216 self._config_wrapper_stack: list[ConfigWrapper] = [config_wrapper] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
218 @property 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
219 def tail(self) -> ConfigWrapper: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
220 return self._config_wrapper_stack[-1] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
222 @contextmanager 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
223 def push(self, config_wrapper: ConfigWrapper | ConfigDict | None): 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
224 if config_wrapper is None: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
225 yield 1abcdefghijklmnopqrstuvwxyzABCDEF
226 return 1abcdefghijklmnopqrstuvwxyzABCDEF
228 if not isinstance(config_wrapper, ConfigWrapper): 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
229 config_wrapper = ConfigWrapper(config_wrapper, check=False) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
231 self._config_wrapper_stack.append(config_wrapper) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
232 try: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
233 yield 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
234 finally:
235 self._config_wrapper_stack.pop() 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
238config_defaults = ConfigDict( 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
239 title=None,
240 str_to_lower=False,
241 str_to_upper=False,
242 str_strip_whitespace=False,
243 str_min_length=0,
244 str_max_length=None,
245 # let the model / dataclass decide how to handle it
246 extra=None,
247 frozen=False,
248 populate_by_name=False,
249 use_enum_values=False,
250 validate_assignment=False,
251 arbitrary_types_allowed=False,
252 from_attributes=False,
253 loc_by_alias=True,
254 alias_generator=None,
255 model_title_generator=None,
256 field_title_generator=None,
257 ignored_types=(),
258 allow_inf_nan=True,
259 json_schema_extra=None,
260 strict=False,
261 revalidate_instances='never',
262 ser_json_timedelta='iso8601',
263 ser_json_bytes='utf8',
264 val_json_bytes='utf8',
265 ser_json_inf_nan='null',
266 validate_default=False,
267 validate_return=False,
268 protected_namespaces=('model_validate', 'model_dump'),
269 hide_input_in_errors=False,
270 json_encoders=None,
271 defer_build=False,
272 schema_generator=None,
273 plugin_settings=None,
274 json_schema_serialization_defaults_required=False,
275 json_schema_mode_override=None,
276 coerce_numbers_to_str=False,
277 regex_engine='rust-regex',
278 validation_error_cause=False,
279 use_attribute_docstrings=False,
280 cache_strings=True,
281)
284def prepare_config(config: ConfigDict | dict[str, Any] | type[Any] | None) -> ConfigDict: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
285 """Create a `ConfigDict` instance from an existing dict, a class (e.g. old class-based config) or None.
287 Args:
288 config: The input config.
290 Returns:
291 A ConfigDict object created from config.
292 """
293 if config is None: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
294 return ConfigDict() 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
296 if not isinstance(config, dict): 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
297 warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning) 1abcdefghijklmnopqrstuvwxyzABCDEF
298 config = {k: getattr(config, k) for k in dir(config) if not k.startswith('__')} 1abcdefghijklmnopqrstuvwxyzABCDEF
300 config_dict = cast(ConfigDict, config) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
301 check_deprecated(config_dict) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
302 return config_dict 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
305config_keys = set(ConfigDict.__annotations__.keys()) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
308V2_REMOVED_KEYS = { 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
309 'allow_mutation',
310 'error_msg_templates',
311 'fields',
312 'getter_dict',
313 'smart_union',
314 'underscore_attrs_are_private',
315 'json_loads',
316 'json_dumps',
317 'copy_on_model_validation',
318 'post_init_call',
319}
320V2_RENAMED_KEYS = { 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
321 'allow_population_by_field_name': 'populate_by_name',
322 'anystr_lower': 'str_to_lower',
323 'anystr_strip_whitespace': 'str_strip_whitespace',
324 'anystr_upper': 'str_to_upper',
325 'keep_untouched': 'ignored_types',
326 'max_anystr_length': 'str_max_length',
327 'min_anystr_length': 'str_min_length',
328 'orm_mode': 'from_attributes',
329 'schema_extra': 'json_schema_extra',
330 'validate_all': 'validate_default',
331}
334def check_deprecated(config_dict: ConfigDict) -> None: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
335 """Check for deprecated config keys and warn the user.
337 Args:
338 config_dict: The input config.
339 """
340 deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys() 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
341 deprecated_renamed_keys = V2_RENAMED_KEYS.keys() & config_dict.keys() 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
342 if deprecated_removed_keys or deprecated_renamed_keys: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF
343 renamings = {k: V2_RENAMED_KEYS[k] for k in sorted(deprecated_renamed_keys)} 1abcdefghijklmnopqrstuvwxyzABCDEF
344 renamed_bullets = [f'* {k!r} has been renamed to {v!r}' for k, v in renamings.items()] 1abcdefghijklmnopqrstuvwxyzABCDEF
345 removed_bullets = [f'* {k!r} has been removed' for k in sorted(deprecated_removed_keys)] 1abcdefghijklmnopqrstuvwxyzABCDEF
346 message = '\n'.join(['Valid config keys have changed in V2:'] + renamed_bullets + removed_bullets) 1abcdefghijklmnopqrstuvwxyzABCDEF
347 warnings.warn(message, UserWarning) 1abcdefghijklmnopqrstuvwxyzABCDEF