Skip to content

BaseConfig

Module: terminaltexteffects.engine.base_config

Base effect configuration classes.

BaseConfig works with argutils.ArgSpec field defaults to define effect configuration options and populate an argparse.ArgumentParser.

BaseConfig._build_config constructs config instances either from a parsed argparse.Namespace or from the default values stored on each field's ArgSpec.

BaseConfig dataclass

Base configuration class for effects.

This class serves as a base for all effect configurations, providing a common interface for argument parser population and configuration building. Effect config classes are created via _build_config, which reads values from a parsed argparse.Namespace and falls back to their declared dataclass defaults. Any config class intended to be used to populate a subparser must define a parser_spec attribute with type argutils.ParserSpec.

Source code in terminaltexteffects/engine/base_config.py
@dataclass
class BaseConfig:
    """Base configuration class for effects.

    This class serves as a base for all effect configurations, providing a common
    interface for argument parser population and configuration building. Effect config
    classes are created via `_build_config`, which reads values from a parsed
    `argparse.Namespace` and falls back to their declared dataclass defaults. Any config
    class intended to be used to populate a subparser must define a `parser_spec`
    attribute with type `argutils.ParserSpec`.
    """

    def __setattr__(self, name: str, value: typing.Any) -> None:
        """Normalize values assigned to `ArgSpec`-backed configuration fields."""
        field = getattr(type(self), "__dataclass_fields__", {}).get(name)
        spec = field.default if field is not None else None
        if not isinstance(spec, argutils.ArgSpec):
            object.__setattr__(self, name, value)
            return
        if value is spec and name not in self.__dict__:
            object.__setattr__(self, name, value)
            return
        if isinstance(value, argutils.ArgSpec):
            msg = f"Invalid value for '{name}': an ArgSpec cannot be used as a configuration value."
            raise ValueError(msg)  # noqa: TRY004
        object.__setattr__(self, name, self._normalize_value(name, spec, value))

    @staticmethod
    def _normalize_value(name: str, spec: argutils.ArgSpec, value: typing.Any) -> typing.Any:
        """Normalize one field value and convert parser errors to configuration errors."""
        try:
            return spec.normalize(value)
        except (argparse.ArgumentTypeError, TypeError, ValueError) as error:
            msg = f"Invalid value for '{name}': {error}"
            raise ValueError(msg) from error

    def __post_init__(self) -> None:
        """Replace unprovided `ArgSpec` field values with their defaults.

        This makes direct configuration construction suitable for library use
        while preserving `ArgSpec` values on the class for parser generation.
        """
        for field in fields(self):
            spec = field.default
            value = getattr(self, field.name)
            if isinstance(spec, argutils.ArgSpec) and value is spec:
                self._normalize_value(field.name, spec, spec.default)
                object.__setattr__(self, field.name, spec.default)

    @classmethod
    def _populate_parser(cls, parser: argparse.ArgumentParser | argparse._SubParsersAction) -> None:
        """Populate the argument parser with the config class's argument specs.

        If `parser` is a subparser collection, `cls.parser_spec` is used to create
        the subparser before adding field argument specs. Config classes used in that mode
        must define `parser_spec`.

        Raises:
            AttributeError: If `parser` is a subparser collection and `cls.parser_spec`
                is not defined.

        """
        if isinstance(parser, argparse._SubParsersAction):
            parser = parser.add_parser(**vars(cls.parser_spec))  # pyright: ignore[reportAttributeAccessIssue]
            parser.formatter_class = argutils.CustomFormatter  # pyright: ignore[reportAttributeAccessIssue]

        assert isinstance(parser, argparse.ArgumentParser)
        for field in fields(cls):
            if field.name == "parser_spec":
                continue
            spec = field.default
            assert isinstance(spec, argutils.ArgSpec)
            add_args_sig = {k: v for k, v in vars(spec).items() if v is not argutils._MISSING}
            default_formatter = add_args_sig.pop("default_formatter", None)
            action = parser.add_argument(add_args_sig.pop("name"), **add_args_sig)
            if default_formatter is not None:
                action.__dict__["default_formatter"] = default_formatter

    @classmethod
    def _build_config(cls: type[CONFIG], parsed_args: argparse.Namespace | None = None) -> CONFIG:
        """Build a config instance from parsed arguments or `ArgSpec` defaults.

        When `parsed_args` is provided, matching namespace attributes are used first.
        Missing `ArgSpec` fields are omitted so dataclass defaults follow the same
        resolution path as direct construction. When `parsed_args` is `None`, the
        config is constructed directly from its dataclass defaults.

        Args:
            parsed_args (argparse.Namespace | None): Parsed CLI arguments, or None to use
                `ArgSpec` defaults only.

        Raises:
            AttributeError: If a required config field is missing from `parsed_args` and
                has no dataclass default or default factory.

        Returns:
            CONFIG: A populated config instance.

        """
        if parsed_args is not None:
            config_args: dict[str, typing.Any] = {}
            for field in fields(cls):
                if field.name == "parser_spec":
                    continue
                if hasattr(parsed_args, field.name):
                    value = getattr(parsed_args, field.name)
                    if isinstance(field.default, argutils.ArgSpec) and value is field.default.default:
                        continue
                    config_args[field.name] = value
                elif (
                    isinstance(field.default, argutils.ArgSpec)
                    or field.default is not MISSING
                    or field.default_factory is not MISSING
                ):
                    continue
                else:
                    msg = f"Missing required config field '{field.name}' for {cls.__name__} in parsed arguments."
                    raise AttributeError(msg)
            return cls(**config_args)
        return cls()

__post_init__()

Replace unprovided ArgSpec field values with their defaults.

This makes direct configuration construction suitable for library use while preserving ArgSpec values on the class for parser generation.

Source code in terminaltexteffects/engine/base_config.py
def __post_init__(self) -> None:
    """Replace unprovided `ArgSpec` field values with their defaults.

    This makes direct configuration construction suitable for library use
    while preserving `ArgSpec` values on the class for parser generation.
    """
    for field in fields(self):
        spec = field.default
        value = getattr(self, field.name)
        if isinstance(spec, argutils.ArgSpec) and value is spec:
            self._normalize_value(field.name, spec, spec.default)
            object.__setattr__(self, field.name, spec.default)

__setattr__(name, value)

Normalize values assigned to ArgSpec-backed configuration fields.

Source code in terminaltexteffects/engine/base_config.py
def __setattr__(self, name: str, value: typing.Any) -> None:
    """Normalize values assigned to `ArgSpec`-backed configuration fields."""
    field = getattr(type(self), "__dataclass_fields__", {}).get(name)
    spec = field.default if field is not None else None
    if not isinstance(spec, argutils.ArgSpec):
        object.__setattr__(self, name, value)
        return
    if value is spec and name not in self.__dict__:
        object.__setattr__(self, name, value)
        return
    if isinstance(value, argutils.ArgSpec):
        msg = f"Invalid value for '{name}': an ArgSpec cannot be used as a configuration value."
        raise ValueError(msg)  # noqa: TRY004
    object.__setattr__(self, name, self._normalize_value(name, spec, value))

FinalGradientDirectionArg dataclass

Bases: ArgSpec

Argument specification for selecting the final text gradient direction.

Source code in terminaltexteffects/engine/base_config.py
@dataclass(frozen=True)
class FinalGradientDirectionArg(argutils.ArgSpec):
    """Argument specification for selecting the final text gradient direction."""

    name: str = "--final-gradient-direction"
    type: typing.Callable[[str], Gradient.Direction] = argutils.GradientDirection.type_parser
    default: Gradient.Direction = Gradient.Direction.VERTICAL
    metavar: str = argutils.GradientDirection.METAVAR
    help: str = "Direction of the final gradient across the text."

FinalGradientFramesArg dataclass

Bases: ArgSpec

Argument specification for selecting the final text gradient frames.

Source code in terminaltexteffects/engine/base_config.py
@dataclass(frozen=True)
class FinalGradientFramesArg(argutils.ArgSpec):
    """Argument specification for selecting the final text gradient frames."""

    name: str = "--final-gradient-frames"
    type: typing.Callable[[str], int] = argutils.PositiveInt.type_parser
    default: int = 5
    metavar: str = argutils.PositiveInt.METAVAR
    help: str = "Number of frames to display each gradient step. Increase to slow down the gradient animation."

FinalGradientStepsArg dataclass

Bases: ArgSpec

Argument specification for selecting the final text gradient steps.

Source code in terminaltexteffects/engine/base_config.py
@dataclass(frozen=True)
class FinalGradientStepsArg(argutils.ArgSpec):
    """Argument specification for selecting the final text gradient steps."""

    name: str = "--final-gradient-steps"
    type: typing.Callable[[str], int] = argutils.PositiveInt.type_parser
    nargs: str = "+"
    action: type[argutils.TupleAction] = argutils.TupleAction
    default: tuple[int, ...] | int = 12
    metavar: str = argutils.PositiveInt.METAVAR
    help: str = (
        "Space separated, unquoted, list of the number of gradient steps to use. "
        "More steps will create a smoother and longer gradient animation."
    )

FinalGradientStopsArg dataclass

Bases: ArgSpec

Argument specification for selecting the final text gradient stops.

Source code in terminaltexteffects/engine/base_config.py
@dataclass(frozen=True)
class FinalGradientStopsArg(argutils.ArgSpec):
    """Argument specification for selecting the final text gradient stops."""

    name: str = "--final-gradient-stops"
    type: typing.Callable[[str], Color] = argutils.ColorArg.type_parser
    nargs: str = "+"
    action: type[argutils.TupleAction] = argutils.TupleAction
    default: tuple[Color, ...] = (Color("#8A008A"), Color("#00D1FF"), Color("#FFFFFF"))
    metavar: str = argutils.ColorArg.METAVAR
    help: str = (
        "Space separated, unquoted, list of colors for the character gradient (applied across the canvas). "
        "If only one color is provided, the characters will be displayed in that color."
    )