Coverage for /pythoncovmergedfiles/medio/medio/src/pydantic/pydantic/_internal/_config.py: 84%
95 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-27 07:38 +0000
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-27 07:38 +0000
1from __future__ import annotations as _annotations
3import warnings
4from typing import TYPE_CHECKING, Any, Callable, cast
6from pydantic_core import core_schema
7from typing_extensions import Literal, Self
9from ..config import ConfigDict, ExtraValues
10from ..errors import PydanticUserError
12DEPRECATION_MESSAGE = 'Support for class-based `config` is deprecated, use ConfigDict instead.'
15class ConfigWrapper:
16 """
17 Internal wrapper for Config which exposes ConfigDict items as attributes.
18 """
20 __slots__ = ('config_dict',)
22 config_dict: ConfigDict
24 # all annotations are copied directly from ConfigDict, and should be kept up to date, a test will fail if they
25 # stop matching
26 title: str | None
27 str_to_lower: bool
28 str_to_upper: bool
29 str_strip_whitespace: bool
30 str_min_length: int
31 str_max_length: int | None
32 extra: ExtraValues | None
33 frozen: bool
34 populate_by_name: bool
35 use_enum_values: bool
36 validate_assignment: bool
37 arbitrary_types_allowed: bool
38 undefined_types_warning: bool
39 from_attributes: bool
40 # whether to use the used alias (or first alias for "field required" errors) instead of field_names
41 # to construct error `loc`s, default True
42 loc_by_alias: bool
43 alias_generator: Callable[[str], str] | None
44 ignored_types: tuple[type, ...]
45 allow_inf_nan: bool
47 # new in V2
48 strict: bool
49 # whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never'
50 revalidate_instances: Literal['always', 'never', 'subclass-instances']
51 ser_json_timedelta: Literal['iso8601', 'float']
52 ser_json_bytes: Literal['utf8', 'base64']
53 # whether to validate default values during validation, default False
54 validate_default: bool
55 validate_return: bool
57 def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, check: bool = True):
58 if check:
59 self.config_dict = prepare_config(config)
60 else:
61 self.config_dict = cast(ConfigDict, config)
63 @classmethod
64 def for_model(cls, bases: tuple[type[Any], ...], namespace: dict[str, Any], kwargs: dict[str, Any]) -> Self:
65 """
66 Build a new `ConfigDict` instance for a `BaseModel` based on (from lowest to highest)
67 - options defined in base
68 - options defined in namespace
69 - options defined via kwargs
71 :param bases: tuple of base classes
72 :param namespace: namespace of the class being created
73 :param kwargs: kwargs passed to the class being created
74 """
76 config_new = ConfigDict()
77 for base in bases:
78 config = getattr(base, 'model_config', None)
79 if config:
80 config_new.update(config.copy())
82 config_class_from_namespace = namespace.get('Config')
83 config_dict_from_namespace = namespace.get('model_config')
85 if config_class_from_namespace and config_dict_from_namespace:
86 raise PydanticUserError('"Config" and "model_config" cannot be used together', code='config-both')
88 config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace)
90 if config_from_namespace is not None:
91 config_new.update(config_from_namespace)
93 for k in list(kwargs.keys()):
94 if k in config_keys:
95 config_new[k] = kwargs.pop(k)
97 return cls(config_new)
99 # we don't show `__getattr__` to type checkers so missing attributes cause errors
100 if not TYPE_CHECKING:
102 def __getattr__(self, name: str) -> Any:
103 try:
104 return self.config_dict[name]
105 except KeyError:
106 try:
107 return config_defaults[name]
108 except KeyError:
109 raise AttributeError(f'Config has no attribute {name!r}') from None
111 def core_config(self, obj: Any = None) -> core_schema.CoreConfig:
112 """
113 Create a pydantic-core config, `obj` is just used to populate `title` if not set in config.
115 We don't use getattr here since we don't want to populate with defaults.
116 """
117 extra = self.config_dict.get('extra')
118 core_config = core_schema.CoreConfig(
119 **core_schema.dict_not_none(
120 title=self.config_dict.get('title') or (obj and obj.__name__),
121 extra_fields_behavior=extra,
122 allow_inf_nan=self.config_dict.get('allow_inf_nan'),
123 populate_by_name=self.config_dict.get('populate_by_name'),
124 str_strip_whitespace=self.config_dict.get('str_strip_whitespace'),
125 str_to_lower=self.config_dict.get('str_to_lower'),
126 str_to_upper=self.config_dict.get('str_to_upper'),
127 strict=self.config_dict.get('strict'),
128 ser_json_timedelta=self.config_dict.get('ser_json_timedelta'),
129 ser_json_bytes=self.config_dict.get('ser_json_bytes'),
130 from_attributes=self.config_dict.get('from_attributes'),
131 loc_by_alias=self.config_dict.get('loc_by_alias'),
132 revalidate_instances=self.config_dict.get('revalidate_instances'),
133 validate_default=self.config_dict.get('validate_default'),
134 str_max_length=self.config_dict.get('str_max_length'),
135 str_min_length=self.config_dict.get('str_min_length'),
136 )
137 )
138 return core_config
140 def __repr__(self):
141 c = ', '.join(f'{k}={v!r}' for k, v in self.config_dict.items())
142 return f'ConfigWrapper({c})'
145config_defaults = ConfigDict(
146 title=None,
147 str_to_lower=False,
148 str_to_upper=False,
149 str_strip_whitespace=False,
150 str_min_length=0,
151 str_max_length=None,
152 # let the model / dataclass decide how to handle it
153 extra=None,
154 frozen=False,
155 populate_by_name=False,
156 use_enum_values=False,
157 validate_assignment=False,
158 arbitrary_types_allowed=False,
159 undefined_types_warning=True,
160 from_attributes=False,
161 loc_by_alias=True,
162 alias_generator=None,
163 ignored_types=(),
164 allow_inf_nan=True,
165 strict=False,
166 revalidate_instances='never',
167 ser_json_timedelta='iso8601',
168 ser_json_bytes='utf8',
169 validate_default=False,
170 validate_return=False,
171)
174def prepare_config(config: ConfigDict | dict[str, Any] | type[Any] | None) -> ConfigDict:
175 """
176 Create a `ConfigDict` instance from an existing dict, a class (e.g. old class-based config) or None.
177 """
178 if config is None:
179 return ConfigDict()
181 if not isinstance(config, dict):
182 warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning)
183 config = {k: getattr(config, k) for k in dir(config) if not k.startswith('__')}
185 config_dict = cast(ConfigDict, config)
186 check_deprecated(config_dict)
187 return config_dict
190config_keys = set(ConfigDict.__annotations__.keys())
193V2_REMOVED_KEYS = {
194 'allow_mutation',
195 'error_msg_templates',
196 'fields',
197 'getter_dict',
198 'schema_extra',
199 'smart_union',
200 'underscore_attrs_are_private',
201 'json_loads',
202 'json_dumps',
203 'json_encoders',
204 'copy_on_model_validation',
205 'post_init_call',
206}
207V2_RENAMED_KEYS = {
208 'allow_population_by_field_name': 'populate_by_name',
209 'anystr_lower': 'str_to_lower',
210 'anystr_strip_whitespace': 'str_strip_whitespace',
211 'anystr_upper': 'str_to_upper',
212 'keep_untouched': 'ignored_types',
213 'max_anystr_length': 'str_max_length',
214 'min_anystr_length': 'str_min_length',
215 'orm_mode': 'from_attributes',
216 'validate_all': 'validate_default',
217}
220def check_deprecated(config_dict: ConfigDict) -> None:
221 """
222 Check for depreciated config keys and warn the user.
223 """
224 deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys()
225 deprecated_renamed_keys = V2_RENAMED_KEYS.keys() & config_dict.keys()
226 if deprecated_removed_keys or deprecated_renamed_keys:
227 renamings = {k: V2_RENAMED_KEYS[k] for k in sorted(deprecated_renamed_keys)}
228 renamed_bullets = [f'* {k!r} has been renamed to {v!r}' for k, v in renamings.items()]
229 removed_bullets = [f'* {k!r} has been removed' for k in sorted(deprecated_removed_keys)]
230 message = '\n'.join(['Valid config keys have changed in V2:'] + renamed_bullets + removed_bullets)
231 warnings.warn(message, UserWarning)