Files
GITEA-MCP/.venv/lib/python3.11/site-packages/pydantic/_internal/_config.py
T
2026-10-21 13:04:35 +01:00

409 lines
15 KiB
Python

from __future__ import annotations as _annotations
import warnings
from collections.abc import Callable
from contextlib import contextmanager
from re import Pattern
from typing import (
TYPE_CHECKING,
Any,
Literal,
cast,
)
from pydantic_core import core_schema
from typing_extensions import Self
from ..aliases import AliasGenerator
from ..config import ConfigDict, ExtraValues, JsonDict, JsonEncoder, JsonSchemaExtraCallable
from ..errors import PydanticUserError
from ..warnings import PydanticDeprecatedSince20, PydanticDeprecatedSince210
if TYPE_CHECKING:
from .._internal._schema_generation_shared import GenerateSchema
from ..fields import ComputedFieldInfo, FieldInfo
DEPRECATION_MESSAGE = 'Support for class-based `config` is deprecated, use ConfigDict instead.'
# Maps the `ConfigDict` keys that have a `CoreConfig` counterpart to the corresponding
# `CoreConfig` key (`'title'` is omitted as it requires special handling, see `ConfigWrapper.core_config()`):
_config_dict_to_core_config_key: dict[str, str] = {
'extra': 'extra_fields_behavior',
'allow_inf_nan': 'allow_inf_nan',
'str_strip_whitespace': 'str_strip_whitespace',
'str_to_lower': 'str_to_lower',
'str_to_upper': 'str_to_upper',
'strict': 'strict',
'ser_json_timedelta': 'ser_json_timedelta',
'ser_json_temporal': 'ser_json_temporal',
'val_temporal_unit': 'val_temporal_unit',
'ser_json_bytes': 'ser_json_bytes',
'val_json_bytes': 'val_json_bytes',
'ser_json_inf_nan': 'ser_json_inf_nan',
'from_attributes': 'from_attributes',
'loc_by_alias': 'loc_by_alias',
'revalidate_instances': 'revalidate_instances',
'validate_default': 'validate_default',
'str_max_length': 'str_max_length',
'str_min_length': 'str_min_length',
'hide_input_in_errors': 'hide_input_in_errors',
'coerce_numbers_to_str': 'coerce_numbers_to_str',
'regex_engine': 'regex_engine',
'validation_error_cause': 'validation_error_cause',
'cache_strings': 'cache_strings',
'validate_by_alias': 'validate_by_alias',
'validate_by_name': 'validate_by_name',
'serialize_by_alias': 'serialize_by_alias',
'url_preserve_empty_path': 'url_preserve_empty_path',
'polymorphic_serialization': 'polymorphic_serialization',
}
class ConfigWrapper:
"""Internal wrapper for Config which exposes ConfigDict items as attributes."""
__slots__ = ('config_dict', 'effective_config')
config_dict: ConfigDict
"""The configuration as declared by the user.
This is the mapping exposed as the `model_config` attribute of models (and the
`__pydantic_config__` attribute of dataclasses), and the one merged with the
configuration of the bases when subclassing. As such, it must never be mutated.
"""
effective_config: ConfigDict
"""The configuration actually in effect, with the deprecated settings applied.
This is what every consumer of the configuration should read (both the attribute
access on this wrapper and `core_config()` do), as `config_dict` alone does not
account for the deprecated settings (see `_build_effective_config()`).
"""
# all annotations are copied directly from ConfigDict, and should be kept up to date, a test will fail if they
# stop matching
title: str | None
str_to_lower: bool
str_to_upper: bool
str_strip_whitespace: bool
str_min_length: int
str_max_length: int | None
extra: ExtraValues | None
frozen: bool
populate_by_name: bool
use_enum_values: bool
validate_assignment: bool
arbitrary_types_allowed: bool
from_attributes: bool
# whether to use the actual key provided in the data (e.g. alias or first alias for "field required" errors) instead of field_names
# to construct error `loc`s, default `True`
loc_by_alias: bool
alias_generator: Callable[[str], str] | AliasGenerator | None
model_title_generator: Callable[[type], str] | None
field_title_generator: Callable[[str, FieldInfo | ComputedFieldInfo], str] | None
ignored_types: tuple[type, ...]
allow_inf_nan: bool
json_schema_extra: JsonDict | JsonSchemaExtraCallable | None
json_encoders: dict[type[object], JsonEncoder] | None
# new in V2
strict: bool
# whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never'
revalidate_instances: Literal['always', 'never', 'subclass-instances']
ser_json_timedelta: Literal['iso8601', 'float']
ser_json_temporal: Literal['iso8601', 'seconds', 'milliseconds']
val_temporal_unit: Literal['seconds', 'milliseconds', 'infer']
ser_json_bytes: Literal['utf8', 'base64', 'hex']
val_json_bytes: Literal['utf8', 'base64', 'hex']
ser_json_inf_nan: Literal['null', 'constants', 'strings']
# whether to validate default values during validation, default False
validate_default: bool
validate_return: bool
protected_namespaces: tuple[str | Pattern[str], ...]
hide_input_in_errors: bool
defer_build: bool
plugin_settings: dict[str, object] | None
schema_generator: type[GenerateSchema] | None
json_schema_serialization_defaults_required: bool
json_schema_mode_override: Literal['validation', 'serialization', None]
coerce_numbers_to_str: bool
regex_engine: Literal['rust-regex', 'python-re']
validation_error_cause: bool
use_attribute_docstrings: bool
cache_strings: bool | Literal['all', 'keys', 'none']
validate_by_alias: bool
validate_by_name: bool
serialize_by_alias: bool
url_preserve_empty_path: bool
polymorphic_serialization: bool
def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, check: bool = True):
if check:
self.config_dict = prepare_config(config)
else:
self.config_dict = cast(ConfigDict, config)
self.effective_config = _build_effective_config(self.config_dict)
@classmethod
def for_model(
cls,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
kwargs: dict[str, Any],
) -> Self:
"""Build a new `ConfigWrapper` instance for a `BaseModel`.
The config wrapper built based on (in descending order of priority):
- options from `kwargs`
- options from the `namespace`
- options from the base classes (`bases`)
Args:
bases: A tuple of base classes.
namespace: The namespace of the class being created.
kwargs: The kwargs passed to the class being created.
Returns:
A `ConfigWrapper` instance for `BaseModel`.
"""
config_new = ConfigDict()
for base in bases:
config = getattr(base, 'model_config', None)
if config:
config_new.update(config.copy())
config_class_from_namespace = namespace.get('Config')
config_dict_from_namespace = namespace.get('model_config')
if config_class_from_namespace and config_dict_from_namespace:
raise PydanticUserError('"Config" and "model_config" cannot be used together', code='config-both')
config_from_namespace = config_dict_from_namespace or prepare_config(config_class_from_namespace)
config_new.update(config_from_namespace)
for k in list(kwargs.keys()):
if k in config_keys:
config_new[k] = kwargs.pop(k)
return cls(config_new)
# we don't show `__getattr__` to type checkers so missing attributes cause errors
if not TYPE_CHECKING: # pragma: no branch
def __getattr__(self, name: str) -> Any:
try:
return self.effective_config[name]
except KeyError:
try:
return config_defaults[name]
except KeyError:
raise AttributeError(f'Config has no attribute {name!r}') from None
def core_config(self, title: str | None) -> core_schema.CoreConfig:
"""Create a pydantic-core config.
We don't use getattr here since we don't want to populate with defaults.
Args:
title: The title to use if not set in config.
Returns:
A `CoreConfig` object created from config.
"""
config = self.effective_config
if not config:
# Fast path for the common case of an empty (default) config:
return core_schema.CoreConfig(title=title) if title else core_schema.CoreConfig()
core_config_values: dict[str, Any] = {}
for k, v in config.items():
if v is not None and (core_config_key := _config_dict_to_core_config_key.get(k)) is not None:
core_config_values[core_config_key] = v
title = config.get('title') or title
if title:
core_config_values['title'] = title
return cast(core_schema.CoreConfig, core_config_values)
def __repr__(self):
c = ', '.join(f'{k}={v!r}' for k, v in self.config_dict.items())
return f'ConfigWrapper({c})'
class ConfigWrapperStack:
"""A stack of `ConfigWrapper` instances."""
def __init__(self, config_wrapper: ConfigWrapper):
self._config_wrapper_stack: list[ConfigWrapper] = [config_wrapper]
@property
def tail(self) -> ConfigWrapper:
return self._config_wrapper_stack[-1]
@contextmanager
def push(self, config_wrapper: ConfigWrapper | ConfigDict | None):
if config_wrapper is None:
yield
return
if not isinstance(config_wrapper, ConfigWrapper):
config_wrapper = ConfigWrapper(config_wrapper, check=False)
self._config_wrapper_stack.append(config_wrapper)
try:
yield
finally:
self._config_wrapper_stack.pop()
config_defaults = ConfigDict(
title=None,
str_to_lower=False,
str_to_upper=False,
str_strip_whitespace=False,
str_min_length=0,
str_max_length=None,
# let the model / dataclass decide how to handle it
extra=None,
frozen=False,
populate_by_name=False,
use_enum_values=False,
validate_assignment=False,
arbitrary_types_allowed=False,
from_attributes=False,
loc_by_alias=True,
alias_generator=None,
model_title_generator=None,
field_title_generator=None,
ignored_types=(),
allow_inf_nan=True,
json_schema_extra=None,
strict=False,
revalidate_instances='never',
ser_json_timedelta='iso8601',
ser_json_temporal='iso8601',
val_temporal_unit='infer',
ser_json_bytes='utf8',
val_json_bytes='utf8',
ser_json_inf_nan='null',
validate_default=False,
validate_return=False,
protected_namespaces=('model_validate', 'model_dump'),
hide_input_in_errors=False,
json_encoders=None,
defer_build=False,
schema_generator=None,
plugin_settings=None,
json_schema_serialization_defaults_required=False,
json_schema_mode_override=None,
coerce_numbers_to_str=False,
regex_engine='rust-regex',
validation_error_cause=False,
use_attribute_docstrings=False,
cache_strings=True,
validate_by_alias=True,
validate_by_name=False,
serialize_by_alias=False,
url_preserve_empty_path=False,
polymorphic_serialization=False,
)
def _build_effective_config(config: ConfigDict) -> ConfigDict:
"""Build the configuration in effect from the configuration as declared.
Raises:
PydanticUserError: If validation can happen by neither alias nor name.
"""
if config.get('validate_by_name') is None:
if (populate_by_name := config.get('populate_by_name')) is not None:
config = cast(ConfigDict, {**config, 'validate_by_alias': True, 'validate_by_name': populate_by_name})
elif config.get('validate_by_alias') is False:
config = cast(ConfigDict, {**config, 'validate_by_name': True})
if (not config.get('validate_by_alias', True)) and (not config.get('validate_by_name', False)):
raise PydanticUserError(
'At least one of `validate_by_alias` or `validate_by_name` must be set to True.',
code='validate-by-alias-and-name-false',
)
return config
def prepare_config(config: ConfigDict | dict[str, Any] | type[Any] | None) -> ConfigDict:
"""Create a `ConfigDict` instance from an existing dict, a class (e.g. old class-based config) or None.
Args:
config: The input config.
Returns:
A ConfigDict object created from config.
"""
if config is None:
return ConfigDict()
if not isinstance(config, dict):
warnings.warn(DEPRECATION_MESSAGE, PydanticDeprecatedSince20, stacklevel=4)
config = {k: getattr(config, k) for k in dir(config) if not k.startswith('__')}
else:
# Copy, so that the `ConfigWrapper` owns its configuration and can never
# mutate the mapping provided by the user:
config = config.copy()
config_dict = cast(ConfigDict, config)
check_deprecated(config_dict)
return config_dict
config_keys = set(ConfigDict.__annotations__.keys())
V2_REMOVED_KEYS = {
'allow_mutation',
'error_msg_templates',
'fields',
'getter_dict',
'smart_union',
'underscore_attrs_are_private',
'json_loads',
'json_dumps',
'copy_on_model_validation',
'post_init_call',
}
V2_RENAMED_KEYS = {
'allow_population_by_field_name': 'validate_by_name',
'anystr_lower': 'str_to_lower',
'anystr_strip_whitespace': 'str_strip_whitespace',
'anystr_upper': 'str_to_upper',
'keep_untouched': 'ignored_types',
'max_anystr_length': 'str_max_length',
'min_anystr_length': 'str_min_length',
'orm_mode': 'from_attributes',
'schema_extra': 'json_schema_extra',
'validate_all': 'validate_default',
}
def check_deprecated(config_dict: ConfigDict) -> None:
"""Check for deprecated config keys and warn the user.
Args:
config_dict: The input config.
"""
if config_dict.get('schema_generator') is not None:
warnings.warn(
'The `schema_generator` setting has been deprecated since v2.10. This setting no longer has any effect.',
PydanticDeprecatedSince210,
)
deprecated_removed_keys = V2_REMOVED_KEYS & config_dict.keys()
deprecated_renamed_keys = V2_RENAMED_KEYS.keys() & config_dict.keys()
if deprecated_removed_keys or deprecated_renamed_keys:
renamings = {k: V2_RENAMED_KEYS[k] for k in sorted(deprecated_renamed_keys)}
renamed_bullets = [f'* {k!r} has been renamed to {v!r}' for k, v in renamings.items()]
removed_bullets = [f'* {k!r} has been removed' for k in sorted(deprecated_removed_keys)]
message = '\n'.join(['Valid config keys have changed in V2:'] + renamed_bullets + removed_bullets)
warnings.warn(message, UserWarning)