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