_config.py 12 KB

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