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

1from __future__ import annotations as _annotations 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

2 

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) 

13 

14from pydantic_core import core_schema 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

15from typing_extensions import Self 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

16 

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

21 

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

26 

27if TYPE_CHECKING: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

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.' 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

32 

33 

34class ConfigWrapper: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

35 """Internal wrapper for Config which exposes ConfigDict items as attributes.""" 

36 

37 __slots__ = ('config_dict',) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

38 

39 config_dict: ConfigDict 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

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 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

66 

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

90 

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

96 

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`. 

100 

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`) 

105 

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. 

110 

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

119 

120 config_class_from_namespace = namespace.get('Config') 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

121 config_dict_from_namespace = namespace.get('model_config') 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

122 

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 ) 

129 

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

132 

133 config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

134 

135 config_new.update(config_from_namespace) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

136 

137 for k in list(kwargs.keys()): 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

138 if k in config_keys: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

139 config_new[k] = kwargs.pop(k) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

140 

141 return cls(config_new) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

142 

143 # we don't show `__getattr__` to type checkers so missing attributes cause errors 

144 if not TYPE_CHECKING: # pragma: no branch 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

145 

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

154 

155 def core_config(self, title: str | None) -> core_schema.CoreConfig: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

156 """Create a pydantic-core config. 

157 

158 We don't use getattr here since we don't want to populate with defaults. 

159 

160 Args: 

161 title: The title to use if not set in config. 

162 

163 Returns: 

164 A `CoreConfig` object created from config. 

165 """ 

166 config = self.config_dict 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

167 

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 ) 

174 

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 ) 

206 

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

210 

211 

212class ConfigWrapperStack: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

213 """A stack of `ConfigWrapper` instances.""" 

214 

215 def __init__(self, config_wrapper: ConfigWrapper): 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

216 self._config_wrapper_stack: list[ConfigWrapper] = [config_wrapper] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

217 

218 @property 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

219 def tail(self) -> ConfigWrapper: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

220 return self._config_wrapper_stack[-1] 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

221 

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

227 

228 if not isinstance(config_wrapper, ConfigWrapper): 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

229 config_wrapper = ConfigWrapper(config_wrapper, check=False) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

230 

231 self._config_wrapper_stack.append(config_wrapper) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

232 try: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

233 yield 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

234 finally: 

235 self._config_wrapper_stack.pop() 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

236 

237 

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) 

282 

283 

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. 

286 

287 Args: 

288 config: The input config. 

289 

290 Returns: 

291 A ConfigDict object created from config. 

292 """ 

293 if config is None: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

294 return ConfigDict() 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

295 

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

299 

300 config_dict = cast(ConfigDict, config) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

301 check_deprecated(config_dict) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

302 return config_dict 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

303 

304 

305config_keys = set(ConfigDict.__annotations__.keys()) 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

306 

307 

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} 

332 

333 

334def check_deprecated(config_dict: ConfigDict) -> None: 1abcdefghijklmnopqrstuvGHIJKLMwxyzABCDEF

335 """Check for deprecated config keys and warn the user. 

336 

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