_config.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. from __future__ import annotations as _annotations
  2. import warnings
  3. from contextlib import contextmanager
  4. from re import Pattern
  5. from typing import (
  6. TYPE_CHECKING,
  7. Any,
  8. Callable,
  9. Literal,
  10. cast,
  11. )
  12. from pydantic_core import core_schema
  13. from typing_extensions import Self
  14. from ..aliases import AliasGenerator
  15. from ..config import ConfigDict, ExtraValues, JsonDict, JsonEncoder, JsonSchemaExtraCallable
  16. from ..errors import PydanticUserError
  17. from ..warnings import PydanticDeprecatedSince20, PydanticDeprecatedSince210
  18. if TYPE_CHECKING:
  19. from .._internal._schema_generation_shared import GenerateSchema
  20. from ..fields import ComputedFieldInfo, FieldInfo
  21. DEPRECATION_MESSAGE = 'Support for class-based `config` is deprecated, use ConfigDict instead.'
  22. class ConfigWrapper:
  23. """Internal wrapper for Config which exposes ConfigDict items as attributes."""
  24. __slots__ = ('config_dict',)
  25. config_dict: ConfigDict
  26. # all annotations are copied directly from ConfigDict, and should be kept up to date, a test will fail if they
  27. # stop matching
  28. title: str | None
  29. str_to_lower: bool
  30. str_to_upper: bool
  31. str_strip_whitespace: bool
  32. str_min_length: int
  33. str_max_length: int | None
  34. extra: ExtraValues | None
  35. frozen: bool
  36. populate_by_name: bool
  37. use_enum_values: bool
  38. validate_assignment: bool
  39. arbitrary_types_allowed: bool
  40. from_attributes: bool
  41. # whether to use the actual key provided in the data (e.g. alias or first alias for "field required" errors) instead of field_names
  42. # to construct error `loc`s, default `True`
  43. loc_by_alias: bool
  44. alias_generator: Callable[[str], str] | AliasGenerator | None
  45. model_title_generator: Callable[[type], str] | None
  46. field_title_generator: Callable[[str, FieldInfo | ComputedFieldInfo], str] | None
  47. ignored_types: tuple[type, ...]
  48. allow_inf_nan: bool
  49. json_schema_extra: JsonDict | JsonSchemaExtraCallable | None
  50. json_encoders: dict[type[object], JsonEncoder] | None
  51. # new in V2
  52. strict: bool
  53. # whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never'
  54. revalidate_instances: Literal['always', 'never', 'subclass-instances']
  55. ser_json_timedelta: Literal['iso8601', 'float']
  56. ser_json_temporal: Literal['iso8601', 'seconds', 'milliseconds']
  57. val_temporal_unit: Literal['seconds', 'milliseconds', 'infer']
  58. ser_json_bytes: Literal['utf8', 'base64', 'hex']
  59. val_json_bytes: Literal['utf8', 'base64', 'hex']
  60. ser_json_inf_nan: Literal['null', 'constants', 'strings']
  61. # whether to validate default values during validation, default False
  62. validate_default: bool
  63. validate_return: bool
  64. protected_namespaces: tuple[str | Pattern[str], ...]
  65. hide_input_in_errors: bool
  66. defer_build: bool
  67. plugin_settings: dict[str, object] | None
  68. schema_generator: type[GenerateSchema] | None
  69. json_schema_serialization_defaults_required: bool
  70. json_schema_mode_override: Literal['validation', 'serialization', None]
  71. coerce_numbers_to_str: bool
  72. regex_engine: Literal['rust-regex', 'python-re']
  73. validation_error_cause: bool
  74. use_attribute_docstrings: bool
  75. cache_strings: bool | Literal['all', 'keys', 'none']
  76. validate_by_alias: bool
  77. validate_by_name: bool
  78. serialize_by_alias: bool
  79. url_preserve_empty_path: bool
  80. def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, check: bool = True):
  81. if check:
  82. self.config_dict = prepare_config(config)
  83. else:
  84. self.config_dict = cast(ConfigDict, config)
  85. @classmethod
  86. def for_model(
  87. cls,
  88. bases: tuple[type[Any], ...],
  89. namespace: dict[str, Any],
  90. raw_annotations: dict[str, Any],
  91. kwargs: dict[str, Any],
  92. ) -> Self:
  93. """Build a new `ConfigWrapper` instance for a `BaseModel`.
  94. The config wrapper built based on (in descending order of priority):
  95. - options from `kwargs`
  96. - options from the `namespace`
  97. - options from the base classes (`bases`)
  98. Args:
  99. bases: A tuple of base classes.
  100. namespace: The namespace of the class being created.
  101. raw_annotations: The (non-evaluated) annotations of the model.
  102. kwargs: The kwargs passed to the class being created.
  103. Returns:
  104. A `ConfigWrapper` instance for `BaseModel`.
  105. """
  106. config_new = ConfigDict()
  107. for base in bases:
  108. config = getattr(base, 'model_config', None)
  109. if config:
  110. config_new.update(config.copy())
  111. config_class_from_namespace = namespace.get('Config')
  112. config_dict_from_namespace = namespace.get('model_config')
  113. if raw_annotations.get('model_config') and config_dict_from_namespace is None:
  114. raise PydanticUserError(
  115. '`model_config` cannot be used as a model field name. Use `model_config` for model configuration.',
  116. code='model-config-invalid-field-name',
  117. )
  118. if config_class_from_namespace and config_dict_from_namespace:
  119. raise PydanticUserError('"Config" and "model_config" cannot be used together', code='config-both')
  120. config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace)
  121. config_new.update(config_from_namespace)
  122. for k in list(kwargs.keys()):
  123. if k in config_keys:
  124. config_new[k] = kwargs.pop(k)
  125. return cls(config_new)
  126. # we don't show `__getattr__` to type checkers so missing attributes cause errors
  127. if not TYPE_CHECKING: # pragma: no branch
  128. def __getattr__(self, name: str) -> Any:
  129. try:
  130. return self.config_dict[name]
  131. except KeyError:
  132. try:
  133. return config_defaults[name]
  134. except KeyError:
  135. raise AttributeError(f'Config has no attribute {name!r}') from None
  136. def core_config(self, title: str | None) -> core_schema.CoreConfig:
  137. """Create a pydantic-core config.
  138. We don't use getattr here since we don't want to populate with defaults.
  139. Args:
  140. title: The title to use if not set in config.
  141. Returns:
  142. A `CoreConfig` object created from config.
  143. """
  144. config = self.config_dict
  145. if config.get('schema_generator') is not None:
  146. warnings.warn(
  147. 'The `schema_generator` setting has been deprecated since v2.10. This setting no longer has any effect.',
  148. PydanticDeprecatedSince210,
  149. stacklevel=2,
  150. )
  151. if (populate_by_name := config.get('populate_by_name')) is not None:
  152. # We include this patch for backwards compatibility purposes, but this config setting will be deprecated in v3.0, and likely removed in v4.0.
  153. # Thus, the above warning and this patch can be removed then as well.
  154. if config.get('validate_by_name') is None:
  155. config['validate_by_alias'] = True
  156. config['validate_by_name'] = populate_by_name
  157. # We dynamically patch validate_by_name to be True if validate_by_alias is set to False
  158. # and validate_by_name is not explicitly set.
  159. if config.get('validate_by_alias') is False and config.get('validate_by_name') is None:
  160. config['validate_by_name'] = True
  161. if (not config.get('validate_by_alias', True)) and (not config.get('validate_by_name', False)):
  162. raise PydanticUserError(
  163. 'At least one of `validate_by_alias` or `validate_by_name` must be set to True.',
  164. code='validate-by-alias-and-name-false',
  165. )
  166. return core_schema.CoreConfig(
  167. **{ # pyright: ignore[reportArgumentType]
  168. k: v
  169. for k, v in (
  170. ('title', config.get('title') or title or None),
  171. ('extra_fields_behavior', config.get('extra')),
  172. ('allow_inf_nan', config.get('allow_inf_nan')),
  173. ('str_strip_whitespace', config.get('str_strip_whitespace')),
  174. ('str_to_lower', config.get('str_to_lower')),
  175. ('str_to_upper', config.get('str_to_upper')),
  176. ('strict', config.get('strict')),
  177. ('ser_json_timedelta', config.get('ser_json_timedelta')),
  178. ('ser_json_temporal', config.get('ser_json_temporal')),
  179. ('val_temporal_unit', config.get('val_temporal_unit')),
  180. ('ser_json_bytes', config.get('ser_json_bytes')),
  181. ('val_json_bytes', config.get('val_json_bytes')),
  182. ('ser_json_inf_nan', config.get('ser_json_inf_nan')),
  183. ('from_attributes', config.get('from_attributes')),
  184. ('loc_by_alias', config.get('loc_by_alias')),
  185. ('revalidate_instances', config.get('revalidate_instances')),
  186. ('validate_default', config.get('validate_default')),
  187. ('str_max_length', config.get('str_max_length')),
  188. ('str_min_length', config.get('str_min_length')),
  189. ('hide_input_in_errors', config.get('hide_input_in_errors')),
  190. ('coerce_numbers_to_str', config.get('coerce_numbers_to_str')),
  191. ('regex_engine', config.get('regex_engine')),
  192. ('validation_error_cause', config.get('validation_error_cause')),
  193. ('cache_strings', config.get('cache_strings')),
  194. ('validate_by_alias', config.get('validate_by_alias')),
  195. ('validate_by_name', config.get('validate_by_name')),
  196. ('serialize_by_alias', config.get('serialize_by_alias')),
  197. ('url_preserve_empty_path', config.get('url_preserve_empty_path')),
  198. )
  199. if v is not None
  200. }
  201. )
  202. def __repr__(self):
  203. c = ', '.join(f'{k}={v!r}' for k, v in self.config_dict.items())
  204. return f'ConfigWrapper({c})'
  205. class ConfigWrapperStack:
  206. """A stack of `ConfigWrapper` instances."""
  207. def __init__(self, config_wrapper: ConfigWrapper):
  208. self._config_wrapper_stack: list[ConfigWrapper] = [config_wrapper]
  209. @property
  210. def tail(self) -> ConfigWrapper:
  211. return self._config_wrapper_stack[-1]
  212. @contextmanager
  213. def push(self, config_wrapper: ConfigWrapper | ConfigDict | None):
  214. if config_wrapper is None:
  215. yield
  216. return
  217. if not isinstance(config_wrapper, ConfigWrapper):
  218. config_wrapper = ConfigWrapper(config_wrapper, check=False)
  219. self._config_wrapper_stack.append(config_wrapper)
  220. try:
  221. yield
  222. finally:
  223. self._config_wrapper_stack.pop()
  224. config_defaults = ConfigDict(
  225. title=None,
  226. str_to_lower=False,
  227. str_to_upper=False,
  228. str_strip_whitespace=False,
  229. str_min_length=0,
  230. str_max_length=None,
  231. # let the model / dataclass decide how to handle it
  232. extra=None,
  233. frozen=False,
  234. populate_by_name=False,
  235. use_enum_values=False,
  236. validate_assignment=False,
  237. arbitrary_types_allowed=False,
  238. from_attributes=False,
  239. loc_by_alias=True,
  240. alias_generator=None,
  241. model_title_generator=None,
  242. field_title_generator=None,
  243. ignored_types=(),
  244. allow_inf_nan=True,
  245. json_schema_extra=None,
  246. strict=False,
  247. revalidate_instances='never',
  248. ser_json_timedelta='iso8601',
  249. ser_json_temporal='iso8601',
  250. val_temporal_unit='infer',
  251. ser_json_bytes='utf8',
  252. val_json_bytes='utf8',
  253. ser_json_inf_nan='null',
  254. validate_default=False,
  255. validate_return=False,
  256. protected_namespaces=('model_validate', 'model_dump'),
  257. hide_input_in_errors=False,
  258. json_encoders=None,
  259. defer_build=False,
  260. schema_generator=None,
  261. plugin_settings=None,
  262. json_schema_serialization_defaults_required=False,
  263. json_schema_mode_override=None,
  264. coerce_numbers_to_str=False,
  265. regex_engine='rust-regex',
  266. validation_error_cause=False,
  267. use_attribute_docstrings=False,
  268. cache_strings=True,
  269. validate_by_alias=True,
  270. validate_by_name=False,
  271. serialize_by_alias=False,
  272. url_preserve_empty_path=False,
  273. )
  274. def prepare_config(config: ConfigDict | dict[str, Any] | type[Any] | None) -> ConfigDict:
  275. """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.
  278. Returns:
  279. A ConfigDict object created from config.
  280. """
  281. if config is None:
  282. return ConfigDict()
  283. if not isinstance(config, dict):
  284. warnings.warn(DEPRECATION_MESSAGE, PydanticDeprecatedSince20, stacklevel=4)
  285. config = {k: getattr(config, k) for k in dir(config) if not k.startswith('__')}
  286. config_dict = cast(ConfigDict, config)
  287. check_deprecated(config_dict)
  288. return config_dict
  289. config_keys = set(ConfigDict.__annotations__.keys())
  290. V2_REMOVED_KEYS = {
  291. 'allow_mutation',
  292. 'error_msg_templates',
  293. 'fields',
  294. 'getter_dict',
  295. 'smart_union',
  296. 'underscore_attrs_are_private',
  297. 'json_loads',
  298. 'json_dumps',
  299. 'copy_on_model_validation',
  300. 'post_init_call',
  301. }
  302. V2_RENAMED_KEYS = {
  303. 'allow_population_by_field_name': 'validate_by_name',
  304. 'anystr_lower': 'str_to_lower',
  305. 'anystr_strip_whitespace': 'str_strip_whitespace',
  306. 'anystr_upper': 'str_to_upper',
  307. 'keep_untouched': 'ignored_types',
  308. 'max_anystr_length': 'str_max_length',
  309. 'min_anystr_length': 'str_min_length',
  310. 'orm_mode': 'from_attributes',
  311. 'schema_extra': 'json_schema_extra',
  312. 'validate_all': 'validate_default',
  313. }
  314. def check_deprecated(config_dict: ConfigDict) -> None:
  315. """Check for deprecated config keys and warn the user.
  316. Args:
  317. config_dict: The input config.
  318. """
  319. deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys()
  320. deprecated_renamed_keys = V2_RENAMED_KEYS.keys() & config_dict.keys()
  321. if deprecated_removed_keys or deprecated_renamed_keys:
  322. renamings = {k: V2_RENAMED_KEYS[k] for k in sorted(deprecated_renamed_keys)}
  323. renamed_bullets = [f'* {k!r} has been renamed to {v!r}' for k, v in renamings.items()]
  324. removed_bullets = [f'* {k!r} has been removed' for k in sorted(deprecated_removed_keys)]
  325. message = '\n'.join(['Valid config keys have changed in V2:'] + renamed_bullets + removed_bullets)
  326. warnings.warn(message, UserWarning)