utils.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. """Utility functions for pydantic-settings sources."""
  2. from __future__ import annotations as _annotations
  3. from collections import deque
  4. from collections.abc import Mapping, Sequence
  5. from dataclasses import is_dataclass
  6. from enum import Enum
  7. from typing import Any, Optional, cast
  8. from pydantic import BaseModel, Json, RootModel, Secret
  9. from pydantic._internal._utils import is_model_class
  10. from pydantic.dataclasses import is_pydantic_dataclass
  11. from typing_extensions import get_args, get_origin
  12. from typing_inspection import typing_objects
  13. from ..exceptions import SettingsError
  14. from ..utils import _lenient_issubclass
  15. from .types import EnvNoneType
  16. def _get_env_var_key(key: str, case_sensitive: bool = False) -> str:
  17. return key if case_sensitive else key.lower()
  18. def _parse_env_none_str(value: str | None, parse_none_str: str | None = None) -> str | None | EnvNoneType:
  19. return value if not (value == parse_none_str and parse_none_str is not None) else EnvNoneType(value)
  20. def parse_env_vars(
  21. env_vars: Mapping[str, str | None],
  22. case_sensitive: bool = False,
  23. ignore_empty: bool = False,
  24. parse_none_str: str | None = None,
  25. ) -> Mapping[str, str | None]:
  26. return {
  27. _get_env_var_key(k, case_sensitive): _parse_env_none_str(v, parse_none_str)
  28. for k, v in env_vars.items()
  29. if not (ignore_empty and v == '')
  30. }
  31. def _annotation_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool:
  32. # If the model is a root model, the root annotation should be used to
  33. # evaluate the complexity.
  34. if annotation is not None and _lenient_issubclass(annotation, RootModel) and annotation is not RootModel:
  35. annotation = cast('type[RootModel[Any]]', annotation)
  36. root_annotation = annotation.model_fields['root'].annotation
  37. if root_annotation is not None:
  38. annotation = root_annotation
  39. if any(isinstance(md, Json) for md in metadata): # type: ignore[misc]
  40. return False
  41. origin = get_origin(annotation)
  42. # Check if annotation is of the form Annotated[type, metadata].
  43. if typing_objects.is_annotated(origin):
  44. # Return result of recursive call on inner type.
  45. inner, *meta = get_args(annotation)
  46. return _annotation_is_complex(inner, meta)
  47. if origin is Secret:
  48. return False
  49. return (
  50. _annotation_is_complex_inner(annotation)
  51. or _annotation_is_complex_inner(origin)
  52. or hasattr(origin, '__pydantic_core_schema__')
  53. or hasattr(origin, '__get_pydantic_core_schema__')
  54. )
  55. def _annotation_is_complex_inner(annotation: type[Any] | None) -> bool:
  56. if _lenient_issubclass(annotation, (str, bytes)):
  57. return False
  58. return _lenient_issubclass(
  59. annotation, (BaseModel, Mapping, Sequence, tuple, set, frozenset, deque)
  60. ) or is_dataclass(annotation)
  61. def _union_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool:
  62. """Check if a union type contains any complex types."""
  63. return any(_annotation_is_complex(arg, metadata) for arg in get_args(annotation))
  64. def _annotation_contains_types(
  65. annotation: type[Any] | None,
  66. types: tuple[Any, ...],
  67. is_include_origin: bool = True,
  68. is_strip_annotated: bool = False,
  69. ) -> bool:
  70. """Check if a type annotation contains any of the specified types."""
  71. if is_strip_annotated:
  72. annotation = _strip_annotated(annotation)
  73. if is_include_origin is True and get_origin(annotation) in types:
  74. return True
  75. for type_ in get_args(annotation):
  76. if _annotation_contains_types(type_, types, is_include_origin=True, is_strip_annotated=is_strip_annotated):
  77. return True
  78. return annotation in types
  79. def _strip_annotated(annotation: Any) -> Any:
  80. if typing_objects.is_annotated(get_origin(annotation)):
  81. return annotation.__origin__
  82. else:
  83. return annotation
  84. def _annotation_enum_val_to_name(annotation: type[Any] | None, value: Any) -> Optional[str]:
  85. for type_ in (annotation, get_origin(annotation), *get_args(annotation)):
  86. if _lenient_issubclass(type_, Enum):
  87. if value in tuple(val.value for val in type_):
  88. return type_(value).name
  89. return None
  90. def _annotation_enum_name_to_val(annotation: type[Any] | None, name: Any) -> Any:
  91. for type_ in (annotation, get_origin(annotation), *get_args(annotation)):
  92. if _lenient_issubclass(type_, Enum):
  93. if name in tuple(val.name for val in type_):
  94. return type_[name]
  95. return None
  96. def _get_model_fields(model_cls: type[Any]) -> dict[str, Any]:
  97. """Get fields from a pydantic model or dataclass."""
  98. if is_pydantic_dataclass(model_cls) and hasattr(model_cls, '__pydantic_fields__'):
  99. return model_cls.__pydantic_fields__
  100. if is_model_class(model_cls):
  101. return model_cls.model_fields
  102. raise SettingsError(f'Error: {model_cls.__name__} is not subclass of BaseModel or pydantic.dataclasses.dataclass')
  103. def _get_alias_names(
  104. field_name: str, field_info: Any, alias_path_args: dict[str, str] = {}, case_sensitive: bool = True
  105. ) -> tuple[tuple[str, ...], bool]:
  106. """Get alias names for a field, handling alias paths and case sensitivity."""
  107. from pydantic import AliasChoices, AliasPath
  108. alias_names: list[str] = []
  109. is_alias_path_only: bool = True
  110. if not any((field_info.alias, field_info.validation_alias)):
  111. alias_names += [field_name]
  112. is_alias_path_only = False
  113. else:
  114. new_alias_paths: list[AliasPath] = []
  115. for alias in (field_info.alias, field_info.validation_alias):
  116. if alias is None:
  117. continue
  118. elif isinstance(alias, str):
  119. alias_names.append(alias)
  120. is_alias_path_only = False
  121. elif isinstance(alias, AliasChoices):
  122. for name in alias.choices:
  123. if isinstance(name, str):
  124. alias_names.append(name)
  125. is_alias_path_only = False
  126. else:
  127. new_alias_paths.append(name)
  128. else:
  129. new_alias_paths.append(alias)
  130. for alias_path in new_alias_paths:
  131. name = cast(str, alias_path.path[0])
  132. name = name.lower() if not case_sensitive else name
  133. alias_path_args[name] = 'dict' if len(alias_path.path) > 2 else 'list'
  134. if not alias_names and is_alias_path_only:
  135. alias_names.append(name)
  136. if not case_sensitive:
  137. alias_names = [alias_name.lower() for alias_name in alias_names]
  138. return tuple(dict.fromkeys(alias_names)), is_alias_path_only
  139. def _is_function(obj: Any) -> bool:
  140. """Check if an object is a function."""
  141. from types import BuiltinFunctionType, FunctionType
  142. return isinstance(obj, (FunctionType, BuiltinFunctionType))
  143. __all__ = [
  144. '_annotation_contains_types',
  145. '_annotation_enum_name_to_val',
  146. '_annotation_enum_val_to_name',
  147. '_annotation_is_complex',
  148. '_annotation_is_complex_inner',
  149. '_get_alias_names',
  150. '_get_env_var_key',
  151. '_get_model_fields',
  152. '_is_function',
  153. '_parse_env_none_str',
  154. '_strip_annotated',
  155. '_union_is_complex',
  156. 'parse_env_vars',
  157. ]