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

1from __future__ import annotations as _annotations 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

2 

3import warnings 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

4from contextlib import contextmanager 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

5from typing import ( 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

6 TYPE_CHECKING, 

7 Any, 

8 Callable, 

9 cast, 

10) 

11 

12from pydantic_core import core_schema 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

13from typing_extensions import ( 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

14 Literal, 

15 Self, 

16) 

17 

18from ..aliases import AliasGenerator 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

19from ..config import ConfigDict, ExtraValues, JsonDict, JsonEncoder, JsonSchemaExtraCallable 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

20from ..errors import PydanticUserError 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

21from ..warnings import PydanticDeprecatedSince20 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

22 

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

27 

28if TYPE_CHECKING: 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

29 from .._internal._schema_generation_shared import GenerateSchema 

30 

31DEPRECATION_MESSAGE = 'Support for class-based `config` is deprecated, use ConfigDict instead.' 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

32 

33 

34class ConfigWrapper: 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

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

36 

37 __slots__ = ('config_dict',) 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

38 

39 config_dict: ConfigDict 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

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

64 

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

88 

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

94 

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

98 

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

103 

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. 

108 

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

117 

118 config_class_from_namespace = namespace.get('Config') 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

119 config_dict_from_namespace = namespace.get('model_config') 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

120 

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 ) 

127 

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

130 

131 config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace) 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

132 

133 config_new.update(config_from_namespace) 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

134 

135 for k in list(kwargs.keys()): 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

136 if k in config_keys: 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

137 config_new[k] = kwargs.pop(k) 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

138 

139 return cls(config_new) 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

140 

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

142 if not TYPE_CHECKING: # pragma: no branch 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

143 

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

152 

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. 

155 

156 Pass `obj=None` if you do not want to attempt to infer the `title`. 

157 

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

159 

160 Args: 

161 obj: An object used to populate `title` if not set in config. 

162 

163 Returns: 

164 A `CoreConfig` object created from config. 

165 """ 

166 

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

169 

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

197 

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

201 

202 

203class ConfigWrapperStack: 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

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

205 

206 def __init__(self, config_wrapper: ConfigWrapper): 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

207 self._config_wrapper_stack: list[ConfigWrapper] = [config_wrapper] 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

208 

209 @property 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

210 def tail(self) -> ConfigWrapper: 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

211 return self._config_wrapper_stack[-1] 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

212 

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

218 

219 if not isinstance(config_wrapper, ConfigWrapper): 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

220 config_wrapper = ConfigWrapper(config_wrapper, check=False) 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

221 

222 self._config_wrapper_stack.append(config_wrapper) 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

223 try: 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

224 yield 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

225 finally: 

226 self._config_wrapper_stack.pop() 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

227 

228 

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) 

271 

272 

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. 

275 

276 Args: 

277 config: The input config. 

278 

279 Returns: 

280 A ConfigDict object created from config. 

281 """ 

282 if config is None: 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

283 return ConfigDict() 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

284 

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

288 

289 config_dict = cast(ConfigDict, config) 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

290 check_deprecated(config_dict) 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

291 return config_dict 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

292 

293 

294config_keys = set(ConfigDict.__annotations__.keys()) 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

295 

296 

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} 

321 

322 

323def check_deprecated(config_dict: ConfigDict) -> None: 1abcdefghijklmnopqrstuvGHIJKLMNOwxyzABCDEF

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

325 

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