添加docker版drawio配置

This commit is contained in:
lujingze committed 2026-07-03 09:23:24 +00:00
1 parent a5d9198bba
commit f73806cfb0
1297 files changed
+240603

No files matched your search

@@ -0,0 +1,386 @@
from __future__ import annotations as _annotations
import warnings
from contextlib import contextmanager
from re import Pattern
from typing import (
TYPE_CHECKING,
Any,
Callable,
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.'
class ConfigWrapper:
"""Internal wrapper for Config which exposes ConfigDict items as attributes."""
__slots__ = ('config_dict',)
config_dict: ConfigDict
# 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)
@classmethod
def for_model(
cls,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
raw_annotations: 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.
raw_annotations: The (non-evaluated) annotations of the model.
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 raw_annotations.get('model_config') and config_dict_from_namespace is None:
raise PydanticUserError(
'`model_config` cannot be used as a model field name. Use `model_config` for model configuration.',
code='model-config-invalid-field-name',
)
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.config_dict[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.config_dict
if config.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,
stacklevel=2,
)
if (populate_by_name := config.get('populate_by_name')) is not None:
# We include this patch for backwards compatibility purposes, but this config setting will be deprecated in v3.0, and likely removed in v4.0.
# Thus, the above warning and this patch can be removed then as well.
if config.get('validate_by_name') is None:
config['validate_by_alias'] = True
config['validate_by_name'] = populate_by_name
# We dynamically patch validate_by_name to be True if validate_by_alias is set to False
# and validate_by_name is not explicitly set.
if config.get('validate_by_alias') is False and config.get('validate_by_name') is None:
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 core_schema.CoreConfig(
**{ # pyright: ignore[reportArgumentType]
k: v
for k, v in (
('title', config.get('title') or title or None),
('extra_fields_behavior', config.get('extra')),
('allow_inf_nan', config.get('allow_inf_nan')),
('str_strip_whitespace', config.get('str_strip_whitespace')),
('str_to_lower', config.get('str_to_lower')),
('str_to_upper', config.get('str_to_upper')),
('strict', config.get('strict')),
('ser_json_timedelta', config.get('ser_json_timedelta')),
('ser_json_temporal', config.get('ser_json_temporal')),
('val_temporal_unit', config.get('val_temporal_unit')),
('ser_json_bytes', config.get('ser_json_bytes')),
('val_json_bytes', config.get('val_json_bytes')),
('ser_json_inf_nan', config.get('ser_json_inf_nan')),
('from_attributes', config.get('from_attributes')),
('loc_by_alias', config.get('loc_by_alias')),
('revalidate_instances', config.get('revalidate_instances')),
('validate_default', config.get('validate_default')),
('str_max_length', config.get('str_max_length')),
('str_min_length', config.get('str_min_length')),
('hide_input_in_errors', config.get('hide_input_in_errors')),
('coerce_numbers_to_str', config.get('coerce_numbers_to_str')),
('regex_engine', config.get('regex_engine')),
('validation_error_cause', config.get('validation_error_cause')),
('cache_strings', config.get('cache_strings')),
('validate_by_alias', config.get('validate_by_alias')),
('validate_by_name', config.get('validate_by_name')),
('serialize_by_alias', config.get('serialize_by_alias')),
('url_preserve_empty_path', config.get('url_preserve_empty_path')),
('polymorphic_serialization', config.get('polymorphic_serialization')),
)
if v is not None
}
)
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 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('__')}
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.
"""
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)
@@ -0,0 +1,174 @@
"""Logic for V1 validators, e.g. `@validator` and `@root_validator`."""
from __future__ import annotations as _annotations
from inspect import Parameter, signature
from typing import Any, Union, cast
from pydantic_core import core_schema
from typing_extensions import Protocol
from ..errors import PydanticUserError
from ._utils import can_be_positional
class V1OnlyValueValidator(Protocol):
"""A simple validator, supported for V1 validators and V2 validators."""
def __call__(self, __value: Any) -> Any: ...
class V1ValidatorWithValues(Protocol):
"""A validator with `values` argument, supported for V1 validators and V2 validators."""
def __call__(self, __value: Any, values: dict[str, Any]) -> Any: ...
class V1ValidatorWithValuesKwOnly(Protocol):
"""A validator with keyword only `values` argument, supported for V1 validators and V2 validators."""
def __call__(self, __value: Any, *, values: dict[str, Any]) -> Any: ...
class V1ValidatorWithKwargs(Protocol):
"""A validator with `kwargs` argument, supported for V1 validators and V2 validators."""
def __call__(self, __value: Any, **kwargs: Any) -> Any: ...
class V1ValidatorWithValuesAndKwargs(Protocol):
"""A validator with `values` and `kwargs` arguments, supported for V1 validators and V2 validators."""
def __call__(self, __value: Any, values: dict[str, Any], **kwargs: Any) -> Any: ...
V1Validator = Union[
V1ValidatorWithValues, V1ValidatorWithValuesKwOnly, V1ValidatorWithKwargs, V1ValidatorWithValuesAndKwargs
]
def can_be_keyword(param: Parameter) -> bool:
return param.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
def make_generic_v1_field_validator(validator: V1Validator) -> core_schema.WithInfoValidatorFunction:
"""Wrap a V1 style field validator for V2 compatibility.
Args:
validator: The V1 style field validator.
Returns:
A wrapped V2 style field validator.
Raises:
PydanticUserError: If the signature is not supported or the parameters are
not available in Pydantic V2.
"""
sig = signature(validator)
needs_values_kw = False
for param_num, (param_name, parameter) in enumerate(sig.parameters.items()):
if can_be_keyword(parameter) and param_name in ('field', 'config'):
raise PydanticUserError(
'The `field` and `config` parameters are not available in Pydantic V2, '
'please use the `info` parameter instead.',
code='validator-field-config-info',
)
if parameter.kind is Parameter.VAR_KEYWORD:
needs_values_kw = True
elif can_be_keyword(parameter) and param_name == 'values':
needs_values_kw = True
elif can_be_positional(parameter) and param_num == 0:
# value
continue
elif parameter.default is Parameter.empty: # ignore params with defaults e.g. bound by functools.partial
raise PydanticUserError(
f'Unsupported signature for V1 style validator {validator}: {sig} is not supported.',
code='validator-v1-signature',
)
if needs_values_kw:
# (v, **kwargs), (v, values, **kwargs), (v, *, values, **kwargs) or (v, *, values)
val1 = cast(V1ValidatorWithValues, validator)
def wrapper1(value: Any, info: core_schema.ValidationInfo) -> Any:
return val1(value, values=info.data)
return wrapper1
else:
val2 = cast(V1OnlyValueValidator, validator)
def wrapper2(value: Any, _: core_schema.ValidationInfo) -> Any:
return val2(value)
return wrapper2
RootValidatorValues = dict[str, Any]
# technically tuple[model_dict, model_extra, fields_set] | tuple[dataclass_dict, init_vars]
RootValidatorFieldsTuple = tuple[Any, ...]
class V1RootValidatorFunction(Protocol):
"""A simple root validator, supported for V1 validators and V2 validators."""
def __call__(self, __values: RootValidatorValues) -> RootValidatorValues: ...
class V2CoreBeforeRootValidator(Protocol):
"""V2 validator with mode='before'."""
def __call__(self, __values: RootValidatorValues, __info: core_schema.ValidationInfo) -> RootValidatorValues: ...
class V2CoreAfterRootValidator(Protocol):
"""V2 validator with mode='after'."""
def __call__(
self, __fields_tuple: RootValidatorFieldsTuple, __info: core_schema.ValidationInfo
) -> RootValidatorFieldsTuple: ...
def make_v1_generic_root_validator(
validator: V1RootValidatorFunction, pre: bool
) -> V2CoreBeforeRootValidator | V2CoreAfterRootValidator:
"""Wrap a V1 style root validator for V2 compatibility.
Args:
validator: The V1 style field validator.
pre: Whether the validator is a pre validator.
Returns:
A wrapped V2 style validator.
"""
if pre is True:
# mode='before' for pydantic-core
def _wrapper1(values: RootValidatorValues, _: core_schema.ValidationInfo) -> RootValidatorValues:
return validator(values)
return _wrapper1
# mode='after' for pydantic-core
def _wrapper2(fields_tuple: RootValidatorFieldsTuple, _: core_schema.ValidationInfo) -> RootValidatorFieldsTuple:
if len(fields_tuple) == 2:
# dataclass, this is easy
values, init_vars = fields_tuple
values = validator(values)
return values, init_vars
else:
# ugly hack: to match v1 behaviour, we merge values and model_extra, then split them up based on fields
# afterwards
model_dict, model_extra, fields_set = fields_tuple
if model_extra:
fields = set(model_dict.keys())
model_dict.update(model_extra)
model_dict_new = validator(model_dict)
for k in list(model_dict_new.keys()):
if k not in fields:
model_extra[k] = model_dict_new.pop(k)
else:
model_dict_new = validator(model_dict)
return model_dict_new, model_extra, fields_set
return _wrapper2
@@ -0,0 +1,113 @@
"""Utilities related to attribute docstring extraction."""
from __future__ import annotations
import ast
import inspect
import sys
import textwrap
from typing import Any
class DocstringVisitor(ast.NodeVisitor):
def __init__(self) -> None:
super().__init__()
self.target: str | None = None
self.attrs: dict[str, str] = {}
self.previous_node_type: type[ast.AST] | None = None
def visit(self, node: ast.AST) -> Any:
node_result = super().visit(node)
self.previous_node_type = type(node)
return node_result
def visit_AnnAssign(self, node: ast.AnnAssign) -> Any:
if isinstance(node.target, ast.Name):
self.target = node.target.id
def visit_Expr(self, node: ast.Expr) -> Any:
if (
isinstance(node.value, ast.Constant)
and isinstance(node.value.value, str)
and self.previous_node_type is ast.AnnAssign
):
docstring = inspect.cleandoc(node.value.value)
if self.target:
self.attrs[self.target] = docstring
self.target = None
def _dedent_source_lines(source: list[str]) -> str:
# Required for nested class definitions, e.g. in a function block
dedent_source = textwrap.dedent(''.join(source))
if dedent_source.startswith((' ', '\t')):
# We are in the case where there's a dedented (usually multiline) string
# at a lower indentation level than the class itself. We wrap our class
# in a function as a workaround.
dedent_source = f'def dedent_workaround():\n{dedent_source}'
return dedent_source
def _extract_source_from_frame(cls: type[Any]) -> list[str] | None:
frame = inspect.currentframe()
while frame:
if inspect.getmodule(frame) is inspect.getmodule(cls):
lnum = frame.f_lineno
try:
lines, _ = inspect.findsource(frame)
except OSError: # pragma: no cover
# Source can't be retrieved (maybe because running in an interactive terminal),
# we don't want to error here.
pass
else:
block_lines = inspect.getblock(lines[lnum - 1 :])
dedent_source = _dedent_source_lines(block_lines)
try:
block_tree = ast.parse(dedent_source)
except SyntaxError:
pass
else:
stmt = block_tree.body[0]
if isinstance(stmt, ast.FunctionDef) and stmt.name == 'dedent_workaround':
# `_dedent_source_lines` wrapped the class around the workaround function
stmt = stmt.body[0]
if isinstance(stmt, ast.ClassDef) and stmt.name == cls.__name__:
return block_lines
frame = frame.f_back
def extract_docstrings_from_cls(cls: type[Any], use_inspect: bool = False) -> dict[str, str]:
"""Map model attributes and their corresponding docstring.
Args:
cls: The class of the Pydantic model to inspect.
use_inspect: Whether to skip usage of frames to find the object and use
the `inspect` module instead.
Returns:
A mapping containing attribute names and their corresponding docstring.
"""
if use_inspect or sys.version_info >= (3, 13):
# On Python < 3.13, `inspect.getsourcelines()` might not work as expected
# if two classes have the same name in the same source file.
# On Python 3.13+, it will use the new `__firstlineno__` class attribute,
# making it way more robust.
try:
source, _ = inspect.getsourcelines(cls)
except OSError: # pragma: no cover
return {}
else:
# TODO remove this implementation when we drop support for Python 3.12:
source = _extract_source_from_frame(cls)
if not source:
return {}
dedent_source = _dedent_source_lines(source)
visitor = DocstringVisitor()
visitor.visit(ast.parse(dedent_source))
return visitor.attrs
@@ -0,0 +1,729 @@
"""Private logic related to fields (the `Field()` function and `FieldInfo` class), and arguments to `Annotated`."""
from __future__ import annotations as _annotations
import dataclasses
import warnings
from collections.abc import Mapping
from functools import cache
from inspect import Parameter, ismethoddescriptor
from re import Pattern
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
from pydantic_core import PydanticUndefined
from typing_extensions import TypeIs
from typing_inspection.introspection import AnnotationSource
from pydantic import PydanticDeprecatedSince211
from pydantic.errors import PydanticUserError
from ..aliases import AliasGenerator
from . import _generics, _typing_extra
from ._config import ConfigWrapper
from ._docs_extraction import extract_docstrings_from_cls
from ._import_utils import import_cached_base_model, import_cached_field_info
from ._internal_dataclass import slots_true
from ._namespace_utils import NsResolver
from ._repr import Representation
from ._utils import can_be_positional, get_first_not_none
if TYPE_CHECKING:
from annotated_types import BaseMetadata
from ..fields import FieldInfo
from ..main import BaseModel
from ._dataclasses import PydanticDataclass, StandardDataclass
from ._decorators import DecoratorInfos
class PydanticMetadata(Representation):
"""Base class for annotation markers like `Strict`."""
__slots__ = ()
@dataclasses.dataclass(**slots_true) # TODO: make kw_only when we drop support for 3.9.
class PydanticExtraInfo:
# TODO: make use of PEP 747:
annotation: Any
complete: bool
def pydantic_general_metadata(**metadata: Any) -> BaseMetadata:
"""Create a new `_PydanticGeneralMetadata` class with the given metadata.
Args:
**metadata: The metadata to add.
Returns:
The new `_PydanticGeneralMetadata` class.
"""
return _general_metadata_cls()(metadata) # type: ignore
@cache
def _general_metadata_cls() -> type[BaseMetadata]:
"""Do it this way to avoid importing `annotated_types` at import time."""
from annotated_types import BaseMetadata
class _PydanticGeneralMetadata(PydanticMetadata, BaseMetadata):
"""Pydantic general metadata like `max_digits`."""
def __init__(self, metadata: Any):
self.__dict__ = metadata
return _PydanticGeneralMetadata # type: ignore
def _check_protected_namespaces(
protected_namespaces: tuple[str | Pattern[str], ...],
ann_name: str,
bases: tuple[type[Any], ...],
cls_name: str,
) -> None:
BaseModel = import_cached_base_model()
for protected_namespace in protected_namespaces:
ns_violation = False
if isinstance(protected_namespace, Pattern):
ns_violation = protected_namespace.match(ann_name) is not None
elif isinstance(protected_namespace, str):
ns_violation = ann_name.startswith(protected_namespace)
if ns_violation:
for b in bases:
if hasattr(b, ann_name):
if not (issubclass(b, BaseModel) and ann_name in getattr(b, '__pydantic_fields__', {})):
raise ValueError(
f'Field {ann_name!r} conflicts with member {getattr(b, ann_name)}'
f' of protected namespace {protected_namespace!r}.'
)
else:
valid_namespaces: list[str] = []
for pn in protected_namespaces:
if isinstance(pn, Pattern):
if not pn.match(ann_name):
valid_namespaces.append(f're.compile({pn.pattern!r})')
else:
if not ann_name.startswith(pn):
valid_namespaces.append(f"'{pn}'")
valid_namespaces_str = f'({", ".join(valid_namespaces)}{",)" if len(valid_namespaces) == 1 else ")"}'
warnings.warn(
f'Field {ann_name!r} in {cls_name!r} conflicts with protected namespace {protected_namespace!r}.\n\n'
f"You may be able to solve this by setting the 'protected_namespaces' configuration to {valid_namespaces_str}.",
UserWarning,
stacklevel=5,
)
def _update_fields_from_docstrings(cls: type[Any], fields: dict[str, FieldInfo], use_inspect: bool = False) -> None:
fields_docs = extract_docstrings_from_cls(cls, use_inspect=use_inspect)
for ann_name, field_info in fields.items():
if field_info.description is None and ann_name in fields_docs:
field_info.description = fields_docs[ann_name]
def _apply_field_title_generator_to_field_info(
title_generator: Callable[[str, FieldInfo], str],
field_name: str,
field_info: FieldInfo,
):
if field_info.title is None:
title = title_generator(field_name, field_info)
if not isinstance(title, str):
raise TypeError(f'field_title_generator {title_generator} must return str, not {title.__class__}')
field_info.title = title
def _apply_alias_generator_to_field_info(
alias_generator: Callable[[str], str] | AliasGenerator, field_name: str, field_info: FieldInfo
):
"""Apply an alias generator to aliases on a `FieldInfo` instance if appropriate.
Args:
alias_generator: A callable that takes a string and returns a string, or an `AliasGenerator` instance.
field_name: The name of the field from which to generate the alias.
field_info: The `FieldInfo` instance to which the alias generator is (maybe) applied.
"""
# Apply an alias_generator if
# 1. An alias is not specified
# 2. An alias is specified, but the priority is <= 1
if (
field_info.alias_priority is None
or field_info.alias_priority <= 1
or field_info.alias is None
or field_info.validation_alias is None
or field_info.serialization_alias is None
):
alias, validation_alias, serialization_alias = None, None, None
if isinstance(alias_generator, AliasGenerator):
alias, validation_alias, serialization_alias = alias_generator.generate_aliases(field_name)
elif callable(alias_generator):
alias = alias_generator(field_name)
if not isinstance(alias, str):
raise TypeError(f'alias_generator {alias_generator} must return str, not {alias.__class__}')
# if priority is not set, we set to 1
# which supports the case where the alias_generator from a child class is used
# to generate an alias for a field in a parent class
if field_info.alias_priority is None or field_info.alias_priority <= 1:
field_info.alias_priority = 1
# if the priority is 1, then we set the aliases to the generated alias
if field_info.alias_priority == 1:
field_info.serialization_alias = get_first_not_none(serialization_alias, alias)
field_info.validation_alias = get_first_not_none(validation_alias, alias)
field_info.alias = alias
# if any of the aliases are not set, then we set them to the corresponding generated alias
if field_info.alias is None:
field_info.alias = alias
if field_info.serialization_alias is None:
field_info.serialization_alias = get_first_not_none(serialization_alias, alias)
if field_info.validation_alias is None:
field_info.validation_alias = get_first_not_none(validation_alias, alias)
def update_field_from_config(config_wrapper: ConfigWrapper, field_name: str, field_info: FieldInfo) -> None:
"""Update the `FieldInfo` instance from the configuration set on the model it belongs to.
This will apply the title and alias generators from the configuration.
Args:
config_wrapper: The configuration from the model.
field_name: The field name the `FieldInfo` instance is attached to.
field_info: The `FieldInfo` instance to update.
"""
field_title_generator = field_info.field_title_generator or config_wrapper.field_title_generator
if field_title_generator is not None:
_apply_field_title_generator_to_field_info(field_title_generator, field_name, field_info)
if config_wrapper.alias_generator is not None:
_apply_alias_generator_to_field_info(config_wrapper.alias_generator, field_name, field_info)
_deprecated_method_names = {'dict', 'json', 'copy', '_iter', '_copy_and_set_values', '_calculate_keys'}
_deprecated_classmethod_names = {
'parse_obj',
'parse_raw',
'parse_file',
'from_orm',
'construct',
'schema',
'schema_json',
'validate',
'update_forward_refs',
'_get_value',
}
def collect_model_fields( # noqa: C901
cls: type[BaseModel],
config_wrapper: ConfigWrapper,
ns_resolver: NsResolver,
*,
typevars_map: Mapping[TypeVar, Any] | None = None,
) -> tuple[dict[str, FieldInfo], PydanticExtraInfo | None, set[str]]:
"""Collect the fields and class variables names of a nascent Pydantic model.
The fields collection process is *lenient*, meaning it won't error if string annotations
fail to evaluate. If this happens, the original annotation (and assigned value, if any)
is stored on the created `FieldInfo` instance.
The `rebuild_model_fields()` should be called at a later point (e.g. when rebuilding the model),
and will make use of these stored attributes.
Args:
cls: BaseModel or dataclass.
config_wrapper: The config wrapper instance.
ns_resolver: Namespace resolver to use when getting model annotations.
typevars_map: A dictionary mapping type variables to their concrete types.
Returns:
A three-tuple containing the model fields, the `PydanticExtraInfo` instance if the `__pydantic_extra__` annotation is set,
and class variables names.
Raises:
NameError:
- If there is a conflict between a field name and protected namespaces.
- If there is a field other than `root` in `RootModel`.
- If a field shadows an attribute in the parent model.
"""
FieldInfo_ = import_cached_field_info()
BaseModel_ = import_cached_base_model()
bases = cls.__bases__
parent_fields_lookup: dict[str, FieldInfo] = {}
for base in reversed(bases):
if model_fields := getattr(base, '__pydantic_fields__', None):
parent_fields_lookup.update(model_fields)
type_hints = _typing_extra.get_model_type_hints(cls, ns_resolver=ns_resolver)
# `cls_annotations` is only used to determine if an annotation comes from a parent class
cls_annotations = _typing_extra.safe_get_annotations(cls)
fields: dict[str, FieldInfo] = {}
class_vars: set[str] = set()
for ann_name, (ann_type, evaluated) in type_hints.items():
if ann_name == 'model_config':
# We never want to treat `model_config` as a field
# Note: we may need to change this logic if/when we introduce a `BareModel` class with no
# protected namespaces (where `model_config` might be allowed as a field name)
continue
_check_protected_namespaces(
protected_namespaces=config_wrapper.protected_namespaces,
ann_name=ann_name,
bases=bases,
cls_name=cls.__name__,
)
if _typing_extra.is_classvar_annotation(ann_type):
class_vars.add(ann_name)
continue
assigned_value = getattr(cls, ann_name, PydanticUndefined)
if assigned_value is not PydanticUndefined and (
# One of the deprecated instance methods was used as a field name (e.g. `dict()`):
any(getattr(BaseModel_, depr_name, None) is assigned_value for depr_name in _deprecated_method_names)
# One of the deprecated class methods was used as a field name (e.g. `schema()`):
or (
hasattr(assigned_value, '__func__')
and any(
getattr(getattr(BaseModel_, depr_name, None), '__func__', None) is assigned_value.__func__ # pyright: ignore[reportAttributeAccessIssue]
for depr_name in _deprecated_classmethod_names
)
)
):
# Then `assigned_value` would be the method, even though no default was specified:
assigned_value = PydanticUndefined
if not is_valid_field_name(ann_name):
continue
if cls.__pydantic_root_model__ and ann_name != 'root':
raise NameError(
f"Unexpected field with name {ann_name!r}; only 'root' is allowed as a field of a `RootModel`"
)
for base in bases:
if hasattr(base, ann_name):
if ann_name not in cls_annotations:
# Don't warn when a field exists in a parent class but has not been defined in the current class
continue
# when building a generic model with `MyModel[int]`, the generic_origin check makes sure we don't get
# "... shadows an attribute" warnings
generic_origin = getattr(cls, '__pydantic_generic_metadata__', {}).get('origin')
if base is generic_origin:
# Don't warn when "shadowing" of attributes in parametrized generics
continue
dataclass_fields = {
field.name for field in (dataclasses.fields(base) if dataclasses.is_dataclass(base) else ())
}
if ann_name in dataclass_fields:
# Don't warn when inheriting stdlib dataclasses whose fields are "shadowed" by defaults being set
# on the class instance.
continue
warnings.warn(
f'Field name "{ann_name}" in "{cls.__qualname__}" shadows an attribute in parent '
f'"{base.__qualname__}"',
UserWarning,
stacklevel=4,
)
if assigned_value is PydanticUndefined: # no assignment, just a plain annotation
if ann_name in cls_annotations or ann_name not in parent_fields_lookup:
# field is either:
# - present in the current model's annotations (and *not* from parent classes)
# - not found on any base classes; this seems to be caused by fields not getting
# generated due to models not being fully defined while initializing recursive models.
# Nothing stops us from just creating a `FieldInfo` for this type hint, so we do this.
field_info = FieldInfo_.from_annotation(ann_type, _source=AnnotationSource.CLASS)
field_info._original_annotation = ann_type
if not evaluated:
field_info._complete = False
# Store the original annotation that should be used to rebuild
# the field info later:
else:
# The field was present on one of the (possibly multiple) base classes, we make a copy directly from it.
parent_field_info = parent_fields_lookup[ann_name]._copy()
# The only case where substituting the type variables is relevant (i.e. when `typevars_map` is not empty)
# is when a generic class is parameterized (e.g. `MyGenericModel[int, str]`), which creates a new class object
# (unlike the stdlib genercis that create a generic alias). In this case, we are guaranteed to only have to copy
# from the origin/parent model (e.g. `MyGenericModel`).
if typevars_map:
field_info = _recreate_field_info(
parent_field_info, ns_resolver=ns_resolver, typevars_map=typevars_map, lenient=True
)
else:
field_info = parent_field_info
else: # An assigned value is present (either the default value, or a `Field()` function)
if isinstance(assigned_value, FieldInfo_) and ismethoddescriptor(assigned_value.default):
# `assigned_value` was fetched using `getattr`, which triggers a call to `__get__`
# for descriptors, so we do the same if the `= field(default=...)` form is used.
# Note that we only do this for method descriptors for now, we might want to
# extend this to any descriptor in the future (by simply checking for
# `hasattr(assigned_value.default, '__get__')`).
default = assigned_value.default.__get__(None, cls)
assigned_value.default = default
assigned_value._attributes_set['default'] = default
field_info = FieldInfo_.from_annotated_attribute(ann_type, assigned_value, _source=AnnotationSource.CLASS)
# Store the original annotation and assignment value that could be used to rebuild the field info later.
field_info._original_assignment = assigned_value
field_info._original_annotation = ann_type
if not evaluated:
field_info._complete = False
elif 'final' in field_info._qualifiers and not field_info.is_required():
warnings.warn(
f'Annotation {ann_name!r} is marked as final and has a default value. Pydantic treats {ann_name!r} as a '
'class variable, but it will be considered as a normal field in V3 to be aligned with dataclasses. If you '
f'still want {ann_name!r} to be considered as a class variable, annotate it as: `ClassVar[<type>] = <default>.`',
category=PydanticDeprecatedSince211,
# Incorrect when `create_model` is used, but the chance that final with a default is used is low in that case:
stacklevel=4,
)
class_vars.add(ann_name)
continue
# attributes which are fields are removed from the class namespace:
# 1. To match the behaviour of annotation-only fields
# 2. To avoid false positives in the NameError check above
try:
delattr(cls, ann_name)
except AttributeError:
pass # indicates the attribute was on a parent class
# Use cls.__dict__['__pydantic_decorators__'] instead of cls.__pydantic_decorators__
# to make sure the decorators have already been built for this exact class
decorators: DecoratorInfos = cls.__dict__['__pydantic_decorators__']
if ann_name in decorators.computed_fields:
raise TypeError(
f'Field {ann_name!r} of class {cls.__name__!r} overrides symbol of same name in a parent class. '
'This override with a computed_field is incompatible.'
)
fields[ann_name] = field_info
if field_info._complete:
# If not complete, this will be called in `rebuild_model_fields()`:
update_field_from_config(config_wrapper, ann_name, field_info)
if config_wrapper.use_attribute_docstrings:
_update_fields_from_docstrings(cls, fields)
pydantic_extra_info: PydanticExtraInfo | None = None
if '__pydantic_extra__' in type_hints:
ann, complete = type_hints['__pydantic_extra__']
pydantic_extra_info = PydanticExtraInfo(
annotation=ann,
complete=complete,
)
return fields, pydantic_extra_info, class_vars
def rebuild_model_fields(
cls: type[BaseModel],
*,
config_wrapper: ConfigWrapper,
ns_resolver: NsResolver,
typevars_map: Mapping[TypeVar, Any],
) -> tuple[dict[str, FieldInfo], PydanticExtraInfo | None]:
"""Rebuild the (already present) model fields by trying to reevaluate annotations.
This function should be called whenever a model with incomplete fields is encountered.
Returns:
A two-tuple, the first element being the rebuilt fields, the second element being
the rebuild `PydanticExtraInfo` instance, if available.
Raises:
NameError: If one of the annotations failed to evaluate.
Note:
This function *doesn't* mutate the model fields in place, as it can be called during
schema generation, where you don't want to mutate other model's fields.
"""
rebuilt_fields: dict[str, FieldInfo] = {}
with ns_resolver.push(cls):
for f_name, field_info in cls.__pydantic_fields__.items():
if field_info._complete:
rebuilt_fields[f_name] = field_info
else:
new_field = _recreate_field_info(
field_info, ns_resolver=ns_resolver, typevars_map=typevars_map, lenient=False
)
update_field_from_config(config_wrapper, f_name, new_field)
rebuilt_fields[f_name] = new_field
if cls.__pydantic_extra_info__ is not None and not cls.__pydantic_extra_info__.complete:
rebuilt_extra_info = PydanticExtraInfo(
annotation=_typing_extra.eval_type(
cls.__pydantic_extra_info__.annotation, *ns_resolver.types_namespace
),
complete=True,
)
else:
rebuilt_extra_info = cls.__pydantic_extra_info__
return rebuilt_fields, rebuilt_extra_info
def _recreate_field_info(
field_info: FieldInfo,
ns_resolver: NsResolver,
typevars_map: Mapping[TypeVar, Any],
*,
lenient: bool,
) -> FieldInfo:
FieldInfo_ = import_cached_field_info()
existing_desc = field_info.description
if lenient:
ann = _generics.replace_types(field_info._original_annotation, typevars_map)
ann, evaluated = _typing_extra.try_eval_type(
ann,
*ns_resolver.types_namespace,
)
else:
# Not the best pattern, maybe we could ship our own `eval_type()`,
# that would replace the type variables on the fly during evaluation.
ann = _typing_extra.eval_type(
field_info._original_annotation,
*ns_resolver.types_namespace,
)
ann = _generics.replace_types(ann, typevars_map)
ann = _typing_extra.eval_type(
ann,
*ns_resolver.types_namespace,
)
evaluated = True
if (assign := field_info._original_assignment) is PydanticUndefined:
new_field = FieldInfo_.from_annotation(ann, _source=AnnotationSource.CLASS)
else:
new_field = FieldInfo_.from_annotated_attribute(ann, assign, _source=AnnotationSource.CLASS)
new_field._original_assignment = assign
new_field._original_annotation = ann
# The description might come from the docstring if `use_attribute_docstrings` was `True`:
new_field.description = new_field.description if new_field.description is not None else existing_desc
if not evaluated:
new_field._complete = False
return new_field
def collect_dataclass_fields(
cls: type[StandardDataclass],
*,
config_wrapper: ConfigWrapper,
ns_resolver: NsResolver | None = None,
typevars_map: dict[Any, Any] | None = None,
) -> dict[str, FieldInfo]:
"""Collect the fields of a dataclass.
Args:
cls: dataclass.
config_wrapper: The config wrapper instance.
ns_resolver: Namespace resolver to use when getting dataclass annotations.
Defaults to an empty instance.
typevars_map: A dictionary mapping type variables to their concrete types.
Returns:
The dataclass fields.
"""
FieldInfo_ = import_cached_field_info()
fields: dict[str, FieldInfo] = {}
ns_resolver = ns_resolver or NsResolver()
dataclass_fields = cls.__dataclass_fields__
# The logic here is similar to `_typing_extra.get_cls_type_hints`,
# although we do it manually as stdlib dataclasses already have annotations
# collected in each class:
for base in reversed(cls.__mro__):
if not dataclasses.is_dataclass(base):
continue
with ns_resolver.push(base):
for ann_name, dataclass_field in dataclass_fields.items():
base_anns = _typing_extra.safe_get_annotations(base)
if ann_name not in base_anns:
# `__dataclass_fields__`contains every field, even the ones from base classes.
# Only collect the ones defined on `base`.
continue
globalns, localns = ns_resolver.types_namespace
ann_type, evaluated = _typing_extra.try_eval_type(dataclass_field.type, globalns, localns)
if _typing_extra.is_classvar_annotation(ann_type):
continue
if (
not dataclass_field.init
and dataclass_field.default is dataclasses.MISSING
and dataclass_field.default_factory is dataclasses.MISSING
):
# TODO: We should probably do something with this so that validate_assignment behaves properly
# Issue: https://github.com/pydantic/pydantic/issues/5470
continue
if isinstance(dataclass_field.default, FieldInfo_):
if dataclass_field.default.init_var:
if dataclass_field.default.init is False:
raise PydanticUserError(
f'Dataclass field {ann_name} has init=False and init_var=True, but these are mutually exclusive.',
code='clashing-init-and-init-var',
)
# TODO: same note as above re validate_assignment
continue
field_info = FieldInfo_.from_annotated_attribute(
ann_type, dataclass_field.default, _source=AnnotationSource.DATACLASS
)
field_info._original_assignment = dataclass_field.default
else:
field_info = FieldInfo_.from_annotated_attribute(
ann_type, dataclass_field, _source=AnnotationSource.DATACLASS
)
field_info._original_assignment = dataclass_field
if not evaluated:
field_info._complete = False
field_info._original_annotation = ann_type
fields[ann_name] = field_info
update_field_from_config(config_wrapper, ann_name, field_info)
if field_info.default is not PydanticUndefined and isinstance(
getattr(cls, ann_name, field_info), FieldInfo_
):
# We need this to fix the default when the "default" from __dataclass_fields__ is a pydantic.FieldInfo
setattr(cls, ann_name, field_info.default)
if typevars_map:
for field in fields.values():
# We don't pass any ns, as `field.annotation`
# was already evaluated. TODO: is this method relevant?
# Can't we juste use `_generics.replace_types`?
field.apply_typevars_map(typevars_map)
if config_wrapper.use_attribute_docstrings:
_update_fields_from_docstrings(
cls,
fields,
# We can't rely on the (more reliable) frame inspection method
# for stdlib dataclasses:
use_inspect=not hasattr(cls, '__is_pydantic_dataclass__'),
)
return fields
def rebuild_dataclass_fields(
cls: type[PydanticDataclass],
*,
config_wrapper: ConfigWrapper,
ns_resolver: NsResolver,
typevars_map: Mapping[TypeVar, Any],
) -> dict[str, FieldInfo]:
"""Rebuild the (already present) dataclass fields by trying to reevaluate annotations.
This function should be called whenever a dataclass with incomplete fields is encountered.
Raises:
NameError: If one of the annotations failed to evaluate.
Note:
This function *doesn't* mutate the dataclass fields in place, as it can be called during
schema generation, where you don't want to mutate other dataclass's fields.
"""
FieldInfo_ = import_cached_field_info()
rebuilt_fields: dict[str, FieldInfo] = {}
with ns_resolver.push(cls):
for f_name, field_info in cls.__pydantic_fields__.items():
if field_info._complete:
rebuilt_fields[f_name] = field_info
else:
existing_desc = field_info.description
ann = _typing_extra.eval_type(
field_info._original_annotation,
*ns_resolver.types_namespace,
)
ann = _generics.replace_types(ann, typevars_map)
new_field = FieldInfo_.from_annotated_attribute(
ann,
field_info._original_assignment,
_source=AnnotationSource.DATACLASS,
)
# The description might come from the docstring if `use_attribute_docstrings` was `True`:
new_field.description = new_field.description if new_field.description is not None else existing_desc
update_field_from_config(config_wrapper, f_name, new_field)
rebuilt_fields[f_name] = new_field
return rebuilt_fields
def is_valid_field_name(name: str) -> bool:
return not name.startswith('_')
def is_valid_privateattr_name(name: str) -> bool:
return name.startswith('_') and not name.startswith('__')
def takes_validated_data_argument(
default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any],
) -> TypeIs[Callable[[dict[str, Any]], Any]]:
"""Whether the provided default factory callable has a validated data parameter."""
try:
sig = _typing_extra.signature_no_eval(default_factory)
except (ValueError, TypeError):
# `inspect.signature` might not be able to infer a signature, e.g. with C objects.
# In this case, we assume no data argument is present:
return False
parameters = list(sig.parameters.values())
return len(parameters) == 1 and can_be_positional(parameters[0]) and parameters[0].default is Parameter.empty
def resolve_default_value(
default: Any,
default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any] | None,
*,
validated_data: dict[str, Any] | None = None,
call_default_factory: bool = False,
) -> Any:
"""Resolve the default value using either a static default or a default_factory."""
from ._utils import smart_deepcopy
if default_factory is None:
return smart_deepcopy(default)
if call_default_factory:
if takes_validated_data_argument(default_factory=default_factory):
fac = cast('Callable[[dict[str, Any]], Any]', default_factory)
if validated_data is None:
raise ValueError(
"The default factory requires the 'validated_data' argument, which was not provided when calling 'get_default()'."
)
return fac(validated_data)
else:
fac = cast('Callable[[], Any]', default_factory)
return fac()
return PydanticUndefined
@@ -0,0 +1,23 @@
from __future__ import annotations as _annotations
from dataclasses import dataclass
from typing import Union
@dataclass
class PydanticRecursiveRef:
type_ref: str
__name__ = 'PydanticRecursiveRef'
__hash__ = object.__hash__
def __call__(self) -> None:
"""Defining __call__ is necessary for the `typing` module to let you use an instance of
this class as the result of resolving a standard ForwardRef.
"""
def __or__(self, other):
return Union[self, other] # type: ignore
def __ror__(self, other):
return Union[other, self] # type: ignore
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,530 @@
from __future__ import annotations
import operator
import sys
import types
import typing
from collections import ChainMap
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from contextvars import ContextVar
from functools import reduce
from itertools import zip_longest
from types import prepare_class
from typing import TYPE_CHECKING, Annotated, Any, TypedDict, TypeVar, cast
from weakref import WeakValueDictionary
import typing_extensions
from typing_inspection import typing_objects
from typing_inspection.introspection import is_union_origin
from . import _typing_extra
from ._core_utils import get_type_ref
from ._forward_ref import PydanticRecursiveRef
from ._utils import all_identical, is_model_class
if TYPE_CHECKING:
from ..main import BaseModel
GenericTypesCacheKey = tuple[Any, Any, tuple[Any, ...]]
# Note: We want to remove LimitedDict, but to do this, we'd need to improve the handling of generics caching.
# Right now, to handle recursive generics, we some types must remain cached for brief periods without references.
# By chaining the WeakValuesDict with a LimitedDict, we have a way to retain caching for all types with references,
# while also retaining a limited number of types even without references. This is generally enough to build
# specific recursive generic models without losing required items out of the cache.
KT = TypeVar('KT')
VT = TypeVar('VT')
_LIMITED_DICT_SIZE = 100
class LimitedDict(dict[KT, VT]):
def __init__(self, size_limit: int = _LIMITED_DICT_SIZE) -> None:
self.size_limit = size_limit
super().__init__()
def __setitem__(self, key: KT, value: VT, /) -> None:
super().__setitem__(key, value)
if len(self) > self.size_limit:
excess = len(self) - self.size_limit + self.size_limit // 10
to_remove = list(self.keys())[:excess]
for k in to_remove:
del self[k]
# weak dictionaries allow the dynamically created parametrized versions of generic models to get collected
# once they are no longer referenced by the caller.
GenericTypesCache = WeakValueDictionary[GenericTypesCacheKey, 'type[BaseModel]']
if TYPE_CHECKING:
class DeepChainMap(ChainMap[KT, VT]): # type: ignore
...
else:
class DeepChainMap(ChainMap):
"""Variant of ChainMap that allows direct updates to inner scopes.
Taken from https://docs.python.org/3/library/collections.html#collections.ChainMap,
with some light modifications for this use case.
"""
def clear(self) -> None:
for mapping in self.maps:
mapping.clear()
def __setitem__(self, key: KT, value: VT) -> None:
for mapping in self.maps:
mapping[key] = value
def __delitem__(self, key: KT) -> None:
hit = False
for mapping in self.maps:
if key in mapping:
del mapping[key]
hit = True
if not hit:
raise KeyError(key)
# Despite the fact that LimitedDict _seems_ no longer necessary, I'm very nervous to actually remove it
# and discover later on that we need to re-add all this infrastructure...
# _GENERIC_TYPES_CACHE = DeepChainMap(GenericTypesCache(), LimitedDict())
_GENERIC_TYPES_CACHE = GenericTypesCache()
class PydanticGenericMetadata(TypedDict):
origin: type[BaseModel] | None # analogous to typing._GenericAlias.__origin__
args: tuple[Any, ...] # analogous to typing._GenericAlias.__args__
parameters: tuple[TypeVar, ...] # analogous to typing.Generic.__parameters__
def create_generic_submodel(
model_name: str, origin: type[BaseModel], args: tuple[Any, ...], params: tuple[Any, ...]
) -> type[BaseModel]:
"""Dynamically create a submodel of a provided (generic) BaseModel.
This is used when producing concrete parametrizations of generic models. This function
only *creates* the new subclass; the schema/validators/serialization must be updated to
reflect a concrete parametrization elsewhere.
Args:
model_name: The name of the newly created model.
origin: The base class for the new model to inherit from.
args: A tuple of generic metadata arguments.
params: A tuple of generic metadata parameters.
Returns:
The created submodel.
"""
namespace: dict[str, Any] = {'__module__': origin.__module__}
bases = (origin,)
meta, ns, kwds = prepare_class(model_name, bases)
namespace.update(ns)
created_model = meta(
model_name,
bases,
namespace,
__pydantic_generic_metadata__={
'origin': origin,
'args': args,
'parameters': params,
},
__pydantic_reset_parent_namespace__=False,
**kwds,
)
model_module, called_globally = _get_caller_frame_info(depth=3)
if called_globally: # create global reference and therefore allow pickling
object_by_reference = None
reference_name = model_name
reference_module_globals = sys.modules[created_model.__module__].__dict__
while object_by_reference is not created_model:
object_by_reference = reference_module_globals.setdefault(reference_name, created_model)
reference_name += '_'
return created_model
def _get_caller_frame_info(depth: int = 2) -> tuple[str | None, bool]:
"""Used inside a function to check whether it was called globally.
Args:
depth: The depth to get the frame.
Returns:
A tuple contains `module_name` and `called_globally`.
Raises:
RuntimeError: If the function is not called inside a function.
"""
try:
previous_caller_frame = sys._getframe(depth)
except ValueError as e:
raise RuntimeError('This function must be used inside another function') from e
except AttributeError: # sys module does not have _getframe function, so there's nothing we can do about it
return None, False
frame_globals = previous_caller_frame.f_globals
return frame_globals.get('__name__'), previous_caller_frame.f_locals is frame_globals
DictValues: type[Any] = {}.values().__class__
def iter_contained_typevars(v: Any) -> Iterator[TypeVar]:
"""Recursively iterate through all subtypes and type args of `v` and yield any typevars that are found.
This is inspired as an alternative to directly accessing the `__parameters__` attribute of a GenericAlias,
since __parameters__ of (nested) generic BaseModel subclasses won't show up in that list.
"""
if isinstance(v, TypeVar):
yield v
elif is_model_class(v):
yield from v.__pydantic_generic_metadata__['parameters']
elif isinstance(v, (DictValues, list)):
for var in v:
yield from iter_contained_typevars(var)
else:
args = get_args(v)
for arg in args:
yield from iter_contained_typevars(arg)
def get_args(v: Any) -> Any:
pydantic_generic_metadata: PydanticGenericMetadata | None = getattr(v, '__pydantic_generic_metadata__', None)
if pydantic_generic_metadata:
return pydantic_generic_metadata.get('args')
return typing_extensions.get_args(v)
def get_origin(v: Any) -> Any:
pydantic_generic_metadata: PydanticGenericMetadata | None = getattr(v, '__pydantic_generic_metadata__', None)
if pydantic_generic_metadata:
return pydantic_generic_metadata.get('origin')
return typing_extensions.get_origin(v)
def get_standard_typevars_map(cls: Any) -> dict[TypeVar, Any] | None:
"""Package a generic type's typevars and parametrization (if present) into a dictionary compatible with the
`replace_types` function. Specifically, this works with standard typing generics and typing._GenericAlias.
"""
origin = get_origin(cls)
if origin is None:
return None
if not hasattr(origin, '__parameters__'):
return None
# In this case, we know that cls is a _GenericAlias, and origin is the generic type
# So it is safe to access cls.__args__ and origin.__parameters__
args: tuple[Any, ...] = cls.__args__ # type: ignore
parameters: tuple[TypeVar, ...] = origin.__parameters__
return dict(zip(parameters, args))
def get_model_typevars_map(cls: type[BaseModel]) -> dict[TypeVar, Any]:
"""Package a generic BaseModel's typevars and concrete parametrization (if present) into a dictionary compatible
with the `replace_types` function.
Since BaseModel.__class_getitem__ does not produce a typing._GenericAlias, and the BaseModel generic info is
stored in the __pydantic_generic_metadata__ attribute, we need special handling here.
"""
# TODO: This could be unified with `get_standard_typevars_map` if we stored the generic metadata
# in the __origin__, __args__, and __parameters__ attributes of the model.
generic_metadata = cls.__pydantic_generic_metadata__
origin = generic_metadata['origin']
args = generic_metadata['args']
if not args:
# No need to go into `iter_contained_typevars`:
return {}
return dict(zip(iter_contained_typevars(origin), args))
def replace_types(type_: Any, type_map: Mapping[TypeVar, Any] | None) -> Any:
"""Return type with all occurrences of `type_map` keys recursively replaced with their values.
Args:
type_: The class or generic alias.
type_map: Mapping from `TypeVar` instance to concrete types.
Returns:
A new type representing the basic structure of `type_` with all
`typevar_map` keys recursively replaced.
Example:
```python
from typing import Union
from pydantic._internal._generics import replace_types
replace_types(tuple[str, Union[list[str], float]], {str: int})
#> tuple[int, Union[list[int], float]]
```
"""
if not type_map:
return type_
type_args = get_args(type_)
origin_type = get_origin(type_)
if typing_objects.is_annotated(origin_type):
annotated_type, *annotations = type_args
annotated_type = replace_types(annotated_type, type_map)
# TODO remove parentheses when we drop support for Python 3.10:
return Annotated[(annotated_type, *annotations)]
# Having type args is a good indicator that this is a typing special form
# instance or a generic alias of some sort.
if type_args:
resolved_type_args = tuple(replace_types(arg, type_map) for arg in type_args)
if all_identical(type_args, resolved_type_args):
# If all arguments are the same, there is no need to modify the
# type or create a new object at all
return type_
if (
origin_type is not None
and isinstance(type_, _typing_extra.typing_base)
and not isinstance(origin_type, _typing_extra.typing_base)
and getattr(type_, '_name', None) is not None
):
# In python < 3.9 generic aliases don't exist so any of these like `list`,
# `type` or `collections.abc.Callable` need to be translated.
# See: https://www.python.org/dev/peps/pep-0585
origin_type = getattr(typing, type_._name)
assert origin_type is not None
if is_union_origin(origin_type):
if any(typing_objects.is_any(arg) for arg in resolved_type_args):
# `Any | T` ~ `Any`:
resolved_type_args = (Any,)
# `Never | T` ~ `T`:
resolved_type_args = tuple(
arg
for arg in resolved_type_args
if not (typing_objects.is_noreturn(arg) or typing_objects.is_never(arg))
)
# PEP-604 syntax (e.g. `list | str`) is represented with a types.UnionType object that does not
# implement `__getitem__()`. In Python 3.14+, `typing.Union` and `types.UnionType` are the same,
# and we instead rely on `typing.Union` as it implicitly converts string annotations to `ForwardRef`
# instances (this is to avoid type errors as per https://github.com/python/cpython/pull/105366).
# TODO remove type ignore comment when we drop support for Python 3.9 (https://github.com/microsoft/pyright/issues/11241):
if (3, 10) <= sys.version_info < (3, 14) and origin_type is types.UnionType: # pyright: ignore[reportAttributeAccessIssue]
return reduce(operator.or_, resolved_type_args)
# NotRequired[T] and Required[T] don't support tuple type resolved_type_args, hence the condition below
return origin_type[resolved_type_args[0] if len(resolved_type_args) == 1 else resolved_type_args]
# We handle pydantic generic models separately as they don't have the same
# semantics as "typing" classes or generic aliases
if not origin_type and is_model_class(type_):
parameters = type_.__pydantic_generic_metadata__['parameters']
if not parameters:
return type_
resolved_type_args = tuple(replace_types(t, type_map) for t in parameters)
if all_identical(parameters, resolved_type_args):
return type_
return type_[resolved_type_args]
# Handle special case for typehints that can have lists as arguments.
# `typing.Callable[[int, str], int]` is an example for this.
if isinstance(type_, list):
resolved_list = [replace_types(element, type_map) for element in type_]
if all_identical(type_, resolved_list):
return type_
return resolved_list
# If all else fails, we try to resolve the type directly and otherwise just
# return the input with no modifications.
return type_map.get(type_, type_)
def map_generic_model_arguments(cls: type[BaseModel], args: tuple[Any, ...]) -> dict[TypeVar, Any]:
"""Return a mapping between the parameters of a generic model and the provided arguments during parameterization.
Raises:
TypeError: If the number of arguments does not match the parameters (i.e. if providing too few or too many arguments).
Example:
```python {test="skip" lint="skip"}
class Model[T, U, V = int](BaseModel): ...
map_generic_model_arguments(Model, (str, bytes))
#> {T: str, U: bytes, V: int}
map_generic_model_arguments(Model, (str,))
#> TypeError: Too few arguments for <class '__main__.Model'>; actual 1, expected at least 2
map_generic_model_arguments(Model, (str, bytes, int, complex))
#> TypeError: Too many arguments for <class '__main__.Model'>; actual 4, expected 3
```
Note:
This function is analogous to the private `typing._check_generic_specialization` function.
"""
parameters = cls.__pydantic_generic_metadata__['parameters']
expected_len = len(parameters)
typevars_map: dict[TypeVar, Any] = {}
_missing = object()
for parameter, argument in zip_longest(parameters, args, fillvalue=_missing):
if parameter is _missing:
raise TypeError(f'Too many arguments for {cls}; actual {len(args)}, expected {expected_len}')
if argument is _missing:
param = cast(TypeVar, parameter)
try:
has_default = param.has_default() # pyright: ignore[reportAttributeAccessIssue]
except AttributeError:
# Happens if using `typing.TypeVar` (and not `typing_extensions`) on Python < 3.13.
has_default = False
if has_default:
# The default might refer to other type parameters. For an example, see:
# https://typing.python.org/en/latest/spec/generics.html#type-parameters-as-parameters-to-generics
typevars_map[param] = replace_types(param.__default__, typevars_map) # pyright: ignore[reportAttributeAccessIssue]
else:
expected_len -= sum(hasattr(p, 'has_default') and p.has_default() for p in parameters) # pyright: ignore[reportAttributeAccessIssue]
raise TypeError(f'Too few arguments for {cls}; actual {len(args)}, expected at least {expected_len}')
else:
param = cast(TypeVar, parameter)
typevars_map[param] = argument
return typevars_map
_generic_recursion_cache: ContextVar[set[str] | None] = ContextVar('_generic_recursion_cache', default=None)
@contextmanager
def generic_recursion_self_type(
origin: type[BaseModel], args: tuple[Any, ...]
) -> Iterator[PydanticRecursiveRef | None]:
"""This contextmanager should be placed around the recursive calls used to build a generic type,
and accept as arguments the generic origin type and the type arguments being passed to it.
If the same origin and arguments are observed twice, it implies that a self-reference placeholder
can be used while building the core schema, and will produce a schema_ref that will be valid in the
final parent schema.
"""
previously_seen_type_refs = _generic_recursion_cache.get()
if previously_seen_type_refs is None:
previously_seen_type_refs = set()
token = _generic_recursion_cache.set(previously_seen_type_refs)
else:
token = None
try:
type_ref = get_type_ref(origin, args_override=args)
if type_ref in previously_seen_type_refs:
self_type = PydanticRecursiveRef(type_ref=type_ref)
yield self_type
else:
previously_seen_type_refs.add(type_ref)
yield
previously_seen_type_refs.remove(type_ref)
finally:
if token:
_generic_recursion_cache.reset(token)
def recursively_defined_type_refs() -> set[str]:
visited = _generic_recursion_cache.get()
if not visited:
return set() # not in a generic recursion, so there are no types
return visited.copy() # don't allow modifications
def get_cached_generic_type_early(parent: type[BaseModel], typevar_values: Any) -> type[BaseModel] | None:
"""The use of a two-stage cache lookup approach was necessary to have the highest performance possible for
repeated calls to `__class_getitem__` on generic types (which may happen in tighter loops during runtime),
while still ensuring that certain alternative parametrizations ultimately resolve to the same type.
As a concrete example, this approach was necessary to make Model[List[T]][int] equal to Model[List[int]].
The approach could be modified to not use two different cache keys at different points, but the
_early_cache_key is optimized to be as quick to compute as possible (for repeated-access speed), and the
_late_cache_key is optimized to be as "correct" as possible, so that two types that will ultimately be the
same after resolving the type arguments will always produce cache hits.
If we wanted to move to only using a single cache key per type, we would either need to always use the
slower/more computationally intensive logic associated with _late_cache_key, or would need to accept
that Model[List[T]][int] is a different type than Model[List[T]][int]. Because we rely on subclass relationships
during validation, I think it is worthwhile to ensure that types that are functionally equivalent are actually
equal.
"""
return _GENERIC_TYPES_CACHE.get(_early_cache_key(parent, typevar_values))
def get_cached_generic_type_late(
parent: type[BaseModel], typevar_values: Any, origin: type[BaseModel], args: tuple[Any, ...]
) -> type[BaseModel] | None:
"""See the docstring of `get_cached_generic_type_early` for more information about the two-stage cache lookup."""
cached = _GENERIC_TYPES_CACHE.get(_late_cache_key(origin, args, typevar_values))
if cached is not None:
set_cached_generic_type(parent, typevar_values, cached, origin, args)
return cached
def set_cached_generic_type(
parent: type[BaseModel],
typevar_values: tuple[Any, ...],
type_: type[BaseModel],
origin: type[BaseModel] | None = None,
args: tuple[Any, ...] | None = None,
) -> None:
"""See the docstring of `get_cached_generic_type_early` for more information about why items are cached with
two different keys.
"""
_GENERIC_TYPES_CACHE[_early_cache_key(parent, typevar_values)] = type_
if len(typevar_values) == 1:
_GENERIC_TYPES_CACHE[_early_cache_key(parent, typevar_values[0])] = type_
if origin and args:
_GENERIC_TYPES_CACHE[_late_cache_key(origin, args, typevar_values)] = type_
def _union_orderings_key(typevar_values: Any) -> Any:
"""This is intended to help differentiate between Union types with the same arguments in different order.
Thanks to caching internal to the `typing` module, it is not possible to distinguish between
List[Union[int, float]] and List[Union[float, int]] (and similarly for other "parent" origins besides List)
because `typing` considers Union[int, float] to be equal to Union[float, int].
However, you _can_ distinguish between (top-level) Union[int, float] vs. Union[float, int].
Because we parse items as the first Union type that is successful, we get slightly more consistent behavior
if we make an effort to distinguish the ordering of items in a union. It would be best if we could _always_
get the exact-correct order of items in the union, but that would require a change to the `typing` module itself.
(See https://github.com/python/cpython/issues/86483 for reference.)
"""
if isinstance(typevar_values, tuple):
return tuple(_union_orderings_key(value) for value in typevar_values)
elif typing_objects.is_union(typing_extensions.get_origin(typevar_values)):
return get_args(typevar_values)
else:
return ()
def _early_cache_key(cls: type[BaseModel], typevar_values: Any) -> GenericTypesCacheKey:
"""This is intended for minimal computational overhead during lookups of cached types.
Note that this is overly simplistic, and it's possible that two different cls/typevar_values
inputs would ultimately result in the same type being created in BaseModel.__class_getitem__.
To handle this, we have a fallback _late_cache_key that is checked later if the _early_cache_key
lookup fails, and should result in a cache hit _precisely_ when the inputs to __class_getitem__
would result in the same type.
"""
return cls, typevar_values, _union_orderings_key(typevar_values)
def _late_cache_key(origin: type[BaseModel], args: tuple[Any, ...], typevar_values: Any) -> GenericTypesCacheKey:
"""This is intended for use later in the process of creating a new type, when we have more information
about the exact args that will be passed. If it turns out that a different set of inputs to
__class_getitem__ resulted in the same inputs to the generic type creation process, we can still
return the cached type, and update the cache with the _early_cache_key as well.
"""
# The _union_orderings_key is placed at the start here to ensure there cannot be a collision with an
# _early_cache_key, as that function will always produce a BaseModel subclass as the first item in the key,
# whereas this function will always produce a tuple as the first item in the key.
return _union_orderings_key(typevar_values), origin, args
@@ -0,0 +1,27 @@
"""Git utilities, adopted from mypy's git utilities (https://github.com/python/mypy/blob/master/mypy/git.py)."""
from __future__ import annotations
import subprocess
from pathlib import Path
def is_git_repo(dir: Path) -> bool:
"""Is the given directory version-controlled with git?"""
return dir.joinpath('.git').exists()
def have_git() -> bool: # pragma: no cover
"""Can we run the git executable?"""
try:
subprocess.check_output(['git', '--help'])
return True
except subprocess.CalledProcessError:
return False
except OSError:
return False
def git_revision(dir: Path) -> str:
"""Get the SHA-1 of the HEAD of a git repository."""
return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], cwd=dir).decode('utf-8').strip()
@@ -0,0 +1,7 @@
import sys
# `slots` is available on Python >= 3.10
if sys.version_info >= (3, 10):
slots_true = {'slots': True}
else:
slots_true = {}
@@ -0,0 +1,403 @@
from __future__ import annotations
from collections import defaultdict
from collections.abc import Iterable
from copy import copy
from functools import lru_cache, partial
from typing import TYPE_CHECKING, Any
from pydantic_core import CoreSchema, PydanticCustomError, ValidationError, to_jsonable_python
from pydantic_core import core_schema as cs
from ._fields import PydanticMetadata
from ._import_utils import import_cached_field_info
if TYPE_CHECKING:
pass
STRICT = {'strict'}
FAIL_FAST = {'fail_fast'}
LENGTH_CONSTRAINTS = {'min_length', 'max_length'}
INEQUALITY = {'le', 'ge', 'lt', 'gt'}
NUMERIC_CONSTRAINTS = {'multiple_of', *INEQUALITY}
ALLOW_INF_NAN = {'allow_inf_nan'}
STR_CONSTRAINTS = {
*LENGTH_CONSTRAINTS,
*STRICT,
'strip_whitespace',
'to_lower',
'to_upper',
'pattern',
'coerce_numbers_to_str',
'ascii_only',
}
BYTES_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT}
LIST_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST}
TUPLE_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST}
SET_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST}
DICT_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT}
GENERATOR_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT}
SEQUENCE_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *FAIL_FAST}
FLOAT_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *ALLOW_INF_NAN, *STRICT}
DECIMAL_CONSTRAINTS = {'max_digits', 'decimal_places', *FLOAT_CONSTRAINTS}
INT_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *ALLOW_INF_NAN, *STRICT}
BOOL_CONSTRAINTS = STRICT
UUID_CONSTRAINTS = STRICT
DATE_TIME_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT}
TIMEDELTA_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT}
TIME_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT}
LAX_OR_STRICT_CONSTRAINTS = STRICT
ENUM_CONSTRAINTS = STRICT
COMPLEX_CONSTRAINTS = STRICT
UNION_CONSTRAINTS = {'union_mode'}
URL_CONSTRAINTS = {
'max_length',
'allowed_schemes',
'host_required',
'default_host',
'default_port',
'default_path',
}
TEXT_SCHEMA_TYPES = ('str', 'bytes', 'url', 'multi-host-url')
SEQUENCE_SCHEMA_TYPES = ('list', 'tuple', 'set', 'frozenset', 'generator', *TEXT_SCHEMA_TYPES)
NUMERIC_SCHEMA_TYPES = ('float', 'int', 'date', 'time', 'timedelta', 'datetime')
CONSTRAINTS_TO_ALLOWED_SCHEMAS: dict[str, set[str]] = defaultdict(set)
constraint_schema_pairings: list[tuple[set[str], tuple[str, ...]]] = [
(STR_CONSTRAINTS, TEXT_SCHEMA_TYPES),
(BYTES_CONSTRAINTS, ('bytes',)),
(LIST_CONSTRAINTS, ('list',)),
(TUPLE_CONSTRAINTS, ('tuple',)),
(SET_CONSTRAINTS, ('set', 'frozenset')),
(DICT_CONSTRAINTS, ('dict',)),
(GENERATOR_CONSTRAINTS, ('generator',)),
(FLOAT_CONSTRAINTS, ('float',)),
(INT_CONSTRAINTS, ('int',)),
(DATE_TIME_CONSTRAINTS, ('date', 'time', 'datetime', 'timedelta')),
# TODO: this is a bit redundant, we could probably avoid some of these
(STRICT, (*TEXT_SCHEMA_TYPES, *SEQUENCE_SCHEMA_TYPES, *NUMERIC_SCHEMA_TYPES, 'typed-dict', 'model')),
(UNION_CONSTRAINTS, ('union',)),
(URL_CONSTRAINTS, ('url', 'multi-host-url')),
(BOOL_CONSTRAINTS, ('bool',)),
(UUID_CONSTRAINTS, ('uuid',)),
(LAX_OR_STRICT_CONSTRAINTS, ('lax-or-strict',)),
(ENUM_CONSTRAINTS, ('enum',)),
(DECIMAL_CONSTRAINTS, ('decimal',)),
(COMPLEX_CONSTRAINTS, ('complex',)),
]
for constraints, schemas in constraint_schema_pairings:
for c in constraints:
CONSTRAINTS_TO_ALLOWED_SCHEMAS[c].update(schemas)
def as_jsonable_value(v: Any) -> Any:
if type(v) not in (int, str, float, bytes, bool, type(None)):
return to_jsonable_python(v)
return v
def expand_grouped_metadata(annotations: Iterable[Any]) -> Iterable[Any]:
"""Expand the annotations.
Args:
annotations: An iterable of annotations.
Returns:
An iterable of expanded annotations.
Example:
```python
from annotated_types import Ge, Len
from pydantic._internal._known_annotated_metadata import expand_grouped_metadata
print(list(expand_grouped_metadata([Ge(4), Len(5)])))
#> [Ge(ge=4), MinLen(min_length=5)]
```
"""
import annotated_types as at
FieldInfo = import_cached_field_info()
for annotation in annotations:
if isinstance(annotation, at.GroupedMetadata):
yield from annotation
elif isinstance(annotation, FieldInfo):
yield from annotation.metadata
# this is a bit problematic in that it results in duplicate metadata
# all of our "consumers" can handle it, but it is not ideal
# we probably should split up FieldInfo into:
# - annotated types metadata
# - individual metadata known only to Pydantic
annotation = copy(annotation)
annotation.metadata = []
yield annotation
else:
yield annotation
@lru_cache
def _get_at_to_constraint_map() -> dict[type, str]:
"""Return a mapping of annotated types to constraints.
Normally, we would define a mapping like this in the module scope, but we can't do that
because we don't permit module level imports of `annotated_types`, in an attempt to speed up
the import time of `pydantic`. We still only want to have this dictionary defined in one place,
so we use this function to cache the result.
"""
import annotated_types as at
return {
at.Gt: 'gt',
at.Ge: 'ge',
at.Lt: 'lt',
at.Le: 'le',
at.MultipleOf: 'multiple_of',
at.MinLen: 'min_length',
at.MaxLen: 'max_length',
}
def apply_known_metadata(annotation: Any, schema: CoreSchema) -> CoreSchema | None: # noqa: C901
"""Apply `annotation` to `schema` if it is an annotation we know about (Gt, Le, etc.).
Otherwise return `None`.
This does not handle all known annotations. If / when it does, it can always
return a CoreSchema and return the unmodified schema if the annotation should be ignored.
Assumes that GroupedMetadata has already been expanded via `expand_grouped_metadata`.
Args:
annotation: The annotation.
schema: The schema.
Returns:
An updated schema with annotation if it is an annotation we know about, `None` otherwise.
Raises:
RuntimeError: If a constraint can't be applied to a specific schema type.
ValueError: If an unknown constraint is encountered.
"""
import annotated_types as at
from ._validators import NUMERIC_VALIDATOR_LOOKUP, forbid_inf_nan_check
schema = schema.copy()
schema_update, other_metadata = collect_known_metadata([annotation])
schema_type = schema['type']
chain_schema_constraints: set[str] = {
'pattern',
'strip_whitespace',
'to_lower',
'to_upper',
'coerce_numbers_to_str',
'ascii_only',
}
chain_schema_steps: list[CoreSchema] = []
for constraint, value in schema_update.items():
if constraint not in CONSTRAINTS_TO_ALLOWED_SCHEMAS:
raise ValueError(f'Unknown constraint {constraint}')
allowed_schemas = CONSTRAINTS_TO_ALLOWED_SCHEMAS[constraint]
# if it becomes necessary to handle more than one constraint
# in this recursive case with function-after or function-wrap, we should refactor
# this is a bit challenging because we sometimes want to apply constraints to the inner schema,
# whereas other times we want to wrap the existing schema with a new one that enforces a new constraint.
if schema_type in {'function-before', 'function-wrap', 'function-after'} and constraint == 'strict':
schema['schema'] = apply_known_metadata(annotation, schema['schema']) # type: ignore # schema is function schema
return schema
# if we're allowed to apply constraint directly to the schema, like le to int, do that
if schema_type in allowed_schemas:
if constraint == 'union_mode' and schema_type == 'union':
schema['mode'] = value # type: ignore # schema is UnionSchema
else:
schema[constraint] = value
continue
# else, apply a function after validator to the schema to enforce the corresponding constraint
if constraint in chain_schema_constraints:
def _apply_constraint_with_incompatibility_info(
value: Any, handler: cs.ValidatorFunctionWrapHandler
) -> Any:
try:
x = handler(value)
except ValidationError as ve:
# if the error is about the type, it's likely that the constraint is incompatible the type of the field
# for example, the following invalid schema wouldn't be caught during schema build, but rather at this point
# with a cryptic 'string_type' error coming from the string validator,
# that we'd rather express as a constraint incompatibility error (TypeError)
# Annotated[list[int], Field(pattern='abc')]
if 'type' in ve.errors()[0]['type']:
raise TypeError(
f"Unable to apply constraint '{constraint}' to supplied value {value} for schema of type '{schema_type}'" # noqa: B023
)
raise ve
return x
chain_schema_steps.append(
cs.no_info_wrap_validator_function(
_apply_constraint_with_incompatibility_info, cs.str_schema(**{constraint: value})
)
)
elif constraint in NUMERIC_VALIDATOR_LOOKUP:
if constraint in LENGTH_CONSTRAINTS:
inner_schema = schema
while inner_schema['type'] in {'function-before', 'function-wrap', 'function-after'}:
inner_schema = inner_schema['schema'] # type: ignore
inner_schema_type = inner_schema['type']
if inner_schema_type == 'list' or (
inner_schema_type == 'json-or-python' and inner_schema['json_schema']['type'] == 'list' # type: ignore
):
js_constraint_key = 'minItems' if constraint == 'min_length' else 'maxItems'
else:
js_constraint_key = 'minLength' if constraint == 'min_length' else 'maxLength'
else:
js_constraint_key = constraint
schema = cs.no_info_after_validator_function(
partial(NUMERIC_VALIDATOR_LOOKUP[constraint], **{constraint: value}), schema
)
metadata = schema.get('metadata', {})
if (existing_json_schema_updates := metadata.get('pydantic_js_updates')) is not None:
metadata['pydantic_js_updates'] = {
**existing_json_schema_updates,
**{js_constraint_key: as_jsonable_value(value)},
}
else:
metadata['pydantic_js_updates'] = {js_constraint_key: as_jsonable_value(value)}
schema['metadata'] = metadata
elif constraint == 'allow_inf_nan' and value is False:
schema = cs.no_info_after_validator_function(
forbid_inf_nan_check,
schema,
)
else:
# It's rare that we'd get here, but it's possible if we add a new constraint and forget to handle it
# Most constraint errors are caught at runtime during attempted application
raise RuntimeError(f"Unable to apply constraint '{constraint}' to schema of type '{schema_type}'")
for annotation in other_metadata:
if (annotation_type := type(annotation)) in (at_to_constraint_map := _get_at_to_constraint_map()):
constraint = at_to_constraint_map[annotation_type]
validator = NUMERIC_VALIDATOR_LOOKUP.get(constraint)
if validator is None:
raise ValueError(f'Unknown constraint {constraint}')
schema = cs.no_info_after_validator_function(
partial(validator, {constraint: getattr(annotation, constraint)}), schema
)
continue
elif isinstance(annotation, (at.Predicate, at.Not)):
predicate_name = f'{annotation.func.__qualname__!r} ' if hasattr(annotation.func, '__qualname__') else ''
# Note: B023 is ignored because even though we iterate over `other_metadata`, it is guaranteed
# to be of length 1. `apply_known_metadata()` is called from `GenerateSchema`, where annotations
# were already expanded via `expand_grouped_metadata()`. Confusing, but this falls into the annotations
# refactor.
if isinstance(annotation, at.Predicate):
def val_func(v: Any) -> Any:
predicate_satisfied = annotation.func(v) # noqa: B023
if not predicate_satisfied:
raise PydanticCustomError(
'predicate_failed',
f'Predicate {predicate_name}failed', # pyright: ignore[reportArgumentType] # noqa: B023
)
return v
else:
def val_func(v: Any) -> Any:
predicate_satisfied = annotation.func(v) # noqa: B023
if predicate_satisfied:
raise PydanticCustomError(
'not_operation_failed',
f'Not of {predicate_name}failed', # pyright: ignore[reportArgumentType] # noqa: B023
)
return v
schema = cs.no_info_after_validator_function(val_func, schema)
else:
# ignore any other unknown metadata
return None
if chain_schema_steps:
chain_schema_steps = [schema] + chain_schema_steps
return cs.chain_schema(chain_schema_steps)
return schema
def collect_known_metadata(annotations: Iterable[Any]) -> tuple[dict[str, Any], list[Any]]:
"""Split `annotations` into known metadata and unknown annotations.
Args:
annotations: An iterable of annotations.
Returns:
A tuple contains a dict of known metadata and a list of unknown annotations.
Example:
```python
from annotated_types import Gt, Len
from pydantic._internal._known_annotated_metadata import collect_known_metadata
print(collect_known_metadata([Gt(1), Len(42), ...]))
#> ({'gt': 1, 'min_length': 42}, [Ellipsis])
```
"""
annotations = expand_grouped_metadata(annotations)
res: dict[str, Any] = {}
remaining: list[Any] = []
for annotation in annotations:
# isinstance(annotation, PydanticMetadata) also covers ._fields:_PydanticGeneralMetadata
if isinstance(annotation, PydanticMetadata):
res.update(annotation.__dict__)
# we don't use dataclasses.asdict because that recursively calls asdict on the field values
elif (annotation_type := type(annotation)) in (at_to_constraint_map := _get_at_to_constraint_map()):
constraint = at_to_constraint_map[annotation_type]
res[constraint] = getattr(annotation, constraint)
elif isinstance(annotation, type) and issubclass(annotation, PydanticMetadata):
# also support PydanticMetadata classes being used without initialisation,
# e.g. `Annotated[int, Strict]` as well as `Annotated[int, Strict()]`
res.update({k: v for k, v in vars(annotation).items() if not k.startswith('_')})
else:
remaining.append(annotation)
# Nones can sneak in but pydantic-core will reject them
# it'd be nice to clean things up so we don't put in None (we probably don't _need_ to, it was just easier)
# but this is simple enough to kick that can down the road
res = {k: v for k, v in res.items() if v is not None}
return res, remaining
def check_metadata(metadata: dict[str, Any], allowed: Iterable[str], source_type: Any) -> None:
"""A small utility function to validate that the given metadata can be applied to the target.
More than saving lines of code, this gives us a consistent error message for all of our internal implementations.
Args:
metadata: A dict of metadata.
allowed: An iterable of allowed metadata.
source_type: The source type.
Raises:
TypeError: If there is metadatas that can't be applied on source type.
"""
unknown = metadata.keys() - set(allowed)
if unknown:
raise TypeError(
f'The following constraints cannot be applied to {source_type!r}: {", ".join([f"{k!r}" for k in unknown])}'
)
@@ -0,0 +1,124 @@
"""Tools to provide pretty/human-readable display of objects."""
from __future__ import annotations as _annotations
import types
from collections.abc import Callable, Collection, Generator, Iterable
from typing import TYPE_CHECKING, Any, ForwardRef, cast
import typing_extensions
from typing_extensions import TypeAlias
from typing_inspection import typing_objects
from typing_inspection.introspection import is_union_origin
from . import _typing_extra
if TYPE_CHECKING:
# TODO remove type error comments when we drop support for Python 3.9
ReprArgs: TypeAlias = Iterable[tuple[str | None, Any]] # pyright: ignore[reportGeneralTypeIssues]
RichReprResult: TypeAlias = Iterable[Any | tuple[Any] | tuple[str, Any] | tuple[str, Any, Any]] # pyright: ignore[reportGeneralTypeIssues]
class PlainRepr(str):
"""String class where repr doesn't include quotes. Useful with Representation when you want to return a string
representation of something that is valid (or pseudo-valid) python.
"""
def __repr__(self) -> str:
return str(self)
class Representation:
# Mixin to provide `__str__`, `__repr__`, and `__pretty__` and `__rich_repr__` methods.
# `__pretty__` is used by [devtools](https://python-devtools.helpmanual.io/).
# `__rich_repr__` is used by [rich](https://rich.readthedocs.io/en/stable/pretty.html).
# (this is not a docstring to avoid adding a docstring to classes which inherit from Representation)
__slots__ = ()
def __repr_args__(self) -> ReprArgs:
"""Returns the attributes to show in __str__, __repr__, and __pretty__ this is generally overridden.
Can either return:
* name - value pairs, e.g.: `[('foo_name', 'foo'), ('bar_name', ['b', 'a', 'r'])]`
* or, just values, e.g.: `[(None, 'foo'), (None, ['b', 'a', 'r'])]`
"""
attrs_names = cast(Collection[str], self.__slots__)
if not attrs_names and hasattr(self, '__dict__'):
attrs_names = self.__dict__.keys()
attrs = ((s, getattr(self, s)) for s in attrs_names)
return [(a, v if v is not self else self.__repr_recursion__(v)) for a, v in attrs if v is not None]
def __repr_name__(self) -> str:
"""Name of the instance's class, used in __repr__."""
return self.__class__.__name__
def __repr_recursion__(self, object: Any) -> str:
"""Returns the string representation of a recursive object."""
# This is copied over from the stdlib `pprint` module:
return f'<Recursion on {type(object).__name__} with id={id(object)}>'
def __repr_str__(self, join_str: str) -> str:
return join_str.join(repr(v) if a is None else f'{a}={v!r}' for a, v in self.__repr_args__())
def __pretty__(self, fmt: Callable[[Any], Any], **kwargs: Any) -> Generator[Any]:
"""Used by devtools (https://python-devtools.helpmanual.io/) to pretty print objects."""
yield self.__repr_name__() + '('
yield 1
for name, value in self.__repr_args__():
if name is not None:
yield name + '='
yield fmt(value)
yield ','
yield 0
yield -1
yield ')'
def __rich_repr__(self) -> RichReprResult:
"""Used by Rich (https://rich.readthedocs.io/en/stable/pretty.html) to pretty print objects."""
for name, field_repr in self.__repr_args__():
if name is None:
yield field_repr
else:
yield name, field_repr
def __str__(self) -> str:
return self.__repr_str__(' ')
def __repr__(self) -> str:
return f'{self.__repr_name__()}({self.__repr_str__(", ")})'
def display_as_type(obj: Any) -> str:
"""Pretty representation of a type, should be as close as possible to the original type definition string.
Takes some logic from `typing._type_repr`.
"""
if isinstance(obj, (types.FunctionType, types.BuiltinFunctionType)):
return obj.__name__
elif obj is ...:
return '...'
elif isinstance(obj, Representation):
return repr(obj)
elif isinstance(obj, ForwardRef) or typing_objects.is_typealiastype(obj):
return str(obj)
if not isinstance(obj, (_typing_extra.typing_base, _typing_extra.WithArgsTypes, type)):
obj = obj.__class__
if is_union_origin(typing_extensions.get_origin(obj)):
args = ', '.join(map(display_as_type, typing_extensions.get_args(obj)))
return f'Union[{args}]'
elif isinstance(obj, _typing_extra.WithArgsTypes):
if typing_objects.is_literal(typing_extensions.get_origin(obj)):
args = ', '.join(map(repr, typing_extensions.get_args(obj)))
else:
args = ', '.join(map(display_as_type, typing_extensions.get_args(obj)))
try:
return f'{obj.__qualname__}[{args}]'
except AttributeError:
return str(obj).replace('typing.', '').replace('typing_extensions.', '') # handles TypeAliasType in 3.12
elif isinstance(obj, type):
return obj.__qualname__
else:
return repr(obj).replace('typing.', '').replace('typing_extensions.', '')
@@ -0,0 +1,125 @@
"""Types and utility functions used by various other internal tools."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable, Literal
from pydantic_core import core_schema
from ..annotated_handlers import GetCoreSchemaHandler, GetJsonSchemaHandler
if TYPE_CHECKING:
from ..json_schema import GenerateJsonSchema, JsonSchemaValue
from ._core_utils import CoreSchemaOrField
from ._generate_schema import GenerateSchema
from ._namespace_utils import NamespacesTuple
GetJsonSchemaFunction = Callable[[CoreSchemaOrField, GetJsonSchemaHandler], JsonSchemaValue]
HandlerOverride = Callable[[CoreSchemaOrField], JsonSchemaValue]
class GenerateJsonSchemaHandler(GetJsonSchemaHandler):
"""JsonSchemaHandler implementation that doesn't do ref unwrapping by default.
This is used for any Annotated metadata so that we don't end up with conflicting
modifications to the definition schema.
Used internally by Pydantic, please do not rely on this implementation.
See `GetJsonSchemaHandler` for the handler API.
"""
def __init__(self, generate_json_schema: GenerateJsonSchema, handler_override: HandlerOverride | None) -> None:
self.generate_json_schema = generate_json_schema
self.handler = handler_override or generate_json_schema.generate_inner
self.mode = generate_json_schema.mode
def __call__(self, core_schema: CoreSchemaOrField, /) -> JsonSchemaValue:
return self.handler(core_schema)
def resolve_ref_schema(self, maybe_ref_json_schema: JsonSchemaValue) -> JsonSchemaValue:
"""Resolves `$ref` in the json schema.
This returns the input json schema if there is no `$ref` in json schema.
Args:
maybe_ref_json_schema: The input json schema that may contains `$ref`.
Returns:
Resolved json schema.
Raises:
LookupError: If it can't find the definition for `$ref`.
"""
if '$ref' not in maybe_ref_json_schema:
return maybe_ref_json_schema
ref = maybe_ref_json_schema['$ref']
json_schema = self.generate_json_schema.get_schema_from_definitions(ref)
if json_schema is None:
raise LookupError(
f'Could not find a ref for {ref}.'
' Maybe you tried to call resolve_ref_schema from within a recursive model?'
)
return json_schema
class CallbackGetCoreSchemaHandler(GetCoreSchemaHandler):
"""Wrapper to use an arbitrary function as a `GetCoreSchemaHandler`.
Used internally by Pydantic, please do not rely on this implementation.
See `GetCoreSchemaHandler` for the handler API.
"""
def __init__(
self,
handler: Callable[[Any], core_schema.CoreSchema],
generate_schema: GenerateSchema,
ref_mode: Literal['to-def', 'unpack'] = 'to-def',
) -> None:
self._handler = handler
self._generate_schema = generate_schema
self._ref_mode = ref_mode
def __call__(self, source_type: Any, /) -> core_schema.CoreSchema:
schema = self._handler(source_type)
if self._ref_mode == 'to-def':
ref = schema.get('ref')
if ref is not None:
return self._generate_schema.defs.create_definition_reference_schema(schema)
return schema
else: # ref_mode = 'unpack'
return self.resolve_ref_schema(schema)
def _get_types_namespace(self) -> NamespacesTuple:
return self._generate_schema._types_namespace
def generate_schema(self, source_type: Any, /) -> core_schema.CoreSchema:
return self._generate_schema.generate_schema(source_type)
@property
def field_name(self) -> str | None:
return self._generate_schema.field_name_stack.get()
def resolve_ref_schema(self, maybe_ref_schema: core_schema.CoreSchema) -> core_schema.CoreSchema:
"""Resolves reference in the core schema.
Args:
maybe_ref_schema: The input core schema that may contains reference.
Returns:
Resolved core schema.
Raises:
LookupError: If it can't find the definition for reference.
"""
if maybe_ref_schema['type'] == 'definition-ref':
ref = maybe_ref_schema['schema_ref']
definition = self._generate_schema.defs.get_schema_from_ref(ref)
if definition is None:
raise LookupError(
f'Could not find a ref for {ref}.'
' Maybe you tried to call resolve_ref_schema from within a recursive model?'
)
return definition
elif maybe_ref_schema['type'] == 'definitions':
return self.resolve_ref_schema(maybe_ref_schema['schema'])
return maybe_ref_schema
@@ -0,0 +1,189 @@
from __future__ import annotations
import dataclasses
from inspect import Parameter, Signature
from typing import TYPE_CHECKING, Any, Callable
from pydantic_core import PydanticUndefined
from ._typing_extra import signature_no_eval
from ._utils import is_valid_identifier
if TYPE_CHECKING:
from ..config import ExtraValues
from ..fields import FieldInfo
# Copied over from stdlib dataclasses
class _HAS_DEFAULT_FACTORY_CLASS:
def __repr__(self):
return '<factory>'
_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS()
def _field_name_for_signature(field_name: str, field_info: FieldInfo) -> str:
"""Extract the correct name to use for the field when generating a signature.
Assuming the field has a valid alias, this will return the alias. Otherwise, it will return the field name.
First priority is given to the alias, then the validation_alias, then the field name.
Args:
field_name: The name of the field
field_info: The corresponding FieldInfo object.
Returns:
The correct name to use when generating a signature.
"""
if isinstance(field_info.alias, str) and is_valid_identifier(field_info.alias):
return field_info.alias
if isinstance(field_info.validation_alias, str) and is_valid_identifier(field_info.validation_alias):
return field_info.validation_alias
return field_name
def _process_param_defaults(param: Parameter) -> Parameter:
"""Modify the signature for a parameter in a dataclass where the default value is a FieldInfo instance.
Args:
param (Parameter): The parameter
Returns:
Parameter: The custom processed parameter
"""
from ..fields import FieldInfo
param_default = param.default
if isinstance(param_default, FieldInfo):
annotation = param.annotation
# Replace the annotation if appropriate
# inspect does "clever" things to show annotations as strings because we have
# `from __future__ import annotations` in main, we don't want that
if annotation == 'Any':
annotation = Any
# Replace the field default
default = param_default.default
if default is PydanticUndefined:
if param_default.default_factory is None:
default = Signature.empty
else:
# this is used by dataclasses to indicate a factory exists:
default = dataclasses._HAS_DEFAULT_FACTORY # type: ignore
return param.replace(
annotation=annotation, name=_field_name_for_signature(param.name, param_default), default=default
)
return param
def _generate_signature_parameters( # noqa: C901 (ignore complexity, could use a refactor)
init: Callable[..., None],
fields: dict[str, FieldInfo],
validate_by_name: bool,
extra: ExtraValues | None,
) -> dict[str, Parameter]:
"""Generate a mapping of parameter names to Parameter objects for a pydantic BaseModel or dataclass."""
from itertools import islice
present_params = signature_no_eval(init).parameters.values()
merged_params: dict[str, Parameter] = {}
var_kw = None
use_var_kw = False
for param in islice(present_params, 1, None): # skip self arg
# inspect does "clever" things to show annotations as strings because we have
# `from __future__ import annotations` in main, we don't want that
if fields.get(param.name):
# exclude params with init=False
if getattr(fields[param.name], 'init', True) is False:
continue
param = param.replace(name=_field_name_for_signature(param.name, fields[param.name]))
if param.annotation == 'Any':
param = param.replace(annotation=Any)
if param.kind is param.VAR_KEYWORD:
var_kw = param
continue
merged_params[param.name] = param
if var_kw: # if custom init has no var_kw, fields which are not declared in it cannot be passed through
allow_names = validate_by_name
for field_name, field in fields.items():
# when alias is a str it should be used for signature generation
param_name = _field_name_for_signature(field_name, field)
if field_name in merged_params or param_name in merged_params:
continue
if not is_valid_identifier(param_name):
if allow_names:
param_name = field_name
else:
use_var_kw = True
continue
if field.is_required():
default = Parameter.empty
elif field.default_factory is not None:
# Mimics stdlib dataclasses:
default = _HAS_DEFAULT_FACTORY
else:
default = field.default
merged_params[param_name] = Parameter(
param_name,
Parameter.KEYWORD_ONLY,
annotation=field.rebuild_annotation(),
default=default,
)
if extra == 'allow':
use_var_kw = True
if var_kw and use_var_kw:
# Make sure the parameter for extra kwargs
# does not have the same name as a field
default_model_signature = [
('self', Parameter.POSITIONAL_ONLY),
('data', Parameter.VAR_KEYWORD),
]
if [(p.name, p.kind) for p in present_params] == default_model_signature:
# if this is the standard model signature, use extra_data as the extra args name
var_kw_name = 'extra_data'
else:
# else start from var_kw
var_kw_name = var_kw.name
# generate a name that's definitely unique
while var_kw_name in fields:
var_kw_name += '_'
merged_params[var_kw_name] = var_kw.replace(name=var_kw_name)
return merged_params
def generate_pydantic_signature(
init: Callable[..., None],
fields: dict[str, FieldInfo],
validate_by_name: bool,
extra: ExtraValues | None,
is_dataclass: bool = False,
) -> Signature:
"""Generate signature for a pydantic BaseModel or dataclass.
Args:
init: The class init.
fields: The model fields.
validate_by_name: The `validate_by_name` value of the config.
extra: The `extra` value of the config.
is_dataclass: Whether the model is a dataclass.
Returns:
The dataclass/BaseModel subclass signature.
"""
merged_params = _generate_signature_parameters(init, fields, validate_by_name, extra)
if is_dataclass:
merged_params = {k: _process_param_defaults(v) for k, v in merged_params.items()}
return Signature(parameters=list(merged_params.values()), return_annotation=None)
@@ -0,0 +1,446 @@
"""Bucket of reusable internal utilities.
This should be reduced as much as possible with functions only used in one place, moved to that place.
"""
from __future__ import annotations as _annotations
import dataclasses
import keyword
import sys
import warnings
import weakref
from collections import OrderedDict, defaultdict, deque
from collections.abc import Callable, Iterable, Mapping
from collections.abc import Set as AbstractSet
from copy import deepcopy
from functools import cached_property
from inspect import Parameter
from itertools import zip_longest
from types import BuiltinFunctionType, CodeType, FunctionType, GeneratorType, LambdaType, ModuleType
from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload
from pydantic_core import MISSING, PydanticUndefined
from typing_extensions import TypeAlias, TypeGuard, deprecated
from pydantic import PydanticDeprecatedSince211
from . import _repr, _typing_extra
from ._import_utils import import_cached_base_model
if TYPE_CHECKING:
# TODO remove type error comments when we drop support for Python 3.9
MappingIntStrAny: TypeAlias = Mapping[int, Any] | Mapping[str, Any] # pyright: ignore[reportGeneralTypeIssues]
AbstractSetIntStr: TypeAlias = AbstractSet[int] | AbstractSet[str] # pyright: ignore[reportGeneralTypeIssues]
from ..main import BaseModel
# these are types that are returned unchanged by deepcopy
IMMUTABLE_NON_COLLECTIONS_TYPES: set[type[Any]] = {
int,
float,
complex,
str,
bool,
bytes,
type,
_typing_extra.NoneType,
FunctionType,
BuiltinFunctionType,
LambdaType,
weakref.ref,
CodeType,
# note: including ModuleType will differ from behaviour of deepcopy by not producing error.
# It might be not a good idea in general, but considering that this function used only internally
# against default values of fields, this will allow to actually have a field with module as default value
ModuleType,
NotImplemented.__class__,
Ellipsis.__class__,
}
# these are types that if empty, might be copied with simple copy() instead of deepcopy()
BUILTIN_COLLECTIONS: set[type[Any]] = {
list,
set,
tuple,
frozenset,
dict,
OrderedDict,
defaultdict,
deque,
}
def can_be_positional(param: Parameter) -> bool:
"""Return whether the parameter accepts a positional argument.
```python {test="skip" lint="skip"}
def func(a, /, b, *, c):
pass
params = inspect.signature(func).parameters
can_be_positional(params['a'])
#> True
can_be_positional(params['b'])
#> True
can_be_positional(params['c'])
#> False
```
"""
return param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD)
def sequence_like(v: Any) -> bool:
return isinstance(v, (list, tuple, set, frozenset, GeneratorType, deque))
def lenient_isinstance(o: Any, class_or_tuple: type[Any] | tuple[type[Any], ...] | None) -> bool: # pragma: no cover
try:
return isinstance(o, class_or_tuple) # type: ignore[arg-type]
except TypeError:
return False
def lenient_issubclass(cls: Any, class_or_tuple: Any) -> bool: # pragma: no cover
try:
return isinstance(cls, type) and issubclass(cls, class_or_tuple)
except TypeError:
if isinstance(cls, _typing_extra.WithArgsTypes):
return False
raise # pragma: no cover
def is_model_class(cls: Any) -> TypeGuard[type[BaseModel]]:
"""Returns true if cls is a _proper_ subclass of BaseModel, and provides proper type-checking,
unlike raw calls to lenient_issubclass.
"""
BaseModel = import_cached_base_model()
return lenient_issubclass(cls, BaseModel) and cls is not BaseModel
def is_valid_identifier(identifier: str) -> bool:
"""Checks that a string is a valid identifier and not a Python keyword.
:param identifier: The identifier to test.
:return: True if the identifier is valid.
"""
return identifier.isidentifier() and not keyword.iskeyword(identifier)
KeyType = TypeVar('KeyType')
def deep_update(mapping: dict[KeyType, Any], *updating_mappings: dict[KeyType, Any]) -> dict[KeyType, Any]:
updated_mapping = mapping.copy()
for updating_mapping in updating_mappings:
for k, v in updating_mapping.items():
if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict):
updated_mapping[k] = deep_update(updated_mapping[k], v)
else:
updated_mapping[k] = v
return updated_mapping
def update_not_none(mapping: dict[Any, Any], **update: Any) -> None:
mapping.update({k: v for k, v in update.items() if v is not None})
T = TypeVar('T')
def unique_list(
input_list: list[T] | tuple[T, ...],
*,
name_factory: Callable[[T], str] = str,
) -> list[T]:
"""Make a list unique while maintaining order.
We update the list if another one with the same name is set
(e.g. model validator overridden in subclass).
"""
result: list[T] = []
result_names: list[str] = []
for v in input_list:
v_name = name_factory(v)
if v_name not in result_names:
result_names.append(v_name)
result.append(v)
else:
result[result_names.index(v_name)] = v
return result
class ValueItems(_repr.Representation):
"""Class for more convenient calculation of excluded or included fields on values."""
__slots__ = ('_items', '_type')
def __init__(self, value: Any, items: AbstractSetIntStr | MappingIntStrAny) -> None:
items = self._coerce_items(items)
if isinstance(value, (list, tuple)):
items = self._normalize_indexes(items, len(value)) # type: ignore
self._items: MappingIntStrAny = items # type: ignore
def is_excluded(self, item: Any) -> bool:
"""Check if item is fully excluded.
:param item: key or index of a value
"""
return self.is_true(self._items.get(item))
def is_included(self, item: Any) -> bool:
"""Check if value is contained in self._items.
:param item: key or index of value
"""
return item in self._items
def for_element(self, e: int | str) -> AbstractSetIntStr | MappingIntStrAny | None:
""":param e: key or index of element on value
:return: raw values for element if self._items is dict and contain needed element
"""
item = self._items.get(e) # type: ignore
return item if not self.is_true(item) else None
def _normalize_indexes(self, items: MappingIntStrAny, v_length: int) -> dict[int | str, Any]:
""":param items: dict or set of indexes which will be normalized
:param v_length: length of sequence indexes of which will be
>>> self._normalize_indexes({0: True, -2: True, -1: True}, 4)
{0: True, 2: True, 3: True}
>>> self._normalize_indexes({'__all__': True}, 4)
{0: True, 1: True, 2: True, 3: True}
"""
normalized_items: dict[int | str, Any] = {}
all_items = None
for i, v in items.items():
if not (isinstance(v, Mapping) or isinstance(v, AbstractSet) or self.is_true(v)):
raise TypeError(f'Unexpected type of exclude value for index "{i}" {v.__class__}')
if i == '__all__':
all_items = self._coerce_value(v)
continue
if not isinstance(i, int):
raise TypeError(
'Excluding fields from a sequence of sub-models or dicts must be performed index-wise: '
'expected integer keys or keyword "__all__"'
)
normalized_i = v_length + i if i < 0 else i
normalized_items[normalized_i] = self.merge(v, normalized_items.get(normalized_i))
if not all_items:
return normalized_items
if self.is_true(all_items):
for i in range(v_length):
normalized_items.setdefault(i, ...)
return normalized_items
for i in range(v_length):
normalized_item = normalized_items.setdefault(i, {})
if not self.is_true(normalized_item):
normalized_items[i] = self.merge(all_items, normalized_item)
return normalized_items
@classmethod
def merge(cls, base: Any, override: Any, intersect: bool = False) -> Any:
"""Merge a `base` item with an `override` item.
Both `base` and `override` are converted to dictionaries if possible.
Sets are converted to dictionaries with the sets entries as keys and
Ellipsis as values.
Each key-value pair existing in `base` is merged with `override`,
while the rest of the key-value pairs are updated recursively with this function.
Merging takes place based on the "union" of keys if `intersect` is
set to `False` (default) and on the intersection of keys if
`intersect` is set to `True`.
"""
override = cls._coerce_value(override)
base = cls._coerce_value(base)
if override is None:
return base
if cls.is_true(base) or base is None:
return override
if cls.is_true(override):
return base if intersect else override
# intersection or union of keys while preserving ordering:
if intersect:
merge_keys = [k for k in base if k in override] + [k for k in override if k in base]
else:
merge_keys = list(base) + [k for k in override if k not in base]
merged: dict[int | str, Any] = {}
for k in merge_keys:
merged_item = cls.merge(base.get(k), override.get(k), intersect=intersect)
if merged_item is not None:
merged[k] = merged_item
return merged
@staticmethod
def _coerce_items(items: AbstractSetIntStr | MappingIntStrAny) -> MappingIntStrAny:
if isinstance(items, Mapping):
pass
elif isinstance(items, AbstractSet):
items = dict.fromkeys(items, ...) # type: ignore
else:
class_name = getattr(items, '__class__', '???')
raise TypeError(f'Unexpected type of exclude value {class_name}')
return items # type: ignore
@classmethod
def _coerce_value(cls, value: Any) -> Any:
if value is None or cls.is_true(value):
return value
return cls._coerce_items(value)
@staticmethod
def is_true(v: Any) -> bool:
return v is True or v is ...
def __repr_args__(self) -> _repr.ReprArgs:
return [(None, self._items)]
if TYPE_CHECKING:
def LazyClassAttribute(name: str, get_value: Callable[[], T]) -> T: ...
else:
class LazyClassAttribute:
"""A descriptor exposing an attribute only accessible on a class (hidden from instances).
The attribute is lazily computed and cached during the first access.
"""
def __init__(self, name: str, get_value: Callable[[], Any]) -> None:
self.name = name
self.get_value = get_value
@cached_property
def value(self) -> Any:
return self.get_value()
def __get__(self, instance: Any, owner: type[Any]) -> None:
if instance is None:
return self.value
raise AttributeError(f'{self.name!r} attribute of {owner.__name__!r} is class-only')
Obj = TypeVar('Obj')
def smart_deepcopy(obj: Obj) -> Obj:
"""Return type as is for immutable built-in types
Use obj.copy() for built-in empty collections
Use copy.deepcopy() for non-empty collections and unknown objects.
"""
if obj is MISSING or obj is PydanticUndefined:
return obj # pyright: ignore[reportReturnType]
obj_type = obj.__class__
if obj_type in IMMUTABLE_NON_COLLECTIONS_TYPES:
return obj # fastest case: obj is immutable and not collection therefore will not be copied anyway
try:
if not obj and obj_type in BUILTIN_COLLECTIONS:
# faster way for empty collections, no need to copy its members
return obj if obj_type is tuple else obj.copy() # tuple doesn't have copy method # type: ignore
except (TypeError, ValueError, RuntimeError):
# do we really dare to catch ALL errors? Seems a bit risky
pass
return deepcopy(obj) # slowest way when we actually might need a deepcopy
_SENTINEL = object()
def all_identical(left: Iterable[Any], right: Iterable[Any]) -> bool:
"""Check that the items of `left` are the same objects as those in `right`.
>>> a, b = object(), object()
>>> all_identical([a, b, a], [a, b, a])
True
>>> all_identical([a, b, [a]], [a, b, [a]]) # new list object, while "equal" is not "identical"
False
"""
for left_item, right_item in zip_longest(left, right, fillvalue=_SENTINEL):
if left_item is not right_item:
return False
return True
def get_first_not_none(a: Any, b: Any) -> Any:
"""Return the first argument if it is not `None`, otherwise return the second argument."""
return a if a is not None else b
@dataclasses.dataclass(frozen=True)
class SafeGetItemProxy:
"""Wrapper redirecting `__getitem__` to `get` with a sentinel value as default
This makes is safe to use in `operator.itemgetter` when some keys may be missing
"""
# Define __slots__manually for performances
# @dataclasses.dataclass() only support slots=True in python>=3.10
__slots__ = ('wrapped',)
wrapped: Mapping[str, Any]
def __getitem__(self, key: str, /) -> Any:
return self.wrapped.get(key, _SENTINEL)
# required to pass the object to operator.itemgetter() instances due to a quirk of typeshed
# https://github.com/python/mypy/issues/13713
# https://github.com/python/typeshed/pull/8785
# Since this is typing-only, hide it in a typing.TYPE_CHECKING block
if TYPE_CHECKING:
def __contains__(self, key: str, /) -> bool:
return self.wrapped.__contains__(key)
_ModelT = TypeVar('_ModelT', bound='BaseModel')
_RT = TypeVar('_RT')
class deprecated_instance_property(Generic[_ModelT, _RT]):
"""A decorator exposing the decorated class method as a property, with a warning on instance access.
This decorator takes a class method defined on the `BaseModel` class and transforms it into
an attribute. The attribute can be accessed on both the class and instances of the class. If accessed
via an instance, a deprecation warning is emitted stating that instance access will be removed in V3.
"""
def __init__(self, fget: Callable[[type[_ModelT]], _RT], /) -> None:
# Note: fget should be a classmethod:
self.fget = fget
@overload
def __get__(self, instance: None, objtype: type[_ModelT]) -> _RT: ...
@overload
@deprecated(
'Accessing this attribute on the instance is deprecated, and will be removed in Pydantic V3. '
'Instead, you should access this attribute from the model class.',
category=None,
)
def __get__(self, instance: _ModelT, objtype: type[_ModelT]) -> _RT: ...
def __get__(self, instance: _ModelT | None, objtype: type[_ModelT]) -> _RT:
if instance is not None:
# fmt: off
attr_name = (
self.fget.__name__
if sys.version_info >= (3, 10)
else self.fget.__func__.__name__ # pyright: ignore[reportFunctionMemberAccess]
)
# fmt: on
warnings.warn(
f'Accessing the {attr_name!r} attribute on the instance is deprecated. '
'Instead, you should access this attribute from the model class.',
category=PydanticDeprecatedSince211,
stacklevel=2,
)
return self.fget.__get__(instance, objtype)()
@@ -0,0 +1,534 @@
"""Validator functions for standard library types.
Import of this module is deferred since it contains imports of many standard library modules.
"""
from __future__ import annotations as _annotations
import collections.abc
import math
import re
import typing
from collections.abc import Sequence
from decimal import Decimal
from fractions import Fraction
from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
from typing import Any, Callable, TypeVar, Union, cast
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import typing_extensions
from pydantic_core import PydanticCustomError, PydanticKnownError, core_schema
from typing_extensions import get_args, get_origin
from typing_inspection import typing_objects
from pydantic._internal._import_utils import import_cached_field_info
from pydantic.errors import PydanticSchemaGenerationError
def sequence_validator(
input_value: Sequence[Any],
/,
validator: core_schema.ValidatorFunctionWrapHandler,
) -> Sequence[Any]:
"""Validator for `Sequence` types, isinstance(v, Sequence) has already been called."""
value_type = type(input_value)
# We don't accept any plain string as a sequence
# Relevant issue: https://github.com/pydantic/pydantic/issues/5595
if issubclass(value_type, (str, bytes)):
raise PydanticCustomError(
'sequence_str',
"'{type_name}' instances are not allowed as a Sequence value",
{'type_name': value_type.__name__},
)
# TODO: refactor sequence validation to validate with either a list or a tuple
# schema, depending on the type of the value.
# Additionally, we should be able to remove one of either this validator or the
# SequenceValidator in _std_types_schema.py (preferably this one, while porting over some logic).
# Effectively, a refactor for sequence validation is needed.
if value_type is tuple:
input_value = list(input_value)
v_list = validator(input_value)
# the rest of the logic is just re-creating the original type from `v_list`
if value_type is list:
return v_list
elif issubclass(value_type, range):
# return the list as we probably can't re-create the range
return v_list
elif value_type is tuple:
return tuple(v_list)
else:
# best guess at how to re-create the original type, more custom construction logic might be required
return value_type(v_list) # type: ignore[call-arg]
def import_string(value: Any) -> Any:
if isinstance(value, str):
try:
return _import_string_logic(value)
except ImportError as e:
raise PydanticCustomError('import_error', 'Invalid python path: {error}', {'error': str(e)}) from e
else:
# otherwise we just return the value and let the next validator do the rest of the work
return value
def _import_string_logic(dotted_path: str) -> Any:
"""Inspired by uvicorn — dotted paths should include a colon before the final item if that item is not a module.
(This is necessary to distinguish between a submodule and an attribute when there is a conflict.).
If the dotted path does not include a colon and the final item is not a valid module, importing as an attribute
rather than a submodule will be attempted automatically.
So, for example, the following values of `dotted_path` result in the following returned values:
* 'collections': <module 'collections'>
* 'collections.abc': <module 'collections.abc'>
* 'collections.abc:Mapping': <class 'collections.abc.Mapping'>
* `collections.abc.Mapping`: <class 'collections.abc.Mapping'> (though this is a bit slower than the previous line)
An error will be raised under any of the following scenarios:
* `dotted_path` contains more than one colon (e.g., 'collections:abc:Mapping')
* the substring of `dotted_path` before the colon is not a valid module in the environment (e.g., '123:Mapping')
* the substring of `dotted_path` after the colon is not an attribute of the module (e.g., 'collections:abc123')
"""
from importlib import import_module
components = dotted_path.strip().split(':')
if len(components) > 2:
raise ImportError(f"Import strings should have at most one ':'; received {dotted_path!r}")
attribute = None
if len(components) == 2:
attribute = components[1]
module_path = components[0]
if not module_path:
raise ImportError(f'Import strings should have a nonempty module name; received {dotted_path!r}')
try:
module = import_module(module_path)
except ModuleNotFoundError:
if attribute is None and '.' in module_path:
# Try interpreting the final dotted segment as an attribute, not a submodule
maybe_module_path, maybe_attribute = module_path.rsplit('.', 1)
try:
return _import_string_logic(f'{maybe_module_path}:{maybe_attribute}')
except ImportError:
pass
raise
if attribute is not None:
try:
return getattr(module, attribute)
except AttributeError as e:
raise ImportError(f'cannot import name {attribute!r} from {module_path!r}') from e
else:
return module
def pattern_either_validator(input_value: Any, /) -> re.Pattern[Any]:
if isinstance(input_value, re.Pattern):
return input_value
elif isinstance(input_value, (str, bytes)):
# todo strict mode
return compile_pattern(input_value) # type: ignore
else:
raise PydanticCustomError('pattern_type', 'Input should be a valid pattern')
def pattern_str_validator(input_value: Any, /) -> re.Pattern[str]:
if isinstance(input_value, re.Pattern):
if isinstance(input_value.pattern, str):
return input_value
else:
raise PydanticCustomError('pattern_str_type', 'Input should be a string pattern')
elif isinstance(input_value, str):
return compile_pattern(input_value)
elif isinstance(input_value, bytes):
raise PydanticCustomError('pattern_str_type', 'Input should be a string pattern')
else:
raise PydanticCustomError('pattern_type', 'Input should be a valid pattern')
def pattern_bytes_validator(input_value: Any, /) -> re.Pattern[bytes]:
if isinstance(input_value, re.Pattern):
if isinstance(input_value.pattern, bytes):
return input_value
else:
raise PydanticCustomError('pattern_bytes_type', 'Input should be a bytes pattern')
elif isinstance(input_value, bytes):
return compile_pattern(input_value)
elif isinstance(input_value, str):
raise PydanticCustomError('pattern_bytes_type', 'Input should be a bytes pattern')
else:
raise PydanticCustomError('pattern_type', 'Input should be a valid pattern')
PatternType = TypeVar('PatternType', str, bytes)
def compile_pattern(pattern: PatternType) -> re.Pattern[PatternType]:
try:
return re.compile(pattern)
except re.error:
raise PydanticCustomError('pattern_regex', 'Input should be a valid regular expression')
def ip_v4_address_validator(input_value: Any, /) -> IPv4Address:
if isinstance(input_value, IPv4Address):
return input_value
try:
return IPv4Address(input_value)
except ValueError:
raise PydanticCustomError('ip_v4_address', 'Input is not a valid IPv4 address')
def ip_v6_address_validator(input_value: Any, /) -> IPv6Address:
if isinstance(input_value, IPv6Address):
return input_value
try:
return IPv6Address(input_value)
except ValueError:
raise PydanticCustomError('ip_v6_address', 'Input is not a valid IPv6 address')
def ip_v4_network_validator(input_value: Any, /) -> IPv4Network:
"""Assume IPv4Network initialised with a default `strict` argument.
See more:
https://docs.python.org/library/ipaddress.html#ipaddress.IPv4Network
"""
if isinstance(input_value, IPv4Network):
return input_value
try:
return IPv4Network(input_value)
except ValueError:
raise PydanticCustomError('ip_v4_network', 'Input is not a valid IPv4 network')
def ip_v6_network_validator(input_value: Any, /) -> IPv6Network:
"""Assume IPv6Network initialised with a default `strict` argument.
See more:
https://docs.python.org/library/ipaddress.html#ipaddress.IPv6Network
"""
if isinstance(input_value, IPv6Network):
return input_value
try:
return IPv6Network(input_value)
except ValueError:
raise PydanticCustomError('ip_v6_network', 'Input is not a valid IPv6 network')
def ip_v4_interface_validator(input_value: Any, /) -> IPv4Interface:
if isinstance(input_value, IPv4Interface):
return input_value
try:
return IPv4Interface(input_value)
except ValueError:
raise PydanticCustomError('ip_v4_interface', 'Input is not a valid IPv4 interface')
def ip_v6_interface_validator(input_value: Any, /) -> IPv6Interface:
if isinstance(input_value, IPv6Interface):
return input_value
try:
return IPv6Interface(input_value)
except ValueError:
raise PydanticCustomError('ip_v6_interface', 'Input is not a valid IPv6 interface')
def fraction_validator(input_value: Any, /) -> Fraction:
if isinstance(input_value, Fraction):
return input_value
try:
return Fraction(input_value)
except ValueError:
raise PydanticCustomError('fraction_parsing', 'Input is not a valid fraction')
def forbid_inf_nan_check(x: Any) -> Any:
if not math.isfinite(x):
raise PydanticKnownError('finite_number')
return x
def _safe_repr(v: Any) -> int | float | str:
"""The context argument for `PydanticKnownError` requires a number or str type, so we do a simple repr() coercion for types like timedelta.
See tests/test_types.py::test_annotated_metadata_any_order for some context.
"""
if isinstance(v, (int, float, str)):
return v
return repr(v)
def greater_than_validator(x: Any, gt: Any) -> Any:
try:
if not (x > gt):
raise PydanticKnownError('greater_than', {'gt': _safe_repr(gt)})
return x
except TypeError:
raise TypeError(f"Unable to apply constraint 'gt' to supplied value {x}")
def greater_than_or_equal_validator(x: Any, ge: Any) -> Any:
try:
if not (x >= ge):
raise PydanticKnownError('greater_than_equal', {'ge': _safe_repr(ge)})
return x
except TypeError:
raise TypeError(f"Unable to apply constraint 'ge' to supplied value {x}")
def less_than_validator(x: Any, lt: Any) -> Any:
try:
if not (x < lt):
raise PydanticKnownError('less_than', {'lt': _safe_repr(lt)})
return x
except TypeError:
raise TypeError(f"Unable to apply constraint 'lt' to supplied value {x}")
def less_than_or_equal_validator(x: Any, le: Any) -> Any:
try:
if not (x <= le):
raise PydanticKnownError('less_than_equal', {'le': _safe_repr(le)})
return x
except TypeError:
raise TypeError(f"Unable to apply constraint 'le' to supplied value {x}")
def multiple_of_validator(x: Any, multiple_of: Any) -> Any:
try:
if x % multiple_of:
raise PydanticKnownError('multiple_of', {'multiple_of': _safe_repr(multiple_of)})
return x
except TypeError:
raise TypeError(f"Unable to apply constraint 'multiple_of' to supplied value {x}")
def min_length_validator(x: Any, min_length: Any) -> Any:
try:
if not (len(x) >= min_length):
raise PydanticKnownError(
'too_short', {'field_type': 'Value', 'min_length': min_length, 'actual_length': len(x)}
)
return x
except TypeError:
raise TypeError(f"Unable to apply constraint 'min_length' to supplied value {x}")
def max_length_validator(x: Any, max_length: Any) -> Any:
try:
if len(x) > max_length:
raise PydanticKnownError(
'too_long',
{'field_type': 'Value', 'max_length': max_length, 'actual_length': len(x)},
)
return x
except TypeError:
raise TypeError(f"Unable to apply constraint 'max_length' to supplied value {x}")
def _extract_decimal_digits_info(decimal: Decimal) -> tuple[int, int]:
"""Compute the total number of digits and decimal places for a given [`Decimal`][decimal.Decimal] instance.
This function handles both normalized and non-normalized Decimal instances.
Example: Decimal('1.230') -> 4 digits, 3 decimal places
Args:
decimal (Decimal): The decimal number to analyze.
Returns:
tuple[int, int]: A tuple containing the number of decimal places and total digits.
Though this could be divided into two separate functions, the logic is easier to follow if we couple the computation
of the number of decimals and digits together.
"""
try:
decimal_tuple = decimal.as_tuple()
assert isinstance(decimal_tuple.exponent, int)
exponent = decimal_tuple.exponent
num_digits = len(decimal_tuple.digits)
if exponent >= 0:
# A positive exponent adds that many trailing zeros
# Ex: digit_tuple=(1, 2, 3), exponent=2 -> 12300 -> 0 decimal places, 5 digits
num_digits += exponent
decimal_places = 0
else:
# If the absolute value of the negative exponent is larger than the
# number of digits, then it's the same as the number of digits,
# because it'll consume all the digits in digit_tuple and then
# add abs(exponent) - len(digit_tuple) leading zeros after the decimal point.
# Ex: digit_tuple=(1, 2, 3), exponent=-2 -> 1.23 -> 2 decimal places, 3 digits
# Ex: digit_tuple=(1, 2, 3), exponent=-4 -> 0.0123 -> 4 decimal places, 4 digits
decimal_places = abs(exponent)
num_digits = max(num_digits, decimal_places)
return decimal_places, num_digits
except (AssertionError, AttributeError):
raise TypeError(f'Unable to extract decimal digits info from supplied value {decimal}')
def max_digits_validator(x: Any, max_digits: Any) -> Any:
try:
_, num_digits = _extract_decimal_digits_info(x)
_, normalized_num_digits = _extract_decimal_digits_info(x.normalize())
if (num_digits > max_digits) and (normalized_num_digits > max_digits):
raise PydanticKnownError(
'decimal_max_digits',
{'max_digits': max_digits},
)
return x
except TypeError:
raise TypeError(f"Unable to apply constraint 'max_digits' to supplied value {x}")
def decimal_places_validator(x: Any, decimal_places: Any) -> Any:
try:
decimal_places_, _ = _extract_decimal_digits_info(x)
if decimal_places_ > decimal_places:
normalized_decimal_places, _ = _extract_decimal_digits_info(x.normalize())
if normalized_decimal_places > decimal_places:
raise PydanticKnownError(
'decimal_max_places',
{'decimal_places': decimal_places},
)
return x
except TypeError:
raise TypeError(f"Unable to apply constraint 'decimal_places' to supplied value {x}")
def deque_validator(input_value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> collections.deque[Any]:
return collections.deque(handler(input_value), maxlen=getattr(input_value, 'maxlen', None))
def defaultdict_validator(
input_value: Any, handler: core_schema.ValidatorFunctionWrapHandler, default_default_factory: Callable[[], Any]
) -> collections.defaultdict[Any, Any]:
if isinstance(input_value, collections.defaultdict):
default_factory = input_value.default_factory
return collections.defaultdict(default_factory, handler(input_value))
else:
return collections.defaultdict(default_default_factory, handler(input_value))
def get_defaultdict_default_default_factory(values_source_type: Any) -> Callable[[], Any]:
FieldInfo = import_cached_field_info()
values_type_origin = get_origin(values_source_type)
def infer_default() -> Callable[[], Any]:
allowed_default_types: dict[Any, Any] = {
tuple: tuple,
collections.abc.Sequence: tuple,
collections.abc.MutableSequence: list,
list: list,
typing.Sequence: list,
set: set,
typing.MutableSet: set,
collections.abc.MutableSet: set,
collections.abc.Set: frozenset,
typing.MutableMapping: dict,
typing.Mapping: dict,
collections.abc.Mapping: dict,
collections.abc.MutableMapping: dict,
float: float,
int: int,
str: str,
bool: bool,
}
values_type = values_type_origin or values_source_type
instructions = 'set using `DefaultDict[..., Annotated[..., Field(default_factory=...)]]`'
if typing_objects.is_typevar(values_type):
def type_var_default_factory() -> None:
raise RuntimeError(
'Generic defaultdict cannot be used without a concrete value type or an'
' explicit default factory, ' + instructions
)
return type_var_default_factory
elif values_type not in allowed_default_types:
# a somewhat subjective set of types that have reasonable default values
allowed_msg = ', '.join([t.__name__ for t in set(allowed_default_types.values())])
raise PydanticSchemaGenerationError(
f'Unable to infer a default factory for keys of type {values_source_type}.'
f' Only {allowed_msg} are supported, other types require an explicit default factory'
' ' + instructions
)
return allowed_default_types[values_type]
# Assume Annotated[..., Field(...)]
if typing_objects.is_annotated(values_type_origin):
field_info = next((v for v in get_args(values_source_type) if isinstance(v, FieldInfo)), None)
else:
field_info = None
if field_info and field_info.default_factory:
# Assume the default factory does not take any argument:
default_default_factory = cast(Callable[[], Any], field_info.default_factory)
else:
default_default_factory = infer_default()
return default_default_factory
def validate_str_is_valid_iana_tz(value: Any, /) -> ZoneInfo:
if isinstance(value, ZoneInfo):
return value
try:
return ZoneInfo(value)
except (ZoneInfoNotFoundError, ValueError, TypeError):
raise PydanticCustomError('zoneinfo_str', 'invalid timezone: {value}', {'value': value})
NUMERIC_VALIDATOR_LOOKUP: dict[str, Callable] = {
'gt': greater_than_validator,
'ge': greater_than_or_equal_validator,
'lt': less_than_validator,
'le': less_than_or_equal_validator,
'multiple_of': multiple_of_validator,
'min_length': min_length_validator,
'max_length': max_length_validator,
'max_digits': max_digits_validator,
'decimal_places': decimal_places_validator,
}
IpType = Union[IPv4Address, IPv6Address, IPv4Network, IPv6Network, IPv4Interface, IPv6Interface]
IP_VALIDATOR_LOOKUP: dict[type[IpType], Callable] = {
IPv4Address: ip_v4_address_validator,
IPv6Address: ip_v6_address_validator,
IPv4Network: ip_v4_network_validator,
IPv6Network: ip_v6_network_validator,
IPv4Interface: ip_v4_interface_validator,
IPv6Interface: ip_v6_interface_validator,
}
MAPPING_ORIGIN_MAP: dict[Any, Any] = {
typing.DefaultDict: collections.defaultdict, # noqa: UP006
collections.defaultdict: collections.defaultdict,
typing.OrderedDict: collections.OrderedDict, # noqa: UP006
collections.OrderedDict: collections.OrderedDict,
typing_extensions.OrderedDict: collections.OrderedDict,
typing.Counter: collections.Counter,
collections.Counter: collections.Counter,
# this doesn't handle subclasses of these
typing.Mapping: dict,
typing.MutableMapping: dict,
# parametrized typing.{Mutable}Mapping creates one of these
collections.abc.Mapping: dict,
collections.abc.MutableMapping: dict,
}