"""
Internal data structures.
"""
import dataclasses
from collections.abc import Callable, Collection
from enum import Enum
from pathlib import Path
from types import MappingProxyType
from typing import (
TYPE_CHECKING,
Any,
Generic,
Literal,
NamedTuple,
NewType,
Optional,
Protocol,
TypeVar,
Union,
)
from .constants import SECRET_REPR
if TYPE_CHECKING:
from typing import TypeGuard
__all__ = [
"AUTO",
"ET",
"SECRETS_TYPES",
"SECRET_REPR",
"ST",
"CollectionChildOptions",
"LoadedSettings",
"LoadedValue",
"LoaderMeta",
"MergedSettings",
"OptionDict",
"OptionInfo",
"OptionList",
"OptionName",
"OptionPath",
"Secret",
"SecretStr",
"SettingsClass",
"SettingsDict",
"SettingsInstance",
"T",
"is_new_type",
]
#: A generic TypeVar
T = TypeVar("T")
#: A TypeVar for :class:`~enum.Enum` types
ET = TypeVar("ET", bound=Enum) # Enum type
#: A TypeVar for settings instances
ST = TypeVar("ST") # Type var for SettingsInstance
SettingsClass = type
SettingsInstance = Any
OptionName = str
OptionPath = str
SettingsDict = dict[OptionName, Union[Any, "SettingsDict"]]
"""
A dictionary with all loaded settings.
Values are not converted to their final type yet.
"""
class _Auto:
"""
Sentinel class to indicate the lack of a value when ``None`` is ambiguous.
``_Auto`` is a singleton. There is only ever one of it.
"""
_singleton = None
def __new__(cls) -> "_Auto":
if _Auto._singleton is None:
_Auto._singleton = super().__new__(cls)
return _Auto._singleton
def __repr__(self) -> str:
return "AUTO"
AUTO = _Auto()
"""
Sentinel to indicate the lack of a value when ``None`` is ambiguous.
"""
def _type2name(value: str | Any) -> str:
"""
Return either *value* if it is a str or else its type name.
"""
if isinstance(value, str):
return value
return type(value).__name__
[docs]
@dataclasses.dataclass(frozen=True)
class OptionInfo:
"""
Information about (possibly nested) option attributes.
Each instance represents a single attribute of an apps's settings class.
"""
#: The option's settings class. This is either the root settings class or a nested
#: one.
parent_cls: type
#: Dotted path to the option name relative to the root settings class.
path: OptionPath
#: The option's name (last component of :attr:`path`)
name: str = dataclasses.field(init=False)
#: The option's type
cls: type
#: the option's default value
default: Any
#: Whether or not the option has a default value.
has_no_default: bool
#: Whether or not the default value is a factory function.
default_is_factory: bool
#: Whether or not the option is a secret.
is_secret: bool = False
#: An optional explicit converter for the option value
converter: Callable[[Any], Any] | None = None
#: Additional metadata.
metadata: dict[Any, Any] = dataclasses.field(default_factory=dict)
#: Options for child elements in case *cls* is a collection (sequence or mapping).
collection_child_options: Optional["CollectionChildOptions"] = None
@property
def has_default(self) -> bool:
"""
Opposite of :attr:`has_no_default`.
"""
return not self.has_no_default
def __post_init__(self) -> None:
_prefix, _, name = self.path.rpartition(".")
object.__setattr__(self, "name", name)
OptionList = tuple[OptionInfo, ...]
"""
A flat list of all available options, including those from nested settings.
"""
[docs]
@dataclasses.dataclass(frozen=True)
class CollectionChildOptions:
"""
*Options* for child elements of *collection*.
"""
options: OptionList
collection: Literal["mapping", "sequence"]
OptionDict = MappingProxyType[OptionPath, OptionInfo]
"""
A dict version of :class:`OptionList`.
"""
[docs]
class LoadedValue(NamedTuple):
"""
A container for a loaded option value and the meta data of the originating loader.
"""
value: Any
"""
The loaded option value.
"""
loader_meta: LoaderMeta
"""
Meta data of the loader that loaded the corresponding value.
"""
[docs]
@dataclasses.dataclass(frozen=True)
class LoadedSettings:
"""
A container for the settings loaded by a single loader, and the meta data of that
loader.
"""
settings: SettingsDict
"""
The loaded settings values.
"""
meta: LoaderMeta
"""
Meta data of the loader that loaded the settings.
"""
MergedSettings = dict[OptionPath, LoadedValue]
"""
A dict that maps a dotted option path to a loaded option value.
The values may come from different loaders, so each option values stores the meta data
of it's loader.
"""
[docs]
class SecretStr(str):
"""
A subclass of :class:`str` that masks the output of :func:`repr()`.
It is less secure than :class:`Secret` but is a drop-in replacement for normal
strings.
The main use case is avoiding accidental secrets leakage via tracebacks.
It also helps to enforce secret usage via Typing.
It does **not help** when you:
- :func:`print()` it
- :class:`str` it
- Log it
- Use it in an f-string (``f"{val}"``)
.. versionadded:: 2.0.0
"""
def __repr__(self) -> str:
"""
Return a secret representation if a non-empty value is set, else a repr for an
empty string.
"""
return f"{SECRET_REPR!r}" if self else "''"
[docs]
class Secret(Generic[T]):
"""
A secret wrapper around any value.
It makes it very hard to accidentally leak the secret, even when printing or logging
it.
You need to explicitly call :meth:`get_secret_value()` to get the wrapped value.
Thus, it is no drop-in replacement for the wrapped data.
See :class:`SecretStr` if you need a drop-in replacement for strings, even if it is
not quite as safe.
You can use :class:`bool` to get the boolean value of the wrapped secret. Other
protocol methods (e.g., for length or comparison operators) are not implemented.
.. versionadded:: 2.0.0
"""
def __init__(self, secret_value: T) -> None:
self._is_collection = isinstance(secret_value, Collection)
self._secret_value = secret_value
def __bool__(self) -> bool:
"""
Return the boolean representation of the stored secret.
"""
return bool(self._secret_value)
def __repr__(self) -> str:
"""
Return a secret representation if a non-empty value is set, else a repr for an
empty string.
"""
r = repr(
self._secret_value
if not self._secret_value and self._is_collection
else SECRET_REPR
)
return f"{self.__class__.__name__}({r})"
def __str__(self) -> str:
"""
Return a secret representation if a non-empty value is set, else an empty
string.
"""
return str(
self._secret_value
if not self._secret_value and self._is_collection
else SECRET_REPR
)
[docs]
def get_secret_value(self) -> T:
"""
Return the wrapped secret value.
"""
return self._secret_value
class SecretRepr:
def __call__(self, v: Any) -> str:
return repr(v if not v and isinstance(v, Collection) else SECRET_REPR)
def __repr__(self) -> str:
return "***"
class NewTypeLike(Protocol):
__supertype__: type
[docs]
def is_new_type(obj: Any) -> "TypeGuard[NewTypeLike]":
"""
Return ``True`` if *obj* is a :class:`~typing.NewType`.
"""
return isinstance(obj, NewType)
SECRETS_TYPES = (Secret, SecretStr)
"""
Types that mask the repr of their values.
"""