Source code for typed_settings.cls_attrs

"""
Helpers for and additions to :mod:`attrs`.
"""

import sys
from collections.abc import Mapping
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    Optional,
    overload,
)

import attr  # The old namespaces is needed in "combine()"
import attrs

from . import constants, types


if TYPE_CHECKING:
    from attr import (  # type: ignore[attr-defined]
        _T,
        _ConverterType,
        _OnSetAttrArgType,
        _ReprArgType,
        _ValidatorArgType,
    )


__all__ = [
    "SECRET",
    "evolve",
    "option",
    "secret",
    "settings",
]


SECRET = types.SecretRepr()


settings = attrs.define
"""An alias to :func:`attrs.define()`"""


@overload
def option(
    *,
    default: None = ...,
    validator: None = ...,
    repr: "_ReprArgType" = ...,
    hash: Optional[bool] = ...,
    init: bool = ...,
    metadata: Optional[dict[Any, Any]] = ...,
    converter: None = ...,
    factory: None = ...,
    kw_only: bool = ...,
    eq: Optional[bool] = ...,
    order: Optional[bool] = ...,
    on_setattr: Optional["_OnSetAttrArgType"] = ...,
    help: Optional[str] = ...,
    click: Optional[dict[str, Any]] = ...,
    argparse: Optional[dict[str, Any]] = ...,
) -> Any: ...


# This form catches an explicit None or no default and infers the type from the
# other arguments.
@overload
def option(
    *,
    default: None = ...,
    validator: "Optional[_ValidatorArgType[_T]]" = ...,
    repr: "_ReprArgType" = ...,
    hash: Optional[bool] = ...,
    init: bool = ...,
    metadata: Optional[dict[Any, Any]] = ...,
    converter: Optional["_ConverterType"] = ...,
    factory: "Optional[Callable[[], _T]]" = ...,
    kw_only: bool = ...,
    eq: Optional[bool] = ...,
    order: Optional[bool] = ...,
    on_setattr: "Optional[_OnSetAttrArgType]" = ...,
    help: Optional[str] = ...,
    click: Optional[dict[str, Any]] = ...,
    argparse: Optional[dict[str, Any]] = ...,
) -> "_T": ...


# This form catches an explicit default argument.
@overload
def option(
    *,
    default: "_T",
    validator: "Optional[_ValidatorArgType[_T]]" = ...,
    repr: "_ReprArgType" = ...,
    hash: Optional[bool] = ...,
    init: bool = ...,
    metadata: Optional[dict[Any, Any]] = ...,
    converter: "Optional[_ConverterType]" = ...,
    factory: "Optional[Callable[[], _T]]" = ...,
    kw_only: bool = ...,
    eq: Optional[bool] = ...,
    order: Optional[bool] = ...,
    on_setattr: "Optional[_OnSetAttrArgType]" = ...,
    help: Optional[str] = ...,
    click: Optional[dict[str, Any]] = ...,
    argparse: Optional[dict[str, Any]] = ...,
) -> "_T": ...


# This form covers type=non-Type: e.g. forward references (str), Any
@overload
def option(
    *,
    default: Optional["_T"] = ...,
    validator: "Optional[_ValidatorArgType[_T]]" = ...,
    repr: "_ReprArgType" = ...,
    hash: Optional[bool] = ...,
    init: bool = ...,
    metadata: Optional[dict[Any, Any]] = ...,
    converter: "Optional[_ConverterType]" = ...,
    factory: "Optional[Callable[[], _T]]" = ...,
    kw_only: bool = ...,
    eq: Optional[bool] = ...,
    order: Optional[bool] = ...,
    on_setattr: "Optional[_OnSetAttrArgType]" = ...,
    help: Optional[str] = ...,
    click: Optional[dict[str, Any]] = ...,
    argparse: Optional[dict[str, Any]] = ...,
) -> Any: ...


[docs] def option( # type: ignore[no-untyped-def] *, default=attrs.NOTHING, validator=None, repr=True, hash=None, init=True, metadata=None, converter=None, factory=None, kw_only=False, eq=None, order=None, on_setattr=None, help=None, click=None, argparse=None, ): """ An alias to :func:`attrs.field()`. """ metadata = _get_metadata(metadata, help, click, argparse) return attrs.field( default=default, validator=validator, repr=repr, hash=hash, init=init, metadata=metadata, converter=converter, factory=factory, kw_only=kw_only, eq=eq, order=order, on_setattr=on_setattr, )
@overload def secret( *, default: None = ..., validator: None = ..., repr: types.SecretRepr = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Optional[dict[Any, Any]] = ..., converter: None = ..., factory: None = ..., kw_only: bool = ..., eq: Optional[bool] = ..., order: Optional[bool] = ..., on_setattr: "Optional[_OnSetAttrArgType]" = ..., help: Optional[str] = ..., click: Optional[dict[str, Any]] = ..., argparse: Optional[dict[str, Any]] = ..., ) -> Any: ... # This form catches an explicit None or no default and infers the type from the # other arguments. @overload def secret( *, default: None = ..., validator: "Optional[_ValidatorArgType[_T]]" = ..., repr: types.SecretRepr = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Optional[dict[Any, Any]] = ..., converter: "Optional[_ConverterType]" = ..., factory: "Optional[Callable[[], _T]]" = ..., kw_only: bool = ..., eq: Optional[bool] = ..., order: Optional[bool] = ..., on_setattr: "Optional[_OnSetAttrArgType]" = ..., help: Optional[str] = ..., click: Optional[dict[str, Any]] = ..., argparse: Optional[dict[str, Any]] = ..., ) -> "_T": ... # This form catches an explicit default argument. @overload def secret( *, default: "_T", validator: "Optional[_ValidatorArgType[_T]]" = ..., repr: types.SecretRepr = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Optional[dict[Any, Any]] = ..., converter: "Optional[_ConverterType]" = ..., factory: "Optional[Callable[[], _T]]" = ..., kw_only: bool = ..., eq: Optional[bool] = ..., order: Optional[bool] = ..., on_setattr: "Optional[_OnSetAttrArgType]" = ..., help: Optional[str] = ..., click: Optional[dict[str, Any]] = ..., argparse: Optional[dict[str, Any]] = ..., ) -> "_T": ... # This form covers type=non-Type: e.g. forward references (str), Any @overload def secret( *, default: "Optional[_T]" = ..., validator: "Optional[_ValidatorArgType[_T]]" = ..., repr: types.SecretRepr = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Optional[dict[Any, Any]] = ..., converter: "Optional[_ConverterType]" = ..., factory: "Optional[Callable[[], _T]]" = ..., kw_only: bool = ..., eq: Optional[bool] = ..., order: Optional[bool] = ..., on_setattr: "Optional[_OnSetAttrArgType]" = ..., help: Optional[str] = ..., click: Optional[dict[str, Any]] = ..., argparse: Optional[dict[str, Any]] = ..., ) -> Any: ...
[docs] def secret( # type: ignore[no-untyped-def] *, default=attrs.NOTHING, validator=None, repr=SECRET, hash=None, init=True, metadata=None, converter=None, factory=None, kw_only=False, eq=None, order=None, on_setattr=None, help=None, click=None, argparse=None, ): """ An alias to :func:`option()` but with a default repr that hides screts. When printing a settings instances, secret settings will represented with `***` istead of their actual value. See Also: All arguments are describted here: - :func:`option()` - :func:`attrs.field()` Example: >>> from typed_settings import settings, secret >>> >>> @settings ... class Settings: ... password: str = secret() ... >>> Settings(password="1234") Settings(password='*******') """ metadata = _get_metadata(metadata, help, click, argparse) return attrs.field( default=default, validator=validator, repr=repr, hash=hash, init=init, metadata=metadata, converter=converter, factory=factory, kw_only=kw_only, eq=eq, order=order, on_setattr=on_setattr, )
def _get_metadata( metadata: Optional[dict[str, Any]], help: Optional[str], click: Optional[dict[str, Any]], argparse: Optional[dict[str, Any]], ) -> dict[str, Any]: click_config = {"help": help} if click: click_config.update(click) argparse_config = {"help": help} if argparse: argparse_config.update(argparse) if metadata is None: metadata = {} ts_meta = metadata.setdefault(constants.METADATA_KEY, {}) ts_meta["help"] = help ts_meta[constants.CLICK_METADATA_KEY] = click_config ts_meta[constants.ARGPARSE_METADATA_KEY] = argparse_config return metadata
[docs] def evolve(inst: attrs.AttrsInstance, **changes: Any) -> attrs.AttrsInstance: """ Create a new instance, based on *inst* with *changes* applied. If the old value of an attribute is an ``attrs`` class and the new value is a dict, the old value is updated recursively. .. warning:: This function is very similar to :func:`attrs.evolve()`, but the ``attrs`` version is not updating values recursively. Instead, it will just replace ``attrs`` instances with a dict. Args: inst: Instance of a class with ``attrs`` attributes. changes: Keyword changes in the new copy. Return: A copy of *inst* with *changes* incorporated. Raise: TypeError: If *attr_name* couldn't be found in the class ``__init__``. attrs.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` class. .. versionadded:: 1.0.0 """ cls = inst.__class__ attribs = attrs.fields(cls) # type: ignore[misc] for a in attribs: if not a.init: continue attr_name = a.name # To deal with private attributes. init_name = attr_name if attr_name[0] != "_" else attr_name[1:] old_value = getattr(inst, attr_name) if init_name not in changes: # Add original value to changes changes[init_name] = old_value elif attrs.has(old_value) and isinstance(changes[init_name], Mapping): # Evolve nested attrs classes changes[init_name] = evolve(old_value, **changes[init_name]) # type: ignore[arg-type] return cls(**changes)
[docs] def combine( name: str, base_cls: type[attrs.AttrsInstance], nested: dict[str, attrs.AttrsInstance], ) -> type[attrs.AttrsInstance]: """ Create a new class called *name* based on *base_class* with additional attributes for *nested* classes. The same effect can be achieved by manually composing settings classes. A use case for this method is to combine settings classes from dynamically loaded plugins with the base settings of the main program. Args: name: The name for the new class. base_cls: The base class from which to copy all attributes. nested: A mapping of attribute names to (settings) class instances for which to generated additional attributes. The attribute's type is the instance's type and its default value is the instance itself. Keys in this dict must not overlap with the attributes of *base_cls*. Return: The created class *name*. Raise: ValueError: If *nested* contains a key for which *base_cls* already defines an attribute. Example: >>> import typed_settings as ts >>> >>> @ts.settings ... class Nested1: ... a: str = "" >>> >>> @ts.settings ... class Nested2: ... a: str = "" >>> >>> # Static composition >>> @ts.settings ... class Composed1: ... a: str = "" ... n1: Nested1 = Nested1() ... n2: Nested2 = Nested2() ... >>> Composed1() Composed1(a='', n1=Nested1(a=''), n2=Nested2(a='')) >>> >>> # Dynamic composition >>> @ts.settings ... class BaseSettings: ... a: str = "" >>> >>> Composed2 = ts.combine( ... "Composed2", ... BaseSettings, ... {"n1": Nested1(), "n2": Nested2()}, ... ) >>> Composed2() Composed2(a='', n1=Nested1(a=''), n2=Nested2(a='')) .. versionadded:: 1.1.0 """ attribs = { a.name: attr.attrib( default=a.default, validator=a.validator, repr=a.repr, hash=a.hash, init=a.init, metadata=a.metadata, type=a.type, converter=a.converter, kw_only=a.kw_only, eq=a.eq, order=a.order, on_setattr=a.on_setattr, ) for a in attr.fields(base_cls) # type: ignore[misc] } annotations = dict(base_cls.__annotations__) for aname, default in nested.items(): if aname in attribs: raise ValueError(f"Duplicate attribute for nested class: {aname}") attribs[aname] = attr.attrib(default=default, type=default.__class__) annotations[aname] = default.__class__ try: globalns = sys.modules[base_cls.__module__].__dict__ except KeyError: # pragma: no cover globalns = None cls = attr.make_class(name, attribs) cls.__annotations__ = annotations cls.__doc__ = base_cls.__doc__ # Store globals in class so that they can later be used, # see ".cls_utils.Attrs.iter_fields()". cls.__globals__ = globalns # type: ignore[attr-defined] return cls