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